diff --git a/astro/.gitignore b/astro/.gitignore index 03f20c64..9f31a541 100644 --- a/astro/.gitignore +++ b/astro/.gitignore @@ -24,3 +24,8 @@ pnpm-debug.log* .vscode # Local Netlify folder .netlify + +# Playwright +test-results/ +playwright-report/ +playwright/.cache/ diff --git a/astro/package-lock.json b/astro/package-lock.json index 332781c6..0bdece84 100644 --- a/astro/package-lock.json +++ b/astro/package-lock.json @@ -29,6 +29,8 @@ "sass": "1.77.5" }, "devDependencies": { + "@axe-core/playwright": "^4.11.3", + "@playwright/test": "^1.59.1", "@types/lodash-es": "^4.17.12", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0" @@ -207,6 +209,19 @@ "integrity": "sha512-qZxHwVnmb5FXuvRsaIGaqWgnftjCuMY+GSbaVZdBmE4j8AfgPqKPxYp8SUERyJcjpKCEmO4wD6ybuGH8A2kVRQ==", "license": "MIT" }, + "node_modules/@axe-core/playwright": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz", + "integrity": "sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.4" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -3536,6 +3551,22 @@ "integrity": "sha512-fz0NOYUEYXtg1TBaPEEvtcBq3FfmLFuTe1VZw4M8sTWX129br5dguu3M15+plOQnc181ShYe67RfwhKgK89VnA==", "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -5417,6 +5448,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -11859,6 +11900,53 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/astro/package.json b/astro/package.json index 6641e804..b5b603c0 100644 --- a/astro/package.json +++ b/astro/package.json @@ -7,7 +7,9 @@ "start": "astro dev", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@astrojs/mdx": "4.3.13", @@ -31,6 +33,8 @@ "sass": "1.77.5" }, "devDependencies": { + "@axe-core/playwright": "^4.11.3", + "@playwright/test": "^1.59.1", "@types/lodash-es": "^4.17.12", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0" diff --git a/astro/playwright.config.ts b/astro/playwright.config.ts new file mode 100644 index 00000000..c534569d --- /dev/null +++ b/astro/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: "html", + use: { + baseURL: "http://localhost:4321", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: "npm run dev", + url: "http://localhost:4321", + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/astro/tests/e2e/blog-authors.spec.ts b/astro/tests/e2e/blog-authors.spec.ts new file mode 100644 index 00000000..cb92c7be --- /dev/null +++ b/astro/tests/e2e/blog-authors.spec.ts @@ -0,0 +1,51 @@ +import AxeBuilder from "@axe-core/playwright"; +import { expect, test } from "@playwright/test"; + +test.describe("Blog authors index (/blog/authors/)", () => { + test("renders author names as strings, not [object Object]", async ({ page }) => { + await page.goto("/blog/authors/"); + + await expect(page.locator("body")).not.toContainText("[object Object]"); + await expect( + page.getByRole("link", { name: /Rachael Bradley Montgomery/i }), + ).toBeVisible(); + }); + + test("axe accessibility scan (informational, not gating)", async ({ page }) => { + await page.goto("/blog/authors/"); + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations).toEqual([]); + }); +}); + +test.describe("Author detail page (/blog/authors/rachael-bradley-montgomery/)", () => { + const path = "/blog/authors/rachael-bradley-montgomery/"; + + test("header shows formatted author name, not [object Object]", async ({ page }) => { + await page.goto(path); + + await expect(page.locator("body")).not.toContainText("[object Object]"); + await expect(page.locator("body")).toContainText( + "Blogs by Rachael Bradley Montgomery", + ); + }); + + test("breadcrumbs show ancestors and formatted author name as current page", async ({ page }) => { + await page.goto(path); + + const nav = page.getByRole("navigation", { name: "Breadcrumb" }); + await expect(nav).toBeVisible(); + + await expect(nav.getByRole("link")).toHaveText(["Home", "Blog", "Authors"]); + + const current = nav.locator('[aria-current="page"]'); + await expect(current).toHaveText("Blogs by Rachael Bradley Montgomery"); + await expect(current).not.toContainText("[object Object]"); + }); + + test("axe accessibility scan (informational, not gating)", async ({ page }) => { + await page.goto(path); + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations).toEqual([]); + }); +});