import { expect, test } from '../helpers/serve-dist'; interface DocumentSnapshot { text: string; sectionIds: string[]; sectionHeadings: string[]; h1: string; role: string; landmarks: { headers: number; mains: number; navigations: number; footers: number; }; } const generatedClientEntry = (rawHtml: string): string => { const entries = [ ...rawHtml.matchAll( /]*\btype="module")(?=[^>]*\bsrc="([^"]+)")[^>]*><\/script>/gu, ), ].map((match) => match[1]); expect(entries).toHaveLength(1); expect(entries[0]).toMatch(/^\/assets\/[^/]+\.js$/u); return entries[0]!; }; const captureDocument = async ( page: import('@playwright/test').Page, ): Promise => page.evaluate(() => ({ text: (document.querySelector('.portfolio-shell')?.textContent ?? '') .replace(/\s+/gu, ' ') .trim(), sectionIds: [ ...document.querySelectorAll('main > section'), ].map((section) => section.id), sectionHeadings: [ ...document.querySelectorAll('main > section > h2'), ].map((heading) => heading.innerText.trim()), h1: document.querySelector('h1')?.textContent?.trim() ?? '', role: document.querySelector('.hero-journey__role')?.textContent?.trim() ?? '', landmarks: { headers: document.querySelectorAll('.site-header').length, mains: document.querySelectorAll('main').length, navigations: document.querySelectorAll('nav').length, footers: document.querySelectorAll('footer').length, }, })); test('hydration preserves the built subject matter and landmark structure without errors or external fetches', async ({ browser, page, distUrl, }, testInfo) => { test.skip( testInfo.project.name === 'mobile-chromium', 'Hydration identity is covered once; semantic and no-script behavior run on mobile.', ); const beforePage = await browser.newPage({ javaScriptEnabled: false, viewport: { width: 1440, height: 900 }, }); await beforePage.goto(distUrl, { waitUntil: 'load' }); const before = await captureDocument(beforePage); await beforePage.close(); const pageErrors: string[] = []; const consoleErrors: string[] = []; const failedRequests: string[] = []; const nonSuccessfulResponses: string[] = []; const requests: string[] = []; page.on('pageerror', (error) => pageErrors.push(error.message)); page.on('console', (message) => { if (message.type() === 'error') consoleErrors.push(message.text()); }); page.on('request', (request) => requests.push(request.url())); page.on('requestfailed', (request) => failedRequests.push( `${request.url()}: ${request.failure()?.errorText ?? 'failed'}`, ), ); page.on('response', (response) => { if (response.status() < 200 || response.status() >= 300) { nonSuccessfulResponses.push(`${response.status()} ${response.url()}`); } }); await page.addInitScript(() => { const unhandled: string[] = []; Reflect.set(globalThis, '__portfolioUnhandledRejections', unhandled); addEventListener('unhandledrejection', (event) => unhandled.push(String(event.reason)), ); }); await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => { const app = document.querySelector('#app'); return app !== null && Reflect.has(app, '__vue_app__'); }); await page.waitForFunction(() => document.readyState === 'complete'); await page.evaluate( () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())), ), ); const after = await captureDocument(page); const unhandledRejections = await page.evaluate( () => (Reflect.get(globalThis, '__portfolioUnhandledRejections') as string[] | undefined) ?? [], ); expect(after).toEqual(before); expect(after.h1).toBe('Luca Pîslaru'); expect(after.role).toBe('System Administrator & Full-Stack Developer'); expect(after.sectionIds).toEqual([ 'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact', ]); expect(after.landmarks).toEqual({ headers: 1, mains: 1, navigations: 1, footers: 1, }); expect(pageErrors).toEqual([]); expect(consoleErrors).toEqual([]); expect(unhandledRejections).toEqual([]); expect(failedRequests).toEqual([]); expect(nonSuccessfulResponses).toEqual([]); expect(requests.length).toBeGreaterThan(1); expect( requests.every((requestUrl) => { const url = new URL(requestUrl); return ( url.origin === distUrl && (url.pathname === '/' || url.pathname.startsWith('/assets/')) ); }), ).toBe(true); }); test('an aborted generated client entry leaves the complete SSR portfolio usable', async ({ page, distUrl, request, }, testInfo) => { test.skip( testInfo.project.name === 'mobile-chromium', 'The exact entry-failure boundary is viewport-independent; mobile no-script and skip activation run separately.', ); const rawResponse = await request.get(`${distUrl}/`); expect(rawResponse.status()).toBe(200); const rawHtml = await rawResponse.text(); const entryUrl = new URL(generatedClientEntry(rawHtml), distUrl).href; const expectedText = await page.evaluate((html) => { const document = new DOMParser().parseFromString(html, 'text/html'); return (document.querySelector('.portfolio-shell')?.textContent ?? '') .replace(/\s+/gu, ' ') .trim(); }, rawHtml); const pageErrors: string[] = []; const consoleErrors: string[] = []; const failedRequests: Array<{ url: string; error: string }> = []; const nonSuccessfulResponses: 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) => failedRequests.push({ url: request.url(), error: request.failure()?.errorText ?? 'failed', }), ); page.on('response', (response) => { if (response.status() < 200 || response.status() >= 300) { nonSuccessfulResponses.push(`${response.status()} ${response.url()}`); } }); await page.addInitScript(() => { const unhandled: string[] = []; Reflect.set(globalThis, '__portfolioUnhandledRejections', unhandled); addEventListener('unhandledrejection', (event) => unhandled.push(String(event.reason)), ); }); await page.route(entryUrl, (route) => route.abort('failed')); await page.goto(distUrl, { waitUntil: 'load' }); const appMounted = await page .locator('#app') .evaluate((app) => Reflect.has(app, '__vue_app__')); const unhandledRejections = await page.evaluate( () => (Reflect.get(globalThis, '__portfolioUnhandledRejections') as string[] | undefined) ?? [], ); expect(appMounted).toBe(false); expect((await captureDocument(page)).text).toBe(expectedText); expect(failedRequests).toEqual([{ url: entryUrl, error: 'net::ERR_FAILED' }]); expect(pageErrors).toEqual([]); expect(unhandledRejections).toEqual([]); expect(nonSuccessfulResponses).toEqual([]); expect( consoleErrors.every((message) => /Failed to load resource.*ERR_FAILED/iu.test(message), ), ).toBe(true); expect(consoleErrors.length).toBeLessThanOrEqual(1); await expect(page.locator('h1')).toHaveText('Luca Pîslaru'); await expect(page.locator('main > section')).toHaveCount(8); await expect(page.locator('nav a')).toHaveCount(8); await expect( page.locator('a[href="mailto:luca.pislaru@gmail.com"]'), ).toBeVisible(); 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(); });