diff --git a/.eslintrc.js b/.eslintrc.js index a2bb208..1c12823 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,6 +23,10 @@ module.exports = { files: ['**/*.test.ts', '**/*.test.tsx'], extends: ['plugin:vitest/recommended', 'plugin:testing-library/react'], }, + { + files: ['e2e/**/*.spec.ts'], + extends: ['plugin:playwright/recommended'], + }, ], ignorePatterns: ['node_modules/', '.next/', 'public/', 'components/ui'], } diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..9662b54 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 9e8616c..c48b157 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ next-env.d.ts # IDE .idea + +/e2e-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/README.md b/README.md index 451c754..527f757 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ This is a [Next.js](https://nextjs.org/) 14 Boilerplate project base on [`create - [next-international](https://github.com/QuiiBz/next-international) seems better, but not compatible with Not found page - Maybe the best choice is official Example: [app-dir-i18n-routing](https://github.com/vercel/next.js/tree/canary/examples/app-dir-i18n-routing) - Docker -- Playwright: Write end-to-end tests like a pro or cypress -TBD +- Playwright: Write end-to-end tests like a pro or cypress +- Github actions/CI ## TODO diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts new file mode 100644 index 0000000..9f01879 --- /dev/null +++ b/e2e/app.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test' + +test('should navigate to the dashboard page', async ({ page }) => { + // Start from the index page (the baseURL is set via the webServer in the playwright.config.ts) + await page.goto('/') + // Find an element with the text 'About' and click on it + await page.click('text=dashboard') + // The new URL should be "/about" (baseURL is used there) + await expect(page).toHaveURL('/dashboard') + // The new page should contain an h1 with "About" + await expect(page.locator('h2')).toContainText('Dashboard') +}) + +test('app journey', async ({ page }) => { + await page.goto('/') + const footerText = page.locator('footer p') + await expect(footerText).toBeVisible() + + const navLinks = page.locator('nav a') + const expectedLinksText = ['Loading', 'dashboard', 'todo demos', 'GitHub'] + + for (let i = 0; i < expectedLinksText.length; i++) { + const linkText = await navLinks.nth(i).textContent() + expect(linkText).toContain(expectedLinksText[i]) + } + + await navLinks.nth(0).click() + await expect(page).toHaveURL('/loading-and-streaming') + + await expect( + page.getByRole('heading', { name: 'Show loading UI and streaming' }), + ).toBeVisible() + + await navLinks.nth(2).click() + await expect(page).toHaveURL('/todo') + await expect( + page.getByRole('heading', { name: 'Todo demo with RCC' }), + ).toBeVisible() + // todo test submit +}) diff --git a/package.json b/package.json index 045640e..5a1c956 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "build": "next build", "build-analyze": "cross-env ANALYZE=true pnpm run build", "check-types": "tsc --noEmit --pretty", - "coverage": "vitest run --coverage", "dev": "next dev | pino-pretty", "dev:turbo": "next dev --turbo | pino-pretty", "preinstall": "npx only-allow pnpm", @@ -14,9 +13,9 @@ "lint-fix": "next lint --fix && pnpm run prettier:fix", "prepare": "husky", "prettier:fix": "prettier '**/*.{js,jsx,ts,tsx,json,md}' --write", - "preview": "next build && cp -r dist/static dist/standalone/dist/static && cp -r public dist/standalone/public && node dist/standalone/server.js", - "start": "next start", + "start": "next build && cp -r dist/static dist/standalone/dist/static && cp -r public dist/standalone/public && node dist/standalone/server.js", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:watch": "vitest" }, @@ -56,6 +55,7 @@ "@commitlint/config-conventional": "^19.1.0", "@commitlint/types": "^19.0.3", "@next/bundle-analyzer": "^14.2.2", + "@playwright/test": "^1.43.1", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^15.0.2", "@testing-library/user-event": "^14.5.2", @@ -69,6 +69,7 @@ "eslint": "^8", "eslint-config-next": "^14.2.2", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-playwright": "^1.6.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-tailwindcss": "^3.15.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..e6f7e91 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,105 @@ +import { defineConfig, devices } from '@playwright/test' +import path from 'path' + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +// Use process.env.PORT by default and fallback to port 3000 +const PORT = process.env.PORT || 3000 + +// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port +const baseURL = `http://localhost:${PORT}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + // Timeout per test + timeout: 30 * 1000, + // Test directory + testDir: path.join(__dirname, 'e2e'), + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? 'github' : 'html', + // Artifacts folder where screenshots, videos, and traces are stored. + outputDir: 'e2e-results/', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + + // Run your local dev server before starting the tests: + // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests + webServer: { + command: process.env.CI ? 'pnpm run start' : 'pnpm run dev:turbo', + url: baseURL, + timeout: 2 * 60 * 1000, + reuseExistingServer: !process.env.CI, + }, + use: { + // Use baseURL so to make navigations relative. + // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url + baseURL, + + // Retry a test if its failing with enabled tracing. This allows you to analyze the DOM, console logs, network traffic etc. + // More information: https://playwright.dev/docs/trace-viewer + trace: 'retry-with-trace', + + // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context + // contextOptions: { + // ignoreHTTPSErrors: true, + // }, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 689f7c8..d56d6fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ dependencies: version: 0.370.0(react@18.2.0) next: specifier: ^14.2.2 - version: 14.2.2(@babel/core@7.24.3)(react-dom@18.2.0)(react@18.2.0) + version: 14.2.2(@babel/core@7.24.3)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.2.0)(react@18.2.0) @@ -106,6 +106,9 @@ devDependencies: '@next/bundle-analyzer': specifier: ^14.2.2 version: 14.2.2 + '@playwright/test': + specifier: ^1.43.1 + version: 1.43.1 '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.4.2(vitest@1.5.0) @@ -145,6 +148,9 @@ devDependencies: eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) + eslint-plugin-playwright: + specifier: ^1.6.0 + version: 1.6.0(eslint@8.57.0) eslint-plugin-prettier: specifier: ^5.1.3 version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) @@ -178,6 +184,9 @@ devDependencies: prettier: specifier: ^3.2.5 version: 3.2.5 + prettier-plugin-packagejson: + specifier: ^2.5.0 + version: 2.5.0(prettier@3.2.5) prettier-plugin-tailwindcss: specifier: ^0.5.14 version: 0.5.14(prettier@3.2.5) @@ -1056,6 +1065,13 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: true + /@playwright/test@1.43.1: + resolution: {integrity: sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.43.1 + /@polka/url@1.0.0-next.25: resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} dev: true @@ -3415,6 +3431,16 @@ packages: engines: {node: '>=6'} dev: true + /detect-indent@7.0.1: + resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} + engines: {node: '>=12.20'} + dev: true + + /detect-newline@4.0.1: + resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false @@ -3839,6 +3865,20 @@ packages: object.fromentries: 2.0.8 dev: true + /eslint-plugin-playwright@1.6.0(eslint@8.57.0): + resolution: {integrity: sha512-tI1E/EDbHT4Fx5KvukUG3RTIT0gk44gvTP8bNwxLCFsUXVM98ZJG5zWU6Om5JOzH9FrmN4AhMu/UKyEsu0ZoDA==} + engines: {node: '>=16.6.0'} + peerDependencies: + eslint: '>=8.40.0' + eslint-plugin-jest: '>=25' + peerDependenciesMeta: + eslint-plugin-jest: + optional: true + dependencies: + eslint: 8.57.0 + globals: 13.24.0 + dev: true + /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5): resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4214,6 +4254,13 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4272,6 +4319,11 @@ packages: engines: {node: '>=6'} dev: false + /get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + dev: true + /get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -4292,6 +4344,10 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /git-hooks-list@3.1.0: + resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} + dev: true + /git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} engines: {node: '>=16'} @@ -4373,6 +4429,17 @@ packages: slash: 3.0.0 dev: true + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -4687,6 +4754,11 @@ packages: engines: {node: '>=8'} dev: true + /is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + dev: true + /is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -5269,7 +5341,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /next@14.2.2(@babel/core@7.24.3)(react-dom@18.2.0)(react@18.2.0): + /next@14.2.2(@babel/core@7.24.3)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-oGwUaa2bCs47FbuxWMpOoXtBMPYpvTPgdZr3UAo+pu7Ns00z9otmYpoeV1HEiYL06AlRQQIA/ypK526KjJfaxg==} engines: {node: '>=18.17.0'} hasBin: true @@ -5288,6 +5360,7 @@ packages: optional: true dependencies: '@next/env': 14.2.2 + '@playwright/test': 1.43.1 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001605 @@ -5629,6 +5702,20 @@ packages: pathe: 1.1.2 dev: true + /playwright-core@1.43.1: + resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==} + engines: {node: '>=16'} + hasBin: true + + /playwright@1.43.1: + resolution: {integrity: sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.43.1 + optionalDependencies: + fsevents: 2.3.2 + /possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -5718,6 +5805,19 @@ packages: fast-diff: 1.3.0 dev: true + /prettier-plugin-packagejson@2.5.0(prettier@3.2.5): + resolution: {integrity: sha512-6XkH3rpin5QEQodBSVNg+rBo4r91g/1mCaRwS1YGdQJZ6jwqrg2UchBsIG9tpS1yK1kNBvOt84OILsX8uHzBGg==} + peerDependencies: + prettier: '>= 1.16.0' + peerDependenciesMeta: + prettier: + optional: true + dependencies: + prettier: 3.2.5 + sort-package-json: 2.10.0 + synckit: 0.9.0 + dev: true + /prettier-plugin-tailwindcss@0.5.14(prettier@3.2.5): resolution: {integrity: sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q==} engines: {node: '>=14.21.3'} @@ -6267,6 +6367,11 @@ packages: engines: {node: '>=8'} dev: true + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: true + /slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -6288,6 +6393,24 @@ packages: dependencies: atomic-sleep: 1.0.0 + /sort-object-keys@1.1.3: + resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} + dev: true + + /sort-package-json@2.10.0: + resolution: {integrity: sha512-MYecfvObMwJjjJskhxYfuOADkXp1ZMMnCFC8yhp+9HDsk7HhR336hd7eiBs96lTXfiqmUNI+WQCeCMRBhl251g==} + hasBin: true + dependencies: + detect-indent: 7.0.1 + detect-newline: 4.0.1 + get-stdin: 9.0.0 + git-hooks-list: 3.1.0 + globby: 13.2.2 + is-plain-obj: 4.1.0 + semver: 7.6.0 + sort-object-keys: 1.1.3 + dev: true + /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -6489,6 +6612,14 @@ packages: tslib: 2.6.2 dev: true + /synckit@0.9.0: + resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.6.2 + dev: true + /tailwind-merge@2.2.2: resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==} dependencies: diff --git a/vitest.config.ts b/vitest.config.ts index da74246..360c2a2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,6 @@ import react from '@vitejs/plugin-react' import * as path from 'path' -import { defineConfig } from 'vitest/config' +import { defaultExclude, defineConfig } from 'vitest/config' // todo msw: https://github.com/vitest-dev/vitest/blob/main/examples/react-testing-lib-msw/src/mocks/server.ts export default defineConfig({ @@ -20,6 +20,8 @@ export default defineConfig({ coverage: { // todo check coverage include: ['**/*.test.ts'], + exclude: ['e2e'], }, + exclude: [...defaultExclude, 'e2e'], }, })