diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..adfbac9a --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [main, master, add-first-e2e-test] + 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 96862086..a251f381 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.husky/pre-commit b/.husky/pre-commit index 0da96d6b..b5b24a69 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,2 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" npx pretty-quick --staged diff --git a/e2e/can-run-quiz.spec.ts b/e2e/can-run-quiz.spec.ts new file mode 100644 index 00000000..6db9f3e6 --- /dev/null +++ b/e2e/can-run-quiz.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "@playwright/test"; + +const CATEGORIES = [ + "HTML", + "CSS", + "JavaScript", + "Accessibility", + "General CS", + "IT", + "Linux", + "Python", + "SQL", + "Random" +] as const; + +test.beforeEach(async ({ page }) => { + await page.goto("http://localhost:3000/#/quizzes"); +}); + +test("has full list of categories", async ({ page }) => { + // Make sure the list has all the categores. + await expect(page.getByRole("button")).toHaveText(CATEGORIES); +}); + +test("clicking HTML category should lead to select-questions-number page ", async ({ + page +}) => { + page.getByRole("button", { name: "HTML" }).click(); + + expect(page.url()).toContain("HTML"); + + await expect( + page.getByRole("heading", { name: "Choose a length for the Quiz" }) + ).toBeVisible(); +}); diff --git a/e2e/sanity.spec.ts b/e2e/sanity.spec.ts new file mode 100644 index 00000000..2f99d617 --- /dev/null +++ b/e2e/sanity.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("http://localhost:3000/"); +}); + +test("has title", async ({ page }) => { + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Developer Quiz/); +}); + +test("pressing get started link should lead to select categories page", async ({ + page +}) => { + // Click the get started link. + await page.getByRole("link", { name: "Get started (it's free)" }).click(); + + expect(page.url()).toContain("quizzes"); + // Expects page to have a heading that says 'Choose a Category'. + await expect( + page.getByRole("heading", { name: "Choose a Category" }) + ).toBeVisible(); +}); diff --git a/package.json b/package.json index a34f7ee5..99b831ac 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "start:local-docker": "docker container run --rm -it -p 3000:3000 developer-quiz-site", "build": "vite build", "test": "vitest", + "e2e:ui": "playwright test --ui", + "e2e:ci": "playwright test", "lint": "eslint .", "pretty-quick": "pretty-quick", "fix-style": "pnpm run lint --fix", @@ -50,6 +52,7 @@ ] }, "devDependencies": { + "@playwright/test": "^1.45.3", "@types/node": "^20.0.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.0.4", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..6be4a8b7 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./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: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry" + }, + + /* 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 26ad326f..cd0e7543 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: specifier: ^4.0.0 version: 4.2.2 devDependencies: + '@playwright/test': + specifier: ^1.45.3 + version: 1.45.3 '@types/node': specifier: ^20.0.0 version: 20.14.12 @@ -417,6 +420,11 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@playwright/test@1.45.3': + resolution: {integrity: sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==} + engines: {node: '>=18'} + hasBin: true + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -1128,6 +1136,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1690,6 +1703,16 @@ packages: engines: {node: '>=0.10'} hasBin: true + playwright-core@1.45.3: + resolution: {integrity: sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.45.3: + resolution: {integrity: sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -2563,6 +2586,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@playwright/test@1.45.3': + dependencies: + playwright: 1.45.3 + '@popperjs/core@2.11.8': {} '@react-aria/ssr@3.9.2(react@18.3.1)': @@ -3465,6 +3492,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -4020,6 +4050,14 @@ snapshots: pidtree@0.6.0: {} + playwright-core@1.45.3: {} + + playwright@1.45.3: + dependencies: + playwright-core: 1.45.3 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.0.0: {} postcss@8.4.39: diff --git a/vite.config.ts b/vite.config.ts index 13d4c30c..5ee8fa8f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,8 +5,8 @@ export default defineConfig({ plugins: [react()], server: { watch: { - usePolling: true, // required for container hot reloading - }, + usePolling: true // required for container hot reloading + }, port: 3000, host: true, // fixes container xdg-open issues open: true @@ -18,6 +18,11 @@ export default defineConfig({ test: { globals: true, environment: "happy-dom", - setupFiles: "./testSetup.ts" + setupFiles: "./testSetup.ts", + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/e2e/*" /* do not include playwright files */ + ] } });