// @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', 'Local AI Stack', 'Reverse Proxy Infrastructure', 'Dynamic DNS Service', '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: second card, Ollama replaced by vLLM, Gufo, llama.cpp const ai = cards.nth(1); 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(//); expect(html).toMatch(/