Configurando Cypress com Jest em app NextJS

Salve, pessoa! Como estás?

Hoje a ideia do post é simples: Configurar o Cypress pra gerar coverage num projeto NextJS que já possui Jest.

Este post não inclui:

Vamos começar detalhando as tecnologias do projeto e depois a instalação.

Tecnologias e versões

Engine

node v16.20.0

Node Package Manager

npm v8.19.4

Dependencies

"babel-plugin-module-resolver": "^4.1.0",
"next": "^12.3.4",
"next-compose-plugins": "^2.2.0",
"next-optimized-images": "^2.6.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",

devDependencies

"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.1",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"babel-plugin-styled-components": "^1.10.7",
"jest": "^26.0.1",
"jest-css-modules": "^2.1.0",
"jest-junit": "^16.0.0",

Instalação do Cypress e dependências de desenvolvimento necessárias

npm install @cypress/code-coverage babel-plugin-istanbul cypress@12.17.2 istanbul-lib-coverage nyc cypress-wait-until --save-dev

Confira as libs:

Lembre-se: Além da flag --save-dev para instalar como dev dependencies, se você instala as dependências do seu projeto com a flag --legacy-peer-deps, não esqueça de inseri-la no final do comando.

Alterações no package.json

Scripts

  "scripts": {
    "dev": "CYPRESS_ENV=true PROJECT_ENV=development PORT=3000 next dev",
    "pretest": "rm -rf .nyc_output || true",
    "test:e2e": "npm run pretest && nyc cypress open",
    "test:e2e-headless": "npm run pretest && nyc cypress run --headed",
  },

Acima temos os comandos:

Para efeitos de documentação, o erro acusado pelo Jest antes da condicional com CYPRESS_ENV:

Duplicate plugin/preset detected.
    If you'd like to use two separate instances of a plugin,
    they need separate names, e.g.

Configuração do nyc

  "nyc": {
    "report-dir": "cypress-coverage",
    "extension": [
      ".js"
    ],
    "all": true,
    "include": [
      "src/containers/**/*.js"
    ],
    "exclude": [
      "cypress/",
      "src/containers/**/*.test.js"
    ],
    "check-coverage": true,
    "excludeAfterRemap": false,
    "reporter": [
      "lcov",
      "text-summary"
    ]
  },

Novamente, vamos percorrer comando por comando:

Atenção na chave include! No meu caso vou testar somente os arquivos de containers, ou seja, aqueles que estão na minha pasta: src/containers e faremos uma hierarquia de diretórios semelhante dentro da pasta cypress alguns passos a frente.

Alterações no babel.config.js

Neste projeto estou utilizando o arquivo babel.config.js na pasta raíz, pode ser que no seu projeto haja um arquivo .babelrc, é válido também.
Pessoalmente não tive uma boa experiência criando os dois, não consegui rodar os testes e a aplicação corretamente.

module.exports = function babelConfig({ cache, env }) {
  cache.invalidate(() => process.env.NODE_ENV);

  const isCypress = process.env.CYPRESS_ENV === "true";

  const presets = [
    [
      "next/babel",
      {
        "preset-env": {
          modules: env("test") ? "commonjs" : "auto",
        },
      },
    ],
  ];

  const plugins = [
    [
      "module-resolver",
      {
        alias: {
          styles: "./styles",
          containers: "./src/containers",
          public: "./public",
        },
      },
    ],
    [
      "styled-components",
      {
        ssr: true,
        displayName: true,
        preprocess: false,
      },
    ],
    ["inline-react-svg"],
  ];

  if (isCypress) {
    plugins.push("istanbul");
  }

  return { presets, plugins };
};

Criando o arquivo cypress.config.js

A seguir você verá o arquivo cypress.config.js que deverá ficar na pasta raíz do seu projeto.

const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
    supportFile: "cypress/support/e2e.js",
    specPattern: "cypress/integration/containers/**/*",
    chromeWebSecurity: false,
    pageLoadTimeout: 70000,
    setupNodeEvents(on, config) {
      require("@cypress/code-coverage/task")(on, config);
      return config;
    },
    video: false,
  },
  env: {
    cypress: true,
  },
});

Criando a pasta cypress e mais arquivos de configuração

  1. Crie a pasta cypress na raíz do seu projeto;
  2. Dentro dela crie as seguintes pastas: integration, plugins, support;

Pasta integration

Dentro desta pasta criei um diretório chamado containers e coloquei meus testes dentro: admin.spec.js, sales.spec.js e, assim por diante.

Exemplo de teste:

describe("Admin page", () => {
  it("Should check if promotional banner is visible", () => {
    cy.visit("/");
    cy.contains("Promoção relâmpago");
    cy.getByDataTestId("banner-promotional").should("exist");
  });
});

Pasta plugins

É necessário criar um arquivo index.js com o seguinte código:

module.exports = (on, config) => {
  require("@cypress/code-coverage/task")(on, config);

  on(
    "file:preprocessor",
    require("@cypress/code-coverage/use-browserify-istanbul"),
  );

  // important - return config because code coverage plugin
  // modifies environment variables there
  return config;
};

Pasta support

Vamos criar três arquivos.

e2e.js

import "@cypress/code-coverage/support";

import "./commands";

index.js

import "@cypress/code-coverage/support";

after(() => {
  cy.task("coverageReport");
});

commands.js

import "cypress-wait-until";

Cypress.Commands.add("getByDataTestId", (selector, ...args) => {
  cy.get(`[data-testid=${selector}]`, ...args);
});

Cypress.on("uncaught:exception", () => false);

Conclusão

Por último mas não menos importante, pode ser que você tenha que adicionar nos seus arquivos .gitignore, .eslintignore (se você usa eslint) e jest.config.js (no parâmetro coveragePathIgnorePatterns) as pastas geradas nesse processo para serem ignoradas.

Espero que seu processo seja mais simples do que foi o meu :)