From 3898dc3c5e50624a8c023e1beb1c8b4553f22e25 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Fri, 18 Sep 2026 17:23:30 -0400 Subject: [PATCH] Add Playwright E2E suite against the built dist/ Pin @playwright/test 1.62.0 (matches the cached chromium revision) and serve dist/ via python3 http.server, so the deployed artifact is what gets tested. Covers page load, terminal commands, vim mode, achievements, and the nav menu. Ignore node_modules, test-results, playwright-report, and the phased-execution runtime scratch (.agents/phase-sessions, pipeline.log). --- .gitignore | 10 ++ package-lock.json | 78 ++++++++++ package.json | 14 ++ playwright.config.js | 28 ++++ tests/e2e/01-page.spec.js | 77 ++++++++++ tests/e2e/02-terminal.spec.js | 235 ++++++++++++++++++++++++++++++ tests/e2e/03-vim.spec.js | 77 ++++++++++ tests/e2e/04-achievements.spec.js | 91 ++++++++++++ tests/e2e/05-menu.spec.js | 42 ++++++ tests/e2e/helpers.js | 30 ++++ 10 files changed, 682 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.js create mode 100644 tests/e2e/01-page.spec.js create mode 100644 tests/e2e/02-terminal.spec.js create mode 100644 tests/e2e/03-vim.spec.js create mode 100644 tests/e2e/04-achievements.spec.js create mode 100644 tests/e2e/05-menu.spec.js create mode 100644 tests/e2e/helpers.js diff --git a/.gitignore b/.gitignore index 19d53b5..234ab4f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,16 @@ dist/ # Local Docker build cache .docker/ +# JS / test tooling +node_modules/ +test-results/ +playwright-report/ + +# Phased-execution runtime artifacts (the .agents/ planning tree itself is +# tracked and committed — only its per-run scratch is ignored) +.agents/phase-sessions/ +.agents/pipeline.log + # OS files .DS_Store Thumbs.db diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1201d61 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,78 @@ +{ + "name": "homepage", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "homepage", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "1.62.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", + "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "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/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..850b3e4 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "homepage", + "version": "1.0.0", + "private": true, + "description": "Static portfolio site — build (build.sh) + E2E tests (Playwright)", + "scripts": { + "build": "./build.sh", + "test": "playwright test", + "test:headed": "playwright test --headed" + }, + "devDependencies": { + "@playwright/test": "1.62.0" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..a633d39 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,28 @@ +// @ts-check +const { defineConfig } = require('@playwright/test'); + +/** + * E2E config: serves the production build (dist/) on a local static server. + * Always run `./build.sh` (npm run build) before `npm test` so the tests + * exercise the exact artifact that gets deployed. + */ +module.exports = defineConfig({ + testDir: './tests/e2e', + timeout: 20000, + retries: 0, + workers: 1, + reporter: [['list'], ['html', { open: 'never' }]], + use: { + baseURL: 'http://127.0.0.1:8123', + viewport: { width: 1440, height: 900 }, + }, + webServer: { + command: 'python3 -m http.server 8123 --bind 127.0.0.1 --directory dist', + url: 'http://127.0.0.1:8123/index.html', + reuseExistingServer: !process.env.CI, + timeout: 15000, + }, + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + ], +}); diff --git a/tests/e2e/01-page.spec.js b/tests/e2e/01-page.spec.js new file mode 100644 index 0000000..d4e3b90 --- /dev/null +++ b/tests/e2e/01-page.spec.js @@ -0,0 +1,77 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +test.describe('Page structure', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('loads with correct title and lang', async ({ page }) => { + await expect(page).toHaveTitle(/Reese Wells/); + await expect(page.locator('html')).toHaveAttribute('lang', 'en'); + }); + + test('nav has all seven section links pointing at existing targets', async ({ page }) => { + const links = page.locator('#navLinks a'); + await expect(links).toHaveCount(7); + const hrefs = await links.evaluateAll(as => as.map(a => a.getAttribute('href'))); + expect(hrefs).toEqual(['#hero', '#about', '#experience', '#skills', '#projects', '#contact', '#gpg']); + for (const href of hrefs) { + await expect(page.locator(href)).toBeAttached(); + } + }); + + test('all major sections exist and are in the DOM', async ({ page }) => { + for (const id of ['hero', 'about', 'experience', 'skills', 'projects', 'contact', 'gpg']) { + await expect(page.locator(`#${id}`)).toBeAttached(); + } + }); + + test('profile image loads', async ({ page }) => { + const img = page.locator('#about img'); + await expect(img).toBeVisible(); + await expect.poll(async () => + page.evaluate(() => document.querySelector('#about img').naturalWidth) + ).toBeGreaterThan(0); + }); + + test('server rack background is generated with a terminal unit', async ({ page }) => { + const rack = page.locator('.server-rack-bg .rack-container'); + await expect(rack).toBeAttached(); + await expect(rack.locator('.rack-unit')).not.toHaveCount(0); + await expect(page.locator('.terminal-display')).toBeAttached(); + }); + + test('GPG section shows both fingerprints and key blocks', async ({ page }) => { + await expect(page.locator('#gpg .gpg-key-card')).toHaveCount(2); + await expect(page.locator('#gpg .gpg-key-fingerprint code').first()).toHaveText( + '7FC1 B297 0011 4F4F C589 E706 5FDD CFA5 44D7 7B8C'); + await expect(page.locator('#gpg .gpg-key-fingerprint code').last()).toHaveText( + '2FF3 619F A6CA 2A4C FA2D 3532 816E 5FE7 8271 602B'); + const blocks = page.locator('#gpg .gpg-key-block'); + await expect(blocks).toHaveCount(2); + for (const block of [blocks.first(), blocks.last()]) { + await expect(block).toContainText('BEGIN PGP PUBLIC KEY BLOCK'); + await expect(block).toContainText('END PGP PUBLIC KEY BLOCK'); + } + }); + + test('external links all carry rel="noopener"', async ({ page }) => { + const links = page.locator('a[target="_blank"]'); + const count = await links.count(); + expect(count).toBeGreaterThan(0); + for (let i = 0; i < count; i++) { + await expect(links.nth(i)).toHaveAttribute('rel', /noopener/); + } + }); + + test('hashed assets are referenced and served', async ({ page }) => { + const html = await page.content(); + expect(html).toMatch(//); + expect(html).toMatch(/