test: lock semantic and no-script portfolio behavior
This commit is contained in:
133
tests/browser/hydration.spec.ts
Normal file
133
tests/browser/hydration.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
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 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('.site-header > p')?.textContent?.trim() ?? '',
|
||||
landmarks: {
|
||||
headers: document.querySelectorAll('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 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'}`,
|
||||
),
|
||||
);
|
||||
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(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);
|
||||
});
|
||||
Reference in New Issue
Block a user