cypress-io / cypress-react-unit-test

Unit test React components using Cypress
https://glebbahmutov.com/blog/my-vision-for-component-tests/
679 stars 78 forks source link

React is not defined - unit testing component in Nextjs project #155

Closed vicusbass closed 4 years ago

vicusbass commented 4 years ago

When running a unit test for a React component in a Nextjs project, mounting the component fails because React is not defined. My guess is it has something to do with the way Nextjs is handling webpack. It works correctly only if React is explicitely imported in the component module, which is not required by Nextjs.

Uncaught ReferenceError: React is not defined
This error originated from your application code, not from Cypress.
When Cypress detects uncaught errors originating from your application it will automatically fail the current test.
This behavior is configurable, and you can choose to turn this off by listening to the `uncaught:exception` event.
    at StatsRow (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:29333:3)
    at renderWithHooks (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:15463:18)
    at mountIndeterminateComponent (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:18142:13)
    at beginWork (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:19256:16)
    at HTMLUnknownElement.callCallback (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:848:14)
    at Object.invokeGuardedCallbackDev (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:897:16)
    at invokeGuardedCallback (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:952:31)
    at beginWork$1 (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:23863:7)
    at performUnitOfWork (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:22817:12)
    at workLoopSync (http://localhost:3000/__cypress/tests?p=pages/components/StatsRow-spec.js-451:22790:22)
From previous event:
    at run (http://localhost:3000/__cypress/runner/cypress_runner.js:144558:19)
    at Object.cy.<computed> [as then] (http://localhost:3000/__cypress/runner/cypress_runner.js:144970:11)
    at Context.runnable.fn (http://localhost:3000/__cypress/runner/cypress_runner.js:145202:24)
    at callFn (http://localhost:3000/__cypress/runner/cypress_runner.js:88335:21)
    at Test.../driver/node_modules/mocha/lib/runnable.js.Runnable.run (http://localhost:3000/__cypress/runner/cypress_runner.js:88322:7)
    at http://localhost:3000/__cypress/runner/cypress_runner.js:149207:28
From previous event:
    at Object.onRunnableRun (http://localhost:3000/__cypress/runner/cypress_runner.js:149195:20)
    at $Cypress.action (http://localhost:3000/__cypress/runner/cypress_runner.js:141473:61)
    at Test.Runnable.run (http://localhost:3000/__cypress/runner/cypress_runner.js:148003:13)
    at Runner.../driver/node_modules/mocha/lib/runner.js.Runner.runTest (http://localhost:3000/__cypress/runner/cypress_runner.js:88994:10)
    at http://localhost:3000/__cypress/runner/cypress_runner.js:89120:12
    at next (http://localhost:3000/__cypress/runner/cypress_runner.js:88903:14)
    at http://localhost:3000/__cypress/runner/cypress_runner.js:88913:7
    at next (http://localhost:3000/__cypress/runner/cypress_runner.js:88815:14)
    at http://localhost:3000/__cypress/runner/cypress_runner.js:88881:5
    at timeslice (http://localhost:3000/__cypress/runner/cypress_runner.js:82807:27)
bahmutov commented 4 years ago

Is this a fresh Next.js project? I had worked a little bit on Next, but the way it exposes Webpack options is async, so it is not really that easy :(

vicusbass commented 4 years ago

@bahmutov Yes, a new project, latest Next.js. Unfortunately, it's not a public project. I could create a "test project" if needed.

denniskortsch commented 4 years ago

We have an ejected react-scripts app with typescript support and we get react is not defined error as well. Would love help to make it work...

Edit: We actually resolved after DEBUG=cypress-react-unit-test,find-webpack yarn cy:open showed that it could not find the webpack config. Problem was we actually did not have react-scripts as a dependency anymore. After adding it, cypress/find-webpack can find the config

bahmutov commented 4 years ago

Relevant: https://github.com/vercel/next.js/blob/86160a5190c50ea315c7ba91d77dfb51c42bc65f/packages/next-plugin-storybook/preset.js

Can something like this work?

const { PHASE_PRODUCTION_BUILD } = require('next/constants')
const { findPagesDir } = require('next/dist/lib/find-pages-dir')
const loadConfig = require('next/dist/next-server/server/config').default
const getWebpackConfig = require('next/dist/build/webpack-config').default

const CWD = process.cwd()

async function webpackFinal(config = {}) {
  const pagesDir = findPagesDir(CWD)
  const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, CWD)
  const nextWebpackConfig = await getWebpackConfig(CWD, {
    pagesDir,
    entrypoints: {},
    isServer: false,
    target: 'server',
    config: nextConfig,
    // buildId: 'storybook',
  })

  config.plugins = [...nextWebpackConfig.plugins]

  config.resolve = {
    ...config.resolve,
    ...nextWebpackConfig.resolve,
  }

  return config
}

const config = {
  plugins: []
}
webpackFinal(config).then(console.log, console.error)
sebkasanzew commented 4 years ago

I have a similar issue and am trying to get cypress-react-unit-test working with my Next.js project.

Relevant: https://github.com/vercel/next.js/blob/86160a5190c50ea315c7ba91d77dfb51c42bc65f/packages/next-plugin-storybook/preset.js

Can something like this work?

const { PHASE_PRODUCTION_BUILD } = require('next/constants')
const { findPagesDir } = require('next/dist/lib/find-pages-dir')
const loadConfig = require('next/dist/next-server/server/config').default
const getWebpackConfig = require('next/dist/build/webpack-config').default

const CWD = process.cwd()

async function webpackFinal(config = {}) {
  const pagesDir = findPagesDir(CWD)
  const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, CWD)
  const nextWebpackConfig = await getWebpackConfig(CWD, {
    pagesDir,
    entrypoints: {},
    isServer: false,
    target: 'server',
    config: nextConfig,
    // buildId: 'storybook',
  })

  config.plugins = [...nextWebpackConfig.plugins]

  config.resolve = {
    ...config.resolve,
    ...nextWebpackConfig.resolve,
  }

  return config
}

const config = {
  plugins: []
}
webpackFinal(config).then(console.log, console.error)

This does output a webpack config. But when I copy the output into a webpack.config.js file and load it like in this example, I get the following error in a cypress unit test:

WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
 - configuration.plugins[0] misses the property 'apply'.
   function
   -> The run point of the plugin, required method.
 - configuration.plugins[1] misses the property 'apply'.
   function
   -> The run point of the plugin, required method.
 - configuration.resolve.plugins[0] misses the property 'apply'.
   function
   -> The run point of the plugin, required method.
itstheandre commented 4 years ago

Hello there! Has there been any development on this issue? Just wondering. Its an amazing lib and really want to make it my all stop shop for testing my react / nextjs apps. Thanks!

leosuncin commented 4 years ago

I manage to get working next.js with cypress https://github.com/leosuncin/mui-next-ts/tree/wip/crud-api I use typescript and absolute paths, but the main parts are in the webpack config

https://github.com/leosuncin/mui-next-ts/blob/2f5ddcb8c38878f2c042784c767cd31f788db398/cypress/plugins/index.js#L18-L43

const options = {
  webpackOptions: {
    resolve: {
      extensions: ['.ts', '.tsx', '.js'], // Not only load *.spec.js files also *.spec.ts and *.spec.tsx
      alias: { // Typescript alias, I defined here because Cypress fail to load when only set in tsconfig.json
        components: path.resolve(__dirname, '../../components/'),
        services: path.resolve(__dirname, '../../services/'),
        libs: path.resolve(__dirname, '../../libs/'),
        machines: path.resolve(__dirname, '../../machines/'),
        middlewares: path.resolve(__dirname, '../../middlewares/'),
        hooks: path.resolve(__dirname, '../../hooks/'),
        pages: path.resolve(__dirname, '../../pages/'),
      },
      modules: [path.resolve(__dirname, '../..'), 'node_modules'], // THIS IS IMPORTANT: Make works the absolute paths inside components
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          loader: 'ts-loader', // Load typescript files
          options: { transpileOnly: true },
        },
      ],
    },
  },
};

The tsconfig.json

https://github.com/leosuncin/mui-next-ts/blob/2f5ddcb8c38878f2c042784c767cd31f788db398/cypress/tsconfig.json#L5-L20

{
  "extends": "../tsconfig.json",
  "include": ["**/*.ts"],
  "exclude": ["../node_modules"],
  "compilerOptions": {
    "isolatedModules": false,
    "target": "es6",
    "module": "es6",
    "jsx": "react", // THIS IS IMPORTANT: Enable JSX transpilation
    "baseUrl": "..", // Set baseUrl to up directory
    "paths": { // Absolute paths
      "components": ["components/*"],
      "services": ["services/*"],
      "libs": ["libs/*"],
      "machines": ["machines/*"],
      "middlewares": ["middlewares/*"],
      "hooks": ["hooks/*"],
      "pages": ["pages/*"],
      "utils": ["utils/*"]
    },
    "types": [
      "cypress",
      "@testing-library/cypress"
    ]
  }
}

The simple solution to React is not defined is to import React inside every component (Next.js internally auto import in every component by default), but I got the same error with Jest and found the easy one is to import React.

Still I have some minor problems, like I can't import components using absolute paths inside test components

https://github.com/leosuncin/mui-next-ts/blob/wip/crud-api/cypress/component/login-form.test.tsx

I still experimenting with this

itstheandre commented 4 years ago

Hey @leosuncin ,

Two questions: 1 - if I dont use the absolute paths, I guess i dont have to worry about that right? And then same things for the aliases in the webpack options, right? 2 - Why is the baseUrl .. ? Just out of curiosity.

Will try this out today

leosuncin commented 4 years ago

1- Yes, you don't need to worry, you can omit that part

const options = {
  webpackOptions: {
    resolve: {
      extensions: ['.ts', '.tsx', '.js'], // Not only load *.spec.js files also *.spec.ts and *.spec.tsx
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          loader: 'ts-loader', // Load typescript files
          options: { transpileOnly: true },
        },
      ],
    },
  },
};
{
  "extends": "../tsconfig.json",
  "include": ["**/*.ts"],
  "exclude": ["../node_modules"],
  "compilerOptions": {
    "isolatedModules": false,
    "target": "es6",
    "module": "es6",
    "jsx": "react", // THIS IS IMPORTANT: Enable JSX transpilation
    "types": [
      "cypress",
      "@testing-library/cypress"
    ]
  }
}

2- Is the project root, under cypress directory I need to go up one directory

# Project root
├── babel.config.js
├── components
│   ├── cards
│   └── todo
├── cypress
│   ├── component
│   ├── fixtures
│   ├── integration
│   ├── plugins
│   ├── README.md
│   ├── screenshots
│   ├── support
│   ├── tsconfig.json <-- You are here
│   └── videos
├── cypress.json
├── cypress-unit.json
├── jest.config.js
├── jest.setup.ts
├── next.config.js
├── next-env.d.ts
├── package.json
├── pages
│   ├── api
│   ├── _app.tsx
│   ├── _document.tsx
│   └── index.tsx
├── README.md
├── __tests__
│   ├── components
│   └── pages
├── tsconfig.json
└── tsconfig.test.json
itstheandre commented 4 years ago

I had to install a couple of dependencies I wasnt expecting but it is working 🙌

I have two questions @leosuncin How would you test something like a router.push from nextjs? Just curious on how to do that. trying to test a login that after successful login that redirects

And another thing i dont get (might be more of a cypress thing instead or just testing) - but you are passing the onSubmit directly to the login component. If I have this inside the whole component and the logic is inside the component itself how would you test that that functon works. would you mock a fetch / axios call? or would you use a stub to intercept the http calls and generate failure and success cases for the tests?

Thanks!

leosuncin commented 4 years ago

I don't know how to mock next/router I have a login form that have a Link component that throws an error because it can't find the Router provider, what I did was mute that error since I don't need to test the router behavior and the form is render anyway.

I had other test component that make network requests, so I stub the network request with cy.route; also I enabled the experimental fetch polyfil, since I use fetch to make the requests.

https://github.com/leosuncin/mui-next-ts/blob/wip/crud-api/cypress/component/todo.test.tsx

bahmutov commented 4 years ago

:tada: This issue has been resolved in version 4.14.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket: