diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5acdbf75e..851064920 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,14 +7,32 @@ concurrency: cancel-in-progress: true jobs: - tests: - name: Tests + lint-styles: + name: Lint Styles runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '18' + cache: 'npm' + - run: npm ci + - run: npm run lint - - name: Start containers - run: npm run services:up + lint-commits: + name: Lint Commits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v4 + + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 - uses: actions/cache@v2 with: @@ -28,29 +46,40 @@ jobs: node-version: '18' cache: 'npm' - run: npm ci - - run: npm run dev & npx vitest run - - - name: Stop containers - if: always() - run: npm run services:down + - run: npm run test:unit - lint-styles: - name: Lint Styles + integration-and-e2e-tests: + name: Integration and E2E Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + + - name: Start containers + run: npm run services:up + + - uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- + - uses: actions/setup-node@v2 with: node-version: '18' cache: 'npm' - run: npm ci - - run: npm run lint - lint-commits: - name: Lint Commits - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + - name: Executing tests + run: npm run concurrently -- --hide next + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} with: - fetch-depth: 0 - - uses: wagoid/commitlint-github-action@v4 + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - name: Stop containers + if: always() + run: npm run services:down diff --git a/.gitignore b/.gitignore index 6897d5f46..3d5c7863d 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,7 @@ terraform.rc yarn.* .tool-versions + +# Playwright test results +test-results/ +playwright-report/ diff --git a/.prettierignore b/.prettierignore index 14285b7eb..7f76bb037 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,5 @@ node_modules pages/pocs .husky public/museu +test-results/ +playwright-report/ diff --git a/README.md b/README.md index e7911a0d5..d586c71e8 100644 --- a/README.md +++ b/README.md @@ -82,13 +82,15 @@ Por padrão, ao rodar o comando `npm run dev` será injetado dois usuários ativ ## Rodar os testes -Há várias formas de rodar os testes dependendo do que você deseja fazer, mas o primeiro passo antes de fazer qualquer alteração no projeto é rodar os testes de forma geral para se certificar que tudo está passando como esperado. O comando abaixo irá rodar todos os serviços necessários, rodar os testes e em seguida derrubar todos os serviços. +### Testes unitários + +Há várias formas de rodar os testes dependendo do que você deseja fazer, mas o primeiro passo antes de fazer qualquer alteração no projeto é rodar os testes unitários de forma geral para se certificar que tudo está passando como esperado. O comando abaixo irá rodar todos os serviços necessários, rodar os testes unitários e em seguida derrubar todos os serviços. ```bash npm test ``` -Caso queira manter os serviços e testes rodando enquanto desenvolve (e rodando novamente a cada alteração salva), use o modo `watch` com o comando abaixo: +Caso queira manter os serviços e testes unitários rodando enquanto desenvolve (e rodando novamente a cada alteração salva), use o modo `watch` com o comando abaixo: ```bash npm run test:watch:services @@ -104,7 +106,7 @@ npm run dev npm run test:watch ``` -Caso não queira executar (ou dar `watch`) em todos os testes e queira isolar arquivos específicos de teste, você pode filtrar pelo caminho. Não é necessário digitar o caminho inteiro para o arquivo e você também pode fornecer mais de um caminho, veja alguns exemplos abaixo: +Caso não queira executar (ou dar `watch`) em todos os testes unitários e queira isolar arquivos específicos de teste, você pode filtrar pelo caminho. Não é necessário digitar o caminho inteiro para o arquivo e você também pode fornecer mais de um caminho, veja alguns exemplos abaixo: ```bash # Rodar todos os testes de "users" e "status" da api "v1" @@ -125,6 +127,44 @@ Observações: - A forma como é tratado o caminho dos arquivos pode mudar dependendo do seu sistema operacional. - A forma como o seu terminal interpreta caracteres especiais como `/` ou `[` pode mudar. +### Testes E2E + +Além dos testes unitários o projeto também inclui testes de ponta a ponta (E2E) que simulam o comportamento dos usuários. Esses testes são executados usando o Playwright. + +Para executar os testes em um navegador em segundo plano, em que você não verá a interface do usuário, execute o comando: + +```bash +npm run test:e2e +``` + +Uma alternativa para execução desses testes, que te permite visualizar exatamente o que está acontecendo na interface do usuário do navegador, rode o seguinte comando: + +```bash +npm run test:e2e:headed +``` + +Agora, se você preferir executar os testes visualizando seus resultados numa timeline em que você consegue acompanhar na interface o momento da execução do script, rode: + +```bash +npm run test:e2e:ui +``` + +Com esse último comando é possível escolher qual teste executar diretamente de uma UI amigável, mas se quiser fazer isso via terminal... + +```bash +# Rodar teste por arquivo +npx playwright test login.spec.js + +# Rodar teste de um arquivo específico (procure pelo título do teste) +npx playwright test -g "should be able to login" +``` + +Depois da execução é possível consultar um relatório acessando com o comando abaixo: + +```bash +npx playwright show-report +``` + ## Formas de contribuir Você pode contribuir com o projeto de várias formas diferentes: diff --git a/package-lock.json b/package-lock.json index e56a614bf..533d3ea6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "@commitlint/cli": "18.6.1", "@commitlint/config-conventional": "18.6.2", "@faker-js/faker": "8.4.1", + "@playwright/test": "1.44.1", "@testing-library/react": "15.0.0", "@vitejs/plugin-react": "4.2.1", "autoprefixer": "10.4.17", @@ -359,9 +360,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1961,6 +1962,21 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "dev": true, + "dependencies": { + "playwright": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@popperjs/core": { "version": "2.11.7", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", @@ -13459,6 +13475,50 @@ "pathe": "^1.1.0" } }, + "node_modules/playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "dependencies": { + "playwright-core": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", @@ -18984,9 +19044,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", "dev": true }, "@babel/helper-simple-access": { @@ -20034,6 +20094,15 @@ "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true }, + "@playwright/test": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "dev": true, + "requires": { + "playwright": "1.44.1" + } + }, "@popperjs/core": { "version": "2.11.7", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", @@ -28144,6 +28213,31 @@ "pathe": "^1.1.0" } }, + "playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.44.1" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + } + } + }, + "playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "dev": true + }, "postcss": { "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", diff --git a/package.json b/package.json index 824afd980..0a941d92e 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@commitlint/cli": "18.6.1", "@commitlint/config-conventional": "18.6.2", "@faker-js/faker": "8.4.1", + "@playwright/test": "1.44.1", "@testing-library/react": "15.0.0", "@vitejs/plugin-react": "4.2.1", "autoprefixer": "10.4.17", @@ -90,11 +91,17 @@ "services:down": "npm run docker:compose -- down", "docker:compose": "docker compose --env-file .env -f infra/docker-compose.development.yml", "preconcurrently": "kill-port 3000 && npm run services:up", - "concurrently": "concurrently -s first -P -k -n next,vitest \"npm run next\" \"vitest {@}\"", + "concurrently": "concurrently -s first -P -k -n next,vitest_playright \"npm run next\" \"npm run test:integration && npm run test:e2e\"", "postconcurrently": "npm run services:stop", - "test": "npm run concurrently -- --hide next -- run", + "test": "npm run test:unit && npm run concurrently -- --hide next", + "test:unit": "vitest run unit", + "test:integration": "vitest run integration", "test:watch": "vitest watch", "test:watch:services": "npm run concurrently -- -- watch", + "test:e2e": "npm run test:e2e:dependencies && npx playwright test", + "test:e2e:dependencies": "npx playwright install --with-deps", + "test:e2e:ui": "npm run test:e2e:dependencies && npx playwright test --ui", + "test:e2e:headed": "npm run test:e2e:dependencies && npx playwright test --headed", "lint": "npm run lint:next && npm run lint:prettier", "lint:next": "next lint --max-warnings=0 --dir .", "lint:prettier": "prettier --check .", diff --git a/pages/interface/components/Header/index.js b/pages/interface/components/Header/index.js index bfd73b0f7..084ee40ad 100644 --- a/pages/interface/components/Header/index.js +++ b/pages/interface/components/Header/index.js @@ -66,6 +66,7 @@ export default function HeaderComponent() { Recentes @@ -89,10 +90,14 @@ export default function HeaderComponent() { {!isScreenSmall && ( <> - Login + + Login + - Cadastrar + + Cadastrar + )} @@ -110,7 +115,7 @@ export default function HeaderComponent() { {!isScreenSmall && ( - + diff --git a/pages/login/index.public.js b/pages/login/index.public.js index 8c8bcf045..8d877e8f0 100644 --- a/pages/login/index.public.js +++ b/pages/login/index.public.js @@ -88,7 +88,11 @@ function LoginForm() { <>
- {globalErrorMessage && {globalErrorMessage}} + {globalErrorMessage && ( + + {globalErrorMessage} + + )} Login diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..e002192bb --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,24 @@ +import { defineConfig, devices } from '@playwright/test'; +import { config, populate } from 'dotenv'; + +config({ path: './.env' }); +populate(process.env, { NODE_ENV: 'test' }); + +const PORT = process.env.PORT || 3000; +const baseURL = `http://localhost:${PORT}`; + +export default defineConfig({ + testDir: './tests/e2e/', + testMatch: '**/*.spec.js', + reporter: [['html', { open: 'never' }]], + workers: 1, + use: { + baseURL, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/tests/e2e/login.spec.js b/tests/e2e/login.spec.js new file mode 100644 index 000000000..18cb2a83d --- /dev/null +++ b/tests/e2e/login.spec.js @@ -0,0 +1,47 @@ +import { expect, test } from '@playwright/test'; + +import orchestrator from 'tests/orchestrator'; + +import { HomePage } from './page-object/home-page'; +import { LoginPage } from './page-object/login-page'; + +test.beforeAll('Running orchestrator', async () => { + await orchestrator.waitForAllServices(); + await orchestrator.dropAllTables(); + await orchestrator.runPendingMigrations(); +}); + +test.beforeEach('Navigating for login page', async ({ page }) => { + let loginPage = new LoginPage(page); + await loginPage.goToPage(); +}); + +test.describe('Login user', () => { + test('should not be able to login', async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.fill('email', 'email_not_exist@gmail.com'); + await loginPage.fill('password', 'password_invalid'); + await loginPage.clickLoginButton(false); + + let expectedMessage = await loginPage.getGlobalErrorMessage(); + expect(expectedMessage).toContain('Dados não conferem. Verifique se os dados enviados estão corretos.'); + }); + + test('should be able to login', async ({ page }) => { + const defaultUser = await orchestrator.createUser({ + username: 'defaultuser', + email: 'email_default_user@gmail.com', + password: 'password_default_user', + }); + await orchestrator.activateUser(defaultUser); + + const loginPage = new LoginPage(page); + await loginPage.fill('email', 'email_default_user@gmail.com'); + await loginPage.fill('password', 'password_default_user'); + await loginPage.clickLoginButton(); + + const homePage = new HomePage(page); + let username = await homePage.getUserLogged(); + expect(username).toBe('defaultuser'); + }); +}); diff --git a/tests/e2e/page-object/home-page.js b/tests/e2e/page-object/home-page.js new file mode 100644 index 000000000..9cde84916 --- /dev/null +++ b/tests/e2e/page-object/home-page.js @@ -0,0 +1,20 @@ +exports.HomePage = class HomePage { + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + this.page = page; + + this.buttonLogin = this.page.getByLabel('Login'); + this.buttonUserMenu = this.page.getByLabel('Abrir o menu'); + } + + async goLogin() { + await this.buttonLogin.click(); + } + + async getUserLogged() { + await this.buttonUserMenu.click(); + return await this.page.textContent("ul[role='menu'] li:first-child > a > div > span > div"); + } +}; diff --git a/tests/e2e/page-object/login-page.js b/tests/e2e/page-object/login-page.js new file mode 100644 index 000000000..88cea83cb --- /dev/null +++ b/tests/e2e/page-object/login-page.js @@ -0,0 +1,42 @@ +const { HomePage } = require('./home-page'); + +exports.LoginPage = class LoginPage { + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + this.page = page; + + this.buttonLogin = this.page.locator('button[type=submit]'); + } + + async login(email, password, detachedLoginButton = true) { + await this.fill('email', email); + await this.fill('password', password); + + await this.clickLoginButton(detachedLoginButton); + } + + async goToPage() { + await this.page.goto('/'); + + const homePage = new HomePage(this.page); + await homePage.goLogin(); + } + + async fill(id, value) { + await this.page.locator(`#${id}`).fill(value); + } + + async clickLoginButton(detached = true) { + await this.buttonLogin.click(); + + if (detached) { + await this.buttonLogin.waitFor({ state: 'detached' }); + } + } + + async getGlobalErrorMessage() { + return await this.page.getByLabel('Mensagem de erro').textContent(); + } +}; diff --git a/tests/orchestrator.js b/tests/orchestrator.js index 62c532873..65eef0803 100644 --- a/tests/orchestrator.js +++ b/tests/orchestrator.js @@ -130,6 +130,10 @@ async function createUser(userObject) { }); } +async function findUserByEmail(email) { + return await user.findOneByEmail(email); +} + async function addFeaturesToUser(userObject, features) { return await user.addFeatures(userObject.id, features); } @@ -382,6 +386,7 @@ const orchestrator = { deleteAllEmails, getLastEmail, createUser, + findUserByEmail, activateUser, createSession, findSessionByToken, diff --git a/vitest.config.mjs b/vitest.config.mjs index afa24c027..6eba94a80 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -1,13 +1,14 @@ import react from '@vitejs/plugin-react'; import { config } from 'dotenv'; import tsconfigPaths from 'vite-tsconfig-paths'; -import { defineConfig } from 'vitest/config'; +import { configDefaults, defineConfig } from 'vitest/config'; config(); export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { + exclude: [...configDefaults.exclude, '**/tests/e2e/**'], environmentMatchGlobs: [['**/interface/**/*', 'jsdom']], globals: true, fileParallelism: false,