236 lines
7.8 KiB
TypeScript
236 lines
7.8 KiB
TypeScript
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(
|
|
/<script\b(?=[^>]*\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<DocumentSnapshot> =>
|
|
page.evaluate(() => ({
|
|
text: (document.querySelector('.portfolio-shell')?.textContent ?? '')
|
|
.replace(/\s+/gu, ' ')
|
|
.trim(),
|
|
sectionIds: [
|
|
...document.querySelectorAll<HTMLElement>('main > section'),
|
|
].map((section) => section.id),
|
|
sectionHeadings: [
|
|
...document.querySelectorAll<HTMLElement>('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<void>((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();
|
|
});
|