From 39d9f694793f33e7850dbe30bbe0aaa557053068 Mon Sep 17 00:00:00 2001 From: Raghad Dahi Date: Mon, 17 Aug 2026 16:02:23 +0300 Subject: [PATCH 1/2] Add Playwright end-to-end testing --- .github/workflows/ci.yml | 25 ++++++++++++++++++++ .gitignore | 8 +++++++ AGENTS.md | 1 + README.md | 1 + apps/guide/settings/e2e.py | 14 ++++++++++++ justfile | 18 +++++++++++++++ package-lock.json | 47 ++++++++++++++++++++++++++++++++++++++ package.json | 8 ++++++- playwright.config.js | 41 +++++++++++++++++++++++++++++++++ tests/e2e/smoke.spec.js | 40 ++++++++++++++++++++++++++++++++ 10 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 apps/guide/settings/e2e.py create mode 100644 playwright.config.js create mode 100644 tests/e2e/smoke.spec.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f97c702..c456dec0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,3 +53,28 @@ jobs: - run: uv run python manage.py createcachetable - run: uv run python manage.py makemigrations --check - run: just test-coverage + + playwright: + runs-on: ubuntu-latest + timeout-minutes: 60 + + env: + DJANGO_SETTINGS_MODULE: apps.guide.settings.e2e + + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + - run: npm ci + - run: npm run build + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + - run: uv sync --frozen + - run: npm run e2e:install + - run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 0f9ad9a3..acb79272 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,11 @@ apps/guide/settings/local.py content/evals/translations-inspect_ai/logs/ # Generated glossaries (regenerate with `just eval-glossary `) content/evals/translations-inspect_ai/glossary/ + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ +/db_e2e.sqlite3 diff --git a/AGENTS.md b/AGENTS.md index c8484919..30825c29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ Important context for AI coding agents working on this project. - `just lint`: Lint the project. - `just format`: Format project files. - `just test`: Run backend (Django) and frontend (Vitest) tests. +- `just e2e`: Run end-to-end tests with Playwright (requires `just e2e-install` first; stop any dev server on :8000 before running). - `just translations`: Generate and compile translation strings. ## Setup & run commands diff --git a/README.md b/README.md index bda42d12..0db6b959 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ If you're a Python or Django developer, fork the repo and join us. You'll find a - Run formatting (Ruff & Oxfmt) `just format` - Run linting (Ruff, Oxlint, Oxfmt, Stylelint) `just lint` - Run tests `just test` +- Run end-to-end tests `just e2e` (first-time setup: `just e2e-install`) # Other Notes diff --git a/apps/guide/settings/e2e.py b/apps/guide/settings/e2e.py new file mode 100644 index 00000000..7baff5ba --- /dev/null +++ b/apps/guide/settings/e2e.py @@ -0,0 +1,14 @@ +from .test import * # noqa: F403 + +DEBUG = True +ALLOWED_HOSTS = ["127.0.0.1", "localhost"] + +CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} + +DATABASES = { + "default": dj_database_url.parse( # noqa: F405 + f"sqlite:///{BASE_DIR / 'db_e2e.sqlite3'}" # noqa: F405 + ) +} + +WAGTAILADMIN_BASE_URL = "http://127.0.0.1:8000" diff --git a/justfile b/justfile index c158b8c4..5d3b20bb 100644 --- a/justfile +++ b/justfile @@ -30,6 +30,24 @@ test-coverage: DJANGO_SETTINGS_MODULE=apps.guide.settings.test uv run coverage run manage.py test uv run coverage report +# Install Playwright browsers (Chromium, Firefox, Edge). +e2e-install: + npm run e2e:install + +# Run end-to-end tests with Playwright. Builds frontend assets first. +e2e: + npm run build + npm run e2e + +# Run end-to-end tests interactively with the Playwright UI. +e2e-ui: + npm run build + npm run e2e:ui + +# Open the last Playwright HTML report. +e2e-report: + npm run e2e:report + # Format the backend code with Ruff. format-backend: uv run ruff check . --fix diff --git a/package-lock.json b/package-lock.json index a84eba56..ba90b958 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "bootstrap": "^5.3.8" }, "devDependencies": { + "@playwright/test": "^1.62.1", + "@types/node": "^26.2.0", "@wagtail/stylelint-config-wagtail": "^2.0.1", "browserslist": "^4.29.3", "happy-dom": "^20.14.5", @@ -1453,6 +1455,22 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -4959,6 +4977,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/pngjs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", diff --git a/package.json b/package.json index 0fc41899..c291bbce 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,19 @@ "lint:js": "vp lint --report-unused-disable-directives", "lint:format": "vp fmt --check", "lint": "npm run lint:js && npm run lint:css && npm run lint:format", - "test": "vp test run" + "test": "vp test run", + "e2e": "playwright test", + "e2e:ui": "playwright test --ui", + "e2e:report": "playwright show-report", + "e2e:install": "playwright install --with-deps chromium firefox msedge" }, "dependencies": { "@popperjs/core": "^2.11.5", "bootstrap": "^5.3.8" }, "devDependencies": { + "@playwright/test": "^1.62.1", + "@types/node": "^26.2.0", "@wagtail/stylelint-config-wagtail": "^2.0.1", "browserslist": "^4.29.3", "happy-dom": "^20.14.5", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 00000000..55fdb4fc --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,41 @@ +// @ts-check +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [['list'], ['html', { open: 'never' }]], + use: { + baseURL: 'http://127.0.0.1:8000', + locale: 'en-US', + timezoneId: 'UTC', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'msedge', + use: { ...devices['Desktop Edge'], channel: 'msedge' }, + }, + ], + webServer: { + command: + 'rm -f db_e2e.sqlite3 && uv run python manage.py migrate && uv run python manage.py buildfixtures && uv run python manage.py runserver --noreload 127.0.0.1:8000', + url: 'http://127.0.0.1:8000/en/', + reuseExistingServer: false, + timeout: 120_000, + env: { + DJANGO_SETTINGS_MODULE: 'apps.guide.settings.e2e', + }, + }, +}); diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js new file mode 100644 index 00000000..f3a39b6c --- /dev/null +++ b/tests/e2e/smoke.spec.js @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test'; + +test('homepage loads with title and sections', async ({ page }) => { + await page.goto('/en/'); + await expect(page).toHaveURL(/\/en\/$/); + await expect( + page.getByRole('heading', { name: /Wagtail User Guide/i }), + ).toBeVisible(); + await expect( + page.getByRole('link', { name: /Tutorial/i }).first(), + ).toBeVisible(); +}); + +test('tutorial section page loads', async ({ page }) => { + const response = await page.goto('/en/tutorial/'); + expect(response?.status()).toBe(200); + await expect( + page.getByRole('heading', { name: /Tutorial/i }).first(), + ).toBeVisible(); +}); + +test('tutorial subpage loads', async ({ page }) => { + const response = await page.goto('/en/tutorial/getting-started/'); + expect(response?.status()).toBe(200); +}); + +test('how-to subpage loads', async ({ page }) => { + const response = await page.goto('/en/how-to/add-a-user/'); + expect(response?.status()).toBe(200); +}); + +test('unknown page returns 404', async ({ request }) => { + const response = await request.get('/en/does-not-exist/'); + expect(response.status()).toBe(404); +}); + +test('dutch locale loads', async ({ page }) => { + const response = await page.goto('/nl/'); + expect(response?.status()).toBe(200); +}); From fa70271f48695d31f739413eed662604bfddcafa Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Tue, 6 Oct 2026 17:41:24 +0100 Subject: [PATCH 2/2] Format Playwright config and e2e tests with Oxfmt Assisted-by: Cursor --- playwright.config.js | 68 ++++++++++++++++++++--------------------- tests/e2e/smoke.spec.js | 42 ++++++++++++------------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 55fdb4fc..a077042b 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -2,40 +2,40 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - testDir: './tests/e2e', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: [['list'], ['html', { open: 'never' }]], - use: { - baseURL: 'http://127.0.0.1:8000', - locale: 'en-US', - timezoneId: 'UTC', - trace: 'on-first-retry', + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [['list'], ['html', { open: 'never' }]], + use: { + baseURL: 'http://127.0.0.1:8000', + locale: 'en-US', + timezoneId: 'UTC', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - { - name: 'msedge', - use: { ...devices['Desktop Edge'], channel: 'msedge' }, - }, - ], - webServer: { - command: - 'rm -f db_e2e.sqlite3 && uv run python manage.py migrate && uv run python manage.py buildfixtures && uv run python manage.py runserver --noreload 127.0.0.1:8000', - url: 'http://127.0.0.1:8000/en/', - reuseExistingServer: false, - timeout: 120_000, - env: { - DJANGO_SETTINGS_MODULE: 'apps.guide.settings.e2e', - }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, }, + { + name: 'msedge', + use: { ...devices['Desktop Edge'], channel: 'msedge' }, + }, + ], + webServer: { + command: + 'rm -f db_e2e.sqlite3 && uv run python manage.py migrate && uv run python manage.py buildfixtures && uv run python manage.py runserver --noreload 127.0.0.1:8000', + url: 'http://127.0.0.1:8000/en/', + reuseExistingServer: false, + timeout: 120_000, + env: { + DJANGO_SETTINGS_MODULE: 'apps.guide.settings.e2e', + }, + }, }); diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js index f3a39b6c..d90b2b66 100644 --- a/tests/e2e/smoke.spec.js +++ b/tests/e2e/smoke.spec.js @@ -1,40 +1,40 @@ import { expect, test } from '@playwright/test'; test('homepage loads with title and sections', async ({ page }) => { - await page.goto('/en/'); - await expect(page).toHaveURL(/\/en\/$/); - await expect( - page.getByRole('heading', { name: /Wagtail User Guide/i }), - ).toBeVisible(); - await expect( - page.getByRole('link', { name: /Tutorial/i }).first(), - ).toBeVisible(); + await page.goto('/en/'); + await expect(page).toHaveURL(/\/en\/$/); + await expect( + page.getByRole('heading', { name: /Wagtail User Guide/i }), + ).toBeVisible(); + await expect( + page.getByRole('link', { name: /Tutorial/i }).first(), + ).toBeVisible(); }); test('tutorial section page loads', async ({ page }) => { - const response = await page.goto('/en/tutorial/'); - expect(response?.status()).toBe(200); - await expect( - page.getByRole('heading', { name: /Tutorial/i }).first(), - ).toBeVisible(); + const response = await page.goto('/en/tutorial/'); + expect(response?.status()).toBe(200); + await expect( + page.getByRole('heading', { name: /Tutorial/i }).first(), + ).toBeVisible(); }); test('tutorial subpage loads', async ({ page }) => { - const response = await page.goto('/en/tutorial/getting-started/'); - expect(response?.status()).toBe(200); + const response = await page.goto('/en/tutorial/getting-started/'); + expect(response?.status()).toBe(200); }); test('how-to subpage loads', async ({ page }) => { - const response = await page.goto('/en/how-to/add-a-user/'); - expect(response?.status()).toBe(200); + const response = await page.goto('/en/how-to/add-a-user/'); + expect(response?.status()).toBe(200); }); test('unknown page returns 404', async ({ request }) => { - const response = await request.get('/en/does-not-exist/'); - expect(response.status()).toBe(404); + const response = await request.get('/en/does-not-exist/'); + expect(response.status()).toBe(404); }); test('dutch locale loads', async ({ page }) => { - const response = await page.goto('/nl/'); - expect(response?.status()).toBe(200); + const response = await page.goto('/nl/'); + expect(response?.status()).toBe(200); });