import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { expect, test, type Page } from '@playwright/test'; import { createServer, type ViteDevServer } from 'vite'; const stylesheetFiles = [ 'src/styles/tokens.css', 'src/styles/base.css', 'src/styles/layout.css', 'src/styles/components.css', 'src/styles/world.css', 'src/styles/print.css', ]; function informationLayerCss(): string { return stylesheetFiles .map((file) => { const path = resolve(process.cwd(), file); return existsSync(path) ? readFileSync(path, 'utf8') : ''; }) .join('\n'); } type ServerRenderer = { render: (url: string, inputs: unknown) => Promise }; type InputValidator = { validateProductionInputs: (content: unknown, manifest: unknown, site: unknown, preparedAssetRoot: URL) => unknown }; let vite: ViteDevServer; let renderedPortfolio = ''; const syntheticProjectMedia = [ { kind: 'image', assetId: 'synthetic-portrait-one', alt: 'Synthetic profile evidence.', caption: 'Fictional demo profile.' }, { kind: 'image', assetId: 'synthetic-portrait-two', alt: 'Synthetic settings evidence.', caption: 'Fictional demo settings.' }, { kind: 'image', assetId: 'synthetic-portrait-three', alt: 'Synthetic schedule evidence.', caption: 'Fictional demo schedule.' }, ] as const; const syntheticChartPair = [ { kind: 'image', assetId: 'synthetic-chart-wide', alt: 'Synthetic wide chart evidence.', caption: 'Fictional wide chart.' }, { kind: 'image', assetId: 'synthetic-chart-tall', alt: 'Synthetic tall chart evidence.', caption: 'Fictional tall chart.' }, ] as const; function withSyntheticProjectMedia(contentInput: unknown, manifestInput: unknown): { content: unknown; manifest: unknown } { const content = structuredClone(contentInput) as { projects: { selected: Array<{ assetIds: string[]; media?: unknown; mediaLayout?: unknown }> }; }; const manifest = structuredClone(manifestInput) as { assets: Array> }; const project = content.projects.selected[0]!; project.assetIds = syntheticProjectMedia.map((media) => media.assetId); project.media = syntheticProjectMedia; project.mediaLayout = 'portrait-strip'; const chartPairProject = content.projects.selected[1]!; chartPairProject.assetIds = syntheticChartPair.map((media) => media.assetId); chartPairProject.media = syntheticChartPair; chartPairProject.mediaLayout = 'chart-pair'; manifest.assets.push(...syntheticProjectMedia.map((media, index) => ({ id: media.assetId, fileName: `${media.assetId}.webp`, sha256: `${index + 1}`.repeat(64), mediaType: 'image/webp', width: 800, height: 1200, durationMs: null, rightsHolder: 'Synthetic test fixture', license: 'Test-only synthetic asset', attribution: 'Synthetic test fixture', }))); manifest.assets.push( { id: syntheticChartPair[0].assetId, fileName: `${syntheticChartPair[0].assetId}.webp`, sha256: '4'.repeat(64), mediaType: 'image/webp', width: 1200, height: 600, durationMs: null, rightsHolder: 'Synthetic test fixture', license: 'Test-only synthetic asset', attribution: 'Synthetic test fixture', }, { id: syntheticChartPair[1].assetId, fileName: `${syntheticChartPair[1].assetId}.webp`, sha256: '5'.repeat(64), mediaType: 'image/webp', width: 600, height: 1200, durationMs: null, rightsHolder: 'Synthetic test fixture', license: 'Test-only synthetic asset', attribution: 'Synthetic test fixture', }, ); return { content, manifest }; } test.beforeAll(async () => { vite = await createServer({ server: { middlewareMode: true, hmr: false, ws: false }, appType: 'custom' }); const { render } = await vite.ssrLoadModule('/src/entry-server.ts') as ServerRenderer; const { validateProductionInputs } = await vite.ssrLoadModule('/src/content/schemas.ts') as InputValidator; const fixture = (file: string): unknown => JSON.parse(readFileSync(resolve(process.cwd(), file), 'utf8')) as unknown; const syntheticFixture = withSyntheticProjectMedia( fixture('content/public/portfolio-content.json'), fixture('content/public/asset-manifest.json'), ); const inputs = validateProductionInputs( syntheticFixture.content, syntheticFixture.manifest, fixture('config/site.preview.json'), new URL('file:///prepared-assets/'), ); renderedPortfolio = await render('/', inputs); }); test.afterAll(async () => { await vite.close(); }); function informationLayerHarness(): string { return ` ${renderedPortfolio} `; } async function loadInformationLayer(page: Page): Promise { await page.setContent(informationLayerHarness()); } test('renders the validated eight-section portfolio structure before responsive checks', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); await loadInformationLayer(page); await expect(page.locator('.portfolio-main > section')).toHaveCount(8); expect(await page.locator('.portfolio-main > section').evaluateAll((sections) => sections.map((section) => section.id))) .toEqual(['start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact']); expect(await page.locator('.site-navigation a').evaluateAll((links) => links.map((link) => link.getAttribute('href')))) .toEqual(['#start', '#profile', '#experience', '#infrastructure', '#selected-builds', '#additional-work', '#toolkit', '#contact']); await expect(page.locator('#selected-builds article, #additional-work article')).not.toHaveCount(0); await expect(page.locator('#toolkit [data-testid="toolkit-technology"]')).not.toHaveCount(0); await expect(page.locator('#contact a[href^="mailto:"]')).toHaveCount(1); await expect(page.locator('#contact a[target="_blank"]')).not.toHaveCount(0); }); for (const viewport of [ { width: 320, height: 800 }, { width: 768, height: 1024 }, { width: 1440, height: 900 }, { width: 2560, height: 1440 }, ]) { test(`keeps information content within the viewport at ${viewport.width}px`, async ({ page }) => { await page.setViewportSize(viewport); await loadInformationLayer(page); const geometry = await page.evaluate(() => { const root = document.documentElement; const boxes = [...document.querySelectorAll('.portfolio-shell :is(h1, h2, h3, p, a, article, section)')] .map((element) => element.getBoundingClientRect()) .filter((rect) => rect.width > 0 && rect.height > 0); return { noOverflow: root.scrollWidth <= root.clientWidth + 1, allBoxesFit: boxes.every((rect) => rect.left >= -1 && rect.right <= root.clientWidth + 1), }; }); expect(geometry.noOverflow).toBe(true); expect(geometry.allBoxesFit).toBe(true); }); } test('uses explicit desktop layout families while preserving the primary-project road corridor', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); await loadInformationLayer(page); expect(await page.locator('.portfolio-main > section').evaluateAll((sections) => sections.map((section) => section.getAttribute('data-layout')), )).toEqual(['hero', 'right-rail', 'left-timeline', 'full-diagram', 'project-stage', 'compact-rail', 'toolkit-field', 'destination']); const projects = await page.locator('#selected-builds [data-project-priority="primary"]').evaluateAll((articles) => articles.map((article) => { const rect = article.getBoundingClientRect(); return { left: rect.left, right: rect.right, width: rect.width }; }), ); expect(projects).toHaveLength(2); expect(projects[0]!.right).toBeLessThanOrEqual(1440 * 0.51); expect(projects[1]!.left).toBeGreaterThanOrEqual(1440 * 0.53); expect(projects.every((project) => project.width <= 1440 * 0.47)).toBe(true); }); test('lays out synthetic portrait evidence in a desktop strip and preserves mobile reflow', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); await loadInformationLayer(page); const gallery = page.locator('.project-media-gallery[data-media-layout="portrait-strip"]'); await expect(gallery).toHaveCount(1); const desktopFigures = await gallery.locator('figure').evaluateAll((figures) => figures.map((figure) => { const rect = figure.getBoundingClientRect(); return { left: rect.left, width: rect.width }; })); expect(desktopFigures).toHaveLength(3); expect(desktopFigures[1]!.left).toBeGreaterThan(desktopFigures[0]!.left); expect(desktopFigures[2]!.left).toBeGreaterThan(desktopFigures[1]!.left); expect(desktopFigures.every((figure) => figure.width < 480)).toBe(true); await page.setViewportSize({ width: 320, height: 800 }); await loadInformationLayer(page); await expect(gallery).toHaveCount(1); expect(await gallery.evaluate((node) => node.scrollWidth <= node.clientWidth || getComputedStyle(node).overflowX === 'auto')).toBe(true); expect(await gallery.locator('figure').evaluateAll((figures) => figures.map((figure) => figure.textContent))) .toEqual(['Fictional demo profile.', 'Fictional demo settings.', 'Fictional demo schedule.']); expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe(true); }); test('aligns chart-pair captions for deliberately mixed source aspect ratios', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); await loadInformationLayer(page); const captions = page.locator('.project-media-gallery[data-media-layout="chart-pair"] figcaption'); await expect(captions).toHaveCount(2); const tops = await captions.evaluateAll((nodes) => nodes.map((node) => node.getBoundingClientRect().top)); expect(Math.abs(tops[0]! - tops[1]!)).toBeLessThanOrEqual(1); }); test('keeps portrait evidence and its caption within an isolated print-page height', async ({ page }) => { await page.setViewportSize({ width: 800, height: 1000 }); await page.emulateMedia({ media: 'print' }); await loadInformationLayer(page); const portrait = page.locator('.project-media-gallery[data-media-layout="portrait-strip"] figure').first(); const printGeometry = await portrait.evaluate((figure) => { const image = figure.querySelector('img'); const caption = figure.querySelector('figcaption'); if (image === null || caption === null) throw new Error('Missing synthetic portrait image or caption.'); const imageRect = image.getBoundingClientRect(); const captionRect = caption.getBoundingClientRect(); const figureStyle = getComputedStyle(figure); return { mediaAndCaptionHeight: imageRect.height + captionRect.height + Number.parseFloat(figureStyle.marginBlockEnd), printableHeight: window.innerHeight - 3 * 16, }; }); expect(printGeometry.mediaAndCaptionHeight).toBeLessThanOrEqual(printGeometry.printableHeight); }); test('uses a stable tablet rhythm and a single readable mobile column with usable navigation targets', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await loadInformationLayer(page); const tabletPanels = await page.locator('.portfolio-main > .portfolio-section').evaluateAll((sections) => Object.fromEntries(sections.map((section) => [section.getAttribute('data-layout'), section.getBoundingClientRect().toJSON()])), ); expect(tabletPanels['right-rail'].left).toBeGreaterThan(768 * 0.3); expect(tabletPanels['left-timeline'].left).toBeLessThan(768 * 0.1); expect(tabletPanels['full-diagram'].width).toBeGreaterThan(768 * 0.8); expect(tabletPanels['compact-rail'].left).toBeGreaterThan(768 * 0.3); await page.setViewportSize({ width: 320, height: 800 }); await loadInformationLayer(page); const mobile = await page.evaluate(() => { const root = document.documentElement; const panels = [...document.querySelectorAll('.portfolio-main > .portfolio-section')] .map((section) => section.getBoundingClientRect()); const navigationTargets = [...document.querySelectorAll('.site-navigation a')] .map((link) => link.getBoundingClientRect()); return { singleColumn: panels.every((panel) => panel.left >= 0 && panel.right <= root.clientWidth && panel.width >= root.clientWidth * 0.85), touchTargets: navigationTargets.every((target) => target.width >= 44 && target.height >= 44), }; }); expect(mobile.singleColumn).toBe(true); expect(mobile.touchTargets).toBe(true); }); test('turns the infrastructure route from a horizontal trust flow into a readable mobile stack', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); await loadInformationLayer(page); const desktop = await page.locator('[data-testid="infrastructure-flow"] [data-infrastructure-node]').evaluateAll((nodes) => nodes.map((node) => { const rect = node.getBoundingClientRect(); return { id: node.getAttribute('data-infrastructure-node'), left: rect.left, top: rect.top, bottom: rect.bottom, width: rect.width }; }), ); expect(desktop.slice(0, 4).every((node) => Math.abs(node.top - desktop[0]!.top) <= 1)).toBe(true); expect(desktop[4]!.top).toBeGreaterThanOrEqual(desktop[3]!.bottom); expect(desktop.slice(0, 4).every((node, index, nodes) => index === 0 || node.left > nodes[index - 1]!.left)).toBe(true); await page.setViewportSize({ width: 320, height: 800 }); await loadInformationLayer(page); const mobile = await page.locator('[data-testid="infrastructure-flow"] [data-infrastructure-node]').evaluateAll((nodes) => nodes.map((node) => { const rect = node.getBoundingClientRect(); return { left: rect.left, top: rect.top, bottom: rect.bottom, width: rect.width }; }), ); expect(mobile.every((node) => Math.abs(node.left - mobile[0]!.left) <= 1)).toBe(true); expect(mobile.every((node) => node.width >= 200)).toBe(true); expect(mobile.every((node, index, nodes) => index === 0 || node.top > nodes[index - 1]!.bottom)).toBe(true); }); test('keeps the compact HUD in one safe lower row without covering the road corridor', async ({ page }) => { for (const viewport of [ { width: 320, height: 800 }, { width: 1440, height: 900 }, ]) { await page.setViewportSize(viewport); await loadInformationLayer(page); const geometry = await page.evaluate(() => { const controls = document.querySelector('.drive-controls'); const hud = document.querySelector('.journey-hud'); if (!controls || !hud) throw new Error('Missing vehicle HUD'); const controlRect = controls.getBoundingClientRect(); const hudRect = hud.getBoundingClientRect(); return { aligned: Math.abs(controlRect.bottom - hudRect.bottom) <= 1, separate: controlRect.right <= hudRect.left, controlsTargetSize: [...controls.querySelectorAll('button')] .every((button) => button.getBoundingClientRect().height >= 44), centerClear: controlRect.right < window.innerWidth * 0.42 && hudRect.left > window.innerWidth * 0.58, }; }); expect(geometry.aligned).toBe(true); expect(geometry.separate).toBe(true); expect(geometry.controlsTargetSize).toBe(true); if (viewport.width >= 768) expect(geometry.centerClear).toBe(true); } }); test('moves drive controls into document flow in short landscape', async ({ page }) => { await page.setViewportSize({ width: 800, height: 390 }); await loadInformationLayer(page); const geometry = await page.evaluate(() => { const controls = document.querySelector('.drive-controls'); const contact = document.querySelector('#contact'); if (!controls || !contact) throw new Error('Missing short-landscape controls'); const controlsRect = controls.getBoundingClientRect(); const contactRect = contact.getBoundingClientRect(); return { position: getComputedStyle(controls).position, afterContact: controlsRect.top >= contactRect.bottom, }; }); expect(geometry).toEqual({ position: 'relative', afterContact: true }); }); test('keeps actual standalone project and contact destinations at least 44 by 44px on mobile', async ({ page }) => { await page.setViewportSize({ width: 320, height: 800 }); await loadInformationLayer(page); const targets = await page.locator('#selected-builds article > a, #additional-work article > a, #contact a').evaluateAll((links) => links.map((link) => { const rect = link.getBoundingClientRect(); return { href: link.getAttribute('href'), width: rect.width, height: rect.height }; }), ); expect(targets.length).toBeGreaterThan(0); expect(targets.every((target) => target.width >= 44 && target.height >= 44)).toBe(true); }); test('removes nonessential information-layer motion when reduced motion is requested', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }); await loadInformationLayer(page); const duration = await page.locator('.portfolio-section').first().evaluate((element) => getComputedStyle(element).transitionDuration); expect(duration).toBe('0.001s'); }); test('creates a clean print portfolio with a visible label but no resume packaging', async ({ page }) => { await page.emulateMedia({ media: 'print' }); await loadInformationLayer(page); await expect(page.locator('.site-navigation')).toBeHidden(); await expect(page.locator('.print-portfolio-label')).toBeVisible(); await expect(page.locator('.portfolio-section').first()).toHaveCSS('break-inside', 'avoid'); const printCopy = await page.locator('body').innerText(); expect(printCopy).not.toMatch(/\b(?:Résumé|Resume)\b/u); await expect(page.locator('a[download]')).toHaveCount(0); });