import type { PortfolioContent } from '@/content/types'; import { expect, expectInsetFocusIndicator, test } from '../helpers/serve-dist'; test.use({ javaScriptEnabled: false }); const sectionIds = [ 'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact', ] as const; const sectionHeadings = [ 'START', 'PROFILE', 'EXPERIENCE', 'INFRASTRUCTURE', 'SELECTED BUILDS', 'ADDITIONAL WORK', 'TOOLKIT', 'CONTACT', ] as const; const visibleTextLeafSelector = 'h1,h2,h3,p,dt,dd,li,figcaption,a,svg text'; const visibleTextSelector = [ '.site-header :is(h1,h2,h3,p,dt,dd,li,figcaption,a)', '.portfolio-main :is(h1,h2,h3,p,dt,dd,li,figcaption,a)', '.portfolio-main svg text', ].join(','); const normalizedText = (value: string): string => value.replace(/\s+/gu, ' ').trim(); type TextMultiset = Array<[text: string, count: number]>; const textMultiset = (values: readonly string[]): TextMultiset => { const counts = new Map(); for (const value of values.map(normalizedText).filter(Boolean)) { counts.set(value, (counts.get(value) ?? 0) + 1); } return [...counts].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0, ); }; const missingText = ( expected: TextMultiset, actual: TextMultiset, ): TextMultiset => { const actualCounts = new Map(actual); return expected.flatMap(([text, count]) => { const missing = count - (actualCounts.get(text) ?? 0); return missing > 0 ? [[text, missing] as [string, number]] : []; }); }; const portfolioContentFromSnapshot = async ( page: import('@playwright/test').Page, rawHtml: string, ): Promise => page.evaluate((html) => { const document = new DOMParser().parseFromString(html, 'text/html'); const snapshot = document.querySelector('#portfolio-inputs'); if (snapshot?.textContent === null || snapshot?.textContent === undefined) { throw new Error('Missing #portfolio-inputs snapshot'); } return (JSON.parse(snapshot.textContent) as { content: PortfolioContent }) .content; }, rawHtml); const expectedVisibleTextSegments = ( content: Readonly, ): string[] => { const navigationById = new Map( content.navigation.map((item) => [item.id, item.label]), ); const section = ( id: (typeof sectionIds)[number], index: number, body: readonly string[], ): string[] => { const label = navigationById.get(id); if (label === undefined) throw new Error(`Missing navigation label: ${id}`); const heading = label.toUpperCase(); return [`${String(index).padStart(2, '0')} / ${heading}`, heading, ...body]; }; const projectLinkText = ( project: PortfolioContent['projects']['selected'][number], ): string[] => project.visibility === 'public' && project.url ? [project.title] : []; return [ content.identity.name, content.identity.role, ...sectionIds.map((id) => { const label = navigationById.get(id); if (label === undefined) throw new Error(`Missing navigation label: ${id}`); return label; }), ...section('start', 1, [content.identity.valueProposition]), ...section('profile', 2, [ content.profile.introduction, ...content.profile.screeningFacts.flatMap((fact) => [ fact.label, fact.value, ]), ]), ...section('experience', 3, [ ...content.experience.flatMap((record) => [ record.role, `${record.organization} · ${record.dates}${record.location === undefined ? '' : ` · ${record.location}`}`, ...record.highlights, ]), content.systemsOperations.title, content.systemsOperations.summary, ...content.systemsOperations.evidence, ]), ...section('infrastructure', 4, [ content.infrastructure.title, content.infrastructure.summary, ...content.infrastructure.bands.map((band) => band.title), content.infrastructure.title, ...content.infrastructure.bands.map( (band) => `${band.title}: ${band.summary}`, ), ]), ...section( 'selected-builds', 5, content.projects.selected.flatMap((project) => [ project.title, project.summary, 'Problem', project.problem, 'Contribution', project.contribution, 'Technical decisions', ...project.technicalDecisions, 'Outcome', project.outcome, ...projectLinkText(project), ]), ), ...section( 'additional-work', 6, content.projects.additional.flatMap((project) => [ project.title, project.summary, project.contribution, project.outcome, ...projectLinkText(project), ]), ), ...section( 'toolkit', 7, content.toolkit.flatMap((group) => [ group.title, ...group.technologies.map((technology) => technology.name), ]), ), ...section('contact', 8, [ content.identity.name, content.identity.role, content.contact.email, content.contact.cvMessage, ...content.contact.social.map((link) => link.service), ]), ]; }; const rawVisibleTextSegments = async ( page: import('@playwright/test').Page, rawHtml: string, ): Promise => page.evaluate( ({ html, leafSelector, rootSelector }) => { const document = new DOMParser().parseFromString(html, 'text/html'); const isHidden = (element: Element): boolean => { for ( let current: Element | null = element; current; current = current.parentElement ) { if ( current.matches('template,script,style,noscript') || current.hasAttribute('hidden') || current.hasAttribute('inert') ) { return true; } const ariaHidden = current.getAttribute('aria-hidden'); if ( ariaHidden !== null && ariaHidden.trim().toLowerCase() !== 'false' ) { return true; } const hiddenClass = current.getAttribute('class')?.toLowerCase() ?? ''; if ( /(?:^|\s|-)print-only(?:\s|-|$)|print-portfolio-label|visually-hidden|sr-only|screen-reader-only/u.test( hiddenClass, ) ) { return true; } const inlineStyle = current.getAttribute('style')?.toLowerCase() ?? ''; if ( /(?:^|;)\s*display\s*:\s*none\b/u.test(inlineStyle) || /(?:^|;)\s*visibility\s*:\s*hidden\b/u.test(inlineStyle) || /(?:^|;)\s*opacity\s*:\s*0(?:[;\s]|$)/u.test(inlineStyle) ) { return true; } } return false; }; return [...document.querySelectorAll(rootSelector)] .filter( (element) => !isHidden(element) && element.querySelector(leafSelector) === null, ) .map((element) => (element.textContent ?? '').replace(/\s+/gu, ' ').trim(), ) .filter((text) => text.length > 0); }, { html: rawHtml, leafSelector: visibleTextLeafSelector, rootSelector: visibleTextSelector, }, ); const browserVisibleTextSegments = async ( page: import('@playwright/test').Page, ): Promise => page.locator(visibleTextSelector).evaluateAll( (elements, leafSelector) => elements .filter((element) => { for ( let current: Element | null = element; current; current = current.parentElement ) { if ( current.matches('template,script,style,noscript') || current.hasAttribute('hidden') || current.hasAttribute('inert') ) { return false; } const ariaHidden = current.getAttribute('aria-hidden'); if ( ariaHidden !== null && ariaHidden.trim().toLowerCase() !== 'false' ) { return false; } const hiddenClass = current.getAttribute('class')?.toLowerCase() ?? ''; if ( /(?:^|\s|-)print-only(?:\s|-|$)|print-portfolio-label|visually-hidden|sr-only|screen-reader-only/u.test( hiddenClass, ) ) { return false; } const style = getComputedStyle(current); if ( style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' ) { return false; } } return ( element.querySelector(leafSelector) === null && element.getClientRects().length > 0 ); }) .map((element) => (element.textContent ?? '').replace(/\s+/gu, ' ').trim(), ) .filter((text) => text.length > 0), visibleTextLeafSelector, ); test('keeps every raw SSR reading-path segment visible without JavaScript', async ({ page, distUrl, request, }) => { const pageErrors: string[] = []; const consoleErrors: string[] = []; const failedRequests: string[] = []; page.on('pageerror', (error) => pageErrors.push(error.message)); page.on('console', (message) => { if (message.type() === 'error') consoleErrors.push(message.text()); }); page.on('requestfailed', (request) => { if (request.resourceType() !== 'script') { failedRequests.push( `${request.url()}: ${request.failure()?.errorText ?? 'failed'}`, ); } }); const rawResponse = await request.get(`${distUrl}/`); expect(rawResponse.status()).toBe(200); const rawHtml = await rawResponse.text(); const content = await portfolioContentFromSnapshot(page, rawHtml); const expectedVisibleText = textMultiset( expectedVisibleTextSegments(content), ); const rawVisibleText = textMultiset( await rawVisibleTextSegments(page, rawHtml), ); await page.goto(distUrl, { waitUntil: 'load' }); const actualVisibleText = textMultiset( await browserVisibleTextSegments(page), ); expect(expectedVisibleText.length).toBeGreaterThan(80); expect(rawVisibleText).toEqual(expectedVisibleText); expect(actualVisibleText).toEqual(expectedVisibleText); expect(normalizedText(JSON.stringify(actualVisibleText))).not.toContain( 'preview-semantic-v1', ); await expect(page.locator('h1:visible')).toHaveCount(1); await expect(page.locator('h1')).toHaveText('Luca Pîslaru'); await expect(page.locator('.site-header > p').first()).toHaveText( 'System Administrator & Full-Stack Developer', ); await expect(page.locator('main')).toBeVisible(); await expect( page.locator('nav[aria-label="Portfolio sections"]'), ).toBeVisible(); await expect(page.locator('footer')).toBeVisible(); expect( await page .locator('main > section') .evaluateAll((sections) => sections.map((section) => section.id)), ).toEqual(sectionIds); expect(await page.locator('main > section > h2').allTextContents()).toEqual( sectionHeadings, ); for (const evidence of [ 'Professional systems operations', 'Secure self-hosted platform and lifecycle', 'Private self-hosted medication tracker', 'Footy Tracker', 'Linux', 'Docker', 'Python', 'Vue.js', ]) { await expect( page.getByText(evidence, { exact: true }).first(), ).toBeVisible(); } const email = page.locator('a[href="mailto:luca.pislaru@gmail.com"]'); await expect(email).toBeVisible(); await expect(email).toHaveText('luca.pislaru@gmail.com'); await expect( page.getByText( 'For a current CV or role-specific information, contact me directly.', { exact: true }, ), ).toBeVisible(); for (const title of [ 'Private self-hosted medication tracker', 'Private healthcare platform', 'Private game prototype', ]) { await expect( page.locator('article').filter({ hasText: title }).locator('a'), ).toHaveCount(0); } await expect(page.locator('a[download]')).toHaveCount(0); expect(pageErrors).toEqual([]); expect(consoleErrors).toEqual([]); expect(failedRequests).toEqual([]); }); test('content oracle detects a removed highlight and an inert reading path', async ({ page, distUrl, request, }) => { const rawResponse = await request.get(`${distUrl}/`); expect(rawResponse.status()).toBe(200); const rawHtml = await rawResponse.text(); const content = await portfolioContentFromSnapshot(page, rawHtml); const expected = textMultiset(expectedVisibleTextSegments(content)); const firstHighlight = content.experience[0]?.highlights[0]; expect(firstHighlight).toBeTruthy(); const withoutHighlight = await page.evaluate( ({ html, highlight }) => { const document = new DOMParser().parseFromString(html, 'text/html'); const target = [...document.querySelectorAll('li')].find( (element) => (element.textContent ?? '').replace(/\s+/gu, ' ').trim() === highlight, ); if (target === undefined) throw new Error('Expected highlight is absent'); target.remove(); return `${document.documentElement.outerHTML}`; }, { html: rawHtml, highlight: firstHighlight ?? '' }, ); const withoutHighlightText = textMultiset( await rawVisibleTextSegments(page, withoutHighlight), ); expect(withoutHighlightText).not.toEqual(expected); const missingHighlight = missingText(expected, withoutHighlightText); expect(missingHighlight).toEqual([[normalizedText(firstHighlight ?? ''), 1]]); expect(missingText(withoutHighlightText, expected)).toEqual([]); const inertMain = await page.evaluate((html) => { const document = new DOMParser().parseFromString(html, 'text/html'); const main = document.querySelector('main'); if (main === null) throw new Error('Expected main is absent'); main.setAttribute('inert', ''); return `${document.documentElement.outerHTML}`; }, rawHtml); const inertMainText = textMultiset( await rawVisibleTextSegments(page, inertMain), ); expect(inertMainText).not.toEqual(expected); const missingWhenInert = missingText(expected, inertMainText); expect(missingWhenInert.length).toBeGreaterThan(80); expect(missingWhenInert).toContainEqual([ normalizedText(content.identity.valueProposition), 1, ]); expect(missingText(inertMainText, expected)).toEqual([]); }); test('activating the skip link focuses visible portfolio content without JavaScript', async ({ page, distUrl, }) => { await page.goto(distUrl, { waitUntil: 'load' }); await page.keyboard.press('Tab'); await expect(page.locator('.skip-link')).toBeFocused(); await page.keyboard.press('Enter'); await expect(page).toHaveURL(`${distUrl}/#portfolio-content`); await expect(page.locator('#portfolio-content')).toBeFocused(); await expect(page.locator('#portfolio-content')).toHaveAttribute( 'tabindex', '-1', ); await expectInsetFocusIndicator(page, '#portfolio-content'); }); test('retains native section navigation when scripts are disabled', async ({ page, distUrl, }) => { await page.goto(distUrl, { waitUntil: 'load' }); await page.locator('nav a[href="#contact"]').click(); await expect(page).toHaveURL(`${distUrl}/#contact`); await expect(page.locator('#contact')).toBeInViewport(); await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible(); });