Build and Push Container / build-and-push (push) Successful in 16s
**Phase 05 final pass — all 4 tasks already complete; verified phase-level criteria + docs. Repo 100% green.** - Verified: 6 `project-card fade-in` cards; order Brain of Reese → Reverse Proxy → Dynamic DNS → Local AI Stack → Deployment Automation → ServWaf; `src/index.html` diff hunks (lines 293–538) all inside `#projects` (286–542). - `./build.sh` → `Build complete: dist/` (exit 0); `npm test` → **48 passed, 0 failed**; `bash .agents/validate.sh` → **exit 0** (48 E2E + `HEADER CHECK PASSED` on `/`, `/index.html`, hashed CSS via podman + repo-hygiene gates). No linter exists in this repo (no `lint` script/eslint config). Coverage: no JS/CSS touched — vacuous per phase spec; E2E is the binding gate. - Completion criteria: card count 6 in `src/` **and** `dist/` ✅ · first card "Brain of Reese" with `…/Experimental/brain-of-reese` href in `dist/index.html` ✅ · `grep -c "Self-Hosted Services\|Kubernetes Cluster" src/index.html` = **0** ✅ · AI card has vLLM/Gufo/llama.cpp + LocalAI/LiteLLM/Langfuse, no "Ollama" ✅ · regression test present in `tests/e2e/01-page.spec.js` and passing ✅ · `./build.sh && npm test` green (48 ≥ 45) ✅ · no edits outside `#projects` ✅ · README/AGENTS consistent ✅. - Docs: README **updated** (new paragraph stating what the E2E suite covers, incl. the six featured cards, so future content edits update the matching test). AGENTS.md **updated** (convention: the `#projects` grid is test-locked by `01-page.spec.js`). All other README claims re-checked against reality (Dockerfile builds `dist/` internally, `listen 8080`, snippet `include`s, `check-headers.sh` port/paths/output, `--bind 0.0.0.0` config, Node ≥20 engine floor, CI `main`+tag publishing) — consistent. - Notable: `.agents/validate.sh` is still **untracked** though AGENTS.md/README reference it — harness should `git add` it (I ran no git index commands). `.agents/PLAN.md` §4/§5 still say "44 tests" (suite is 48 since Phases 04/05); PLAN.md is off-limits, so this drift needs an explicit user-approved edit. No live-infrastructure touched. - Next pending phase: none in `.agents/phases/todo/` (05 was last). Remaining unphased audit items (L-3 CSP `unsafe-inline`, L-5 SSH-key labelling, L-7 CI pinning, L-8 build.sh CI check) would need new phases via `phase-authoring`.
126 lines
6.2 KiB
JavaScript
126 lines
6.2 KiB
JavaScript
// @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('experience timeline leads with Huntington National Bank role', async ({ page }) => {
|
||
const items = page.locator('#experience .timeline-item');
|
||
await expect(items).toHaveCount(5);
|
||
const first = items.first().locator('.timeline-content');
|
||
await expect(first.locator('h3')).toHaveText('Lead MLOps Engineer');
|
||
await expect(first.locator('.timeline-company')).toHaveText('Huntington National Bank');
|
||
await expect(first.locator('.timeline-date')).toHaveText('Jun 2026 – Present');
|
||
const bullets = first.locator('.timeline-list li');
|
||
await expect(bullets).toHaveCount(3);
|
||
await expect(bullets.nth(0)).toContainText('agentic vulnerability remediation');
|
||
await expect(bullets.nth(1)).toContainText('LLM harness skills');
|
||
await expect(bullets.nth(1)).toContainText('ServiceNow');
|
||
await expect(bullets.nth(2)).toContainText('developer landing zone');
|
||
// previous role's end date was closed out
|
||
const nimbisManager = items.nth(1).locator('.timeline-content');
|
||
await expect(nimbisManager.locator('h3')).toHaveText('DevOps Manager');
|
||
await expect(nimbisManager.locator('.timeline-date')).toHaveText('Jan 2022 – Apr 2026');
|
||
});
|
||
|
||
test('featured projects show the six updated cards in order', async ({ page }) => {
|
||
const cards = page.locator('#projects .project-card');
|
||
await expect(cards).toHaveCount(6);
|
||
const titles = await cards.locator('.project-info h3').allTextContents();
|
||
expect(titles).toEqual([
|
||
'Brain of Reese',
|
||
'Reverse Proxy Infrastructure',
|
||
'Dynamic DNS Service',
|
||
'Local AI Stack',
|
||
'Deployment Automation',
|
||
'ServWaf',
|
||
]);
|
||
// Brain of Reese leads, with its source link
|
||
const brain = cards.first();
|
||
await expect(brain.locator('.project-tech span')).toHaveCount(4);
|
||
await expect(brain).toHaveText(/RAG chatbot/i);
|
||
await expect(brain.locator('a[href="https://gitea.reeseapps.com/Experimental/brain-of-reese"]')).toBeAttached();
|
||
// Local AI Stack: Ollama replaced by vLLM, Gufo, llama.cpp
|
||
const ai = cards.nth(3);
|
||
await expect(ai.locator('.project-tech span')).toHaveText(['vLLM', 'Gufo', 'llama.cpp', 'LocalAI', 'LiteLLM', 'Langfuse']);
|
||
await expect(ai).not.toContainText('Ollama');
|
||
// ServWaf is closed source — description only, no source link
|
||
await expect(cards.last()).toHaveText(/Web Application Firewall/i);
|
||
await expect(cards.last().locator('.project-links')).not.toBeAttached();
|
||
// removed cards are gone from the grid
|
||
expect(titles).not.toContain('Self-Hosted Services');
|
||
expect(titles).not.toContain('Kubernetes Cluster');
|
||
});
|
||
|
||
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(/<link rel="stylesheet" href="style\.[0-9a-f]{32}\.css"/);
|
||
expect(html).toMatch(/<script src="terminal\.[0-9a-f]{32}\.js" defer="?">/);
|
||
expect(html).toMatch(/<script src="script\.[0-9a-f]{32}\.js" defer="?">/);
|
||
const resp = await page.request.get(page.url().replace(/\/$/, '') + '/' +
|
||
(await page.locator('link[rel="stylesheet"]').getAttribute('href')));
|
||
expect(resp.ok()).toBeTruthy();
|
||
});
|
||
});
|