113 lines
5.7 KiB
TypeScript
113 lines
5.7 KiB
TypeScript
import { expect, test } from '../helpers/serve-dist';
|
||
import {
|
||
assertSemanticFaultOracle,
|
||
authorizedFaultUrl,
|
||
collectBrowserErrors,
|
||
missingTokenFaultUrl,
|
||
readFaultSnapshot,
|
||
readUnhandledRejections,
|
||
wrongTokenFaultUrl,
|
||
} from '../helpers/test-build-auth';
|
||
import { installSceneInstrumentation, readSceneInstrumentation } from '../helpers/scene-instrumentation';
|
||
import type { PortfolioFaultId } from '@/testing/fault-hooks';
|
||
|
||
const assertNoEscapedErrors = async (
|
||
page: Parameters<typeof readUnhandledRejections>[0],
|
||
pageErrors: readonly string[],
|
||
): Promise<void> => {
|
||
expect(pageErrors).toEqual([]);
|
||
expect(await readUnhandledRejections(page)).toEqual([]);
|
||
expect((await readSceneInstrumentation(page)).unhandledRejections).toEqual([]);
|
||
};
|
||
|
||
test.describe('authenticated fault-build boundary', () => {
|
||
test.beforeEach(async ({}, testInfo) => {
|
||
test.skip(testInfo.project.name !== 'desktop-chromium', 'The complete F1–F13 matrix runs once on current desktop Chromium.');
|
||
});
|
||
|
||
test('missing token exposes no fault API and leaves the portfolio operational', async ({ page, distUrl }) => {
|
||
await page.goto(missingTokenFaultUrl(distUrl, 'F1-scene-chunk'), { waitUntil: 'domcontentloaded' });
|
||
expect(await page.evaluate(() => '__PORTFOLIO_TEST_HOOKS__' in window)).toBe(false);
|
||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
|
||
await assertSemanticFaultOracle(page);
|
||
});
|
||
|
||
test('wrong token exposes no fault API and leaves the portfolio operational', async ({ page, distUrl }) => {
|
||
await page.goto(wrongTokenFaultUrl(distUrl, 'F1-scene-chunk'), { waitUntil: 'domcontentloaded' });
|
||
expect(await page.evaluate(() => '__PORTFOLIO_TEST_HOOKS__' in window)).toBe(false);
|
||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
|
||
await assertSemanticFaultOracle(page);
|
||
});
|
||
|
||
test('correct token exposes only the frozen named fault interface', async ({ page, distUrl }) => {
|
||
await page.goto(authorizedFaultUrl(distUrl, 'F9-hydration-failure'), { waitUntil: 'domcontentloaded' });
|
||
await expect.poll(() => page.evaluate(() => '__PORTFOLIO_TEST_HOOKS__' in window)).toBe(true);
|
||
const shape = await page.evaluate(() => {
|
||
const hooks = (window as unknown as { __PORTFOLIO_TEST_HOOKS__: object }).__PORTFOLIO_TEST_HOOKS__;
|
||
return { frozen: Object.isFrozen(hooks), keys: Object.keys(hooks).sort() };
|
||
});
|
||
expect(shape).toEqual({ frozen: true, keys: ['fault', 'snapshot', 'trigger', 'version'] });
|
||
expect((await readFaultSnapshot(page)).fault).toBe('F9-hydration-failure');
|
||
await expect(page.locator('.drive-controls')).toHaveCount(1);
|
||
await assertSemanticFaultOracle(page);
|
||
});
|
||
});
|
||
|
||
test.describe('scene failure matrix', () => {
|
||
test.beforeEach(async ({}, testInfo) => {
|
||
test.skip(testInfo.project.name !== 'desktop-chromium', 'The complete F1–F13 matrix runs once on current desktop Chromium.');
|
||
});
|
||
|
||
for (const fault of [
|
||
'F1-scene-chunk',
|
||
'F1-shader',
|
||
'F1-car',
|
||
'F1-texture',
|
||
] as const satisfies readonly PortfolioFaultId[]) {
|
||
test(`${fault} enters one static fallback without escaping an error`, async ({ page, distUrl }) => {
|
||
await installSceneInstrumentation(page);
|
||
const errors = await collectBrowserErrors(page);
|
||
await page.goto(authorizedFaultUrl(distUrl, fault), { waitUntil: 'domcontentloaded' });
|
||
|
||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'static');
|
||
await expect(page.locator('.journey-world')).toHaveAttribute('data-static-fallback', 'true');
|
||
await expect(page.locator('.journey-poster')).toHaveCount(1);
|
||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||
await expect(page.locator('.drive-controls__status')).toHaveText('Visuals unavailable');
|
||
await assertSemanticFaultOracle(page);
|
||
const resources = await readSceneInstrumentation(page);
|
||
expect(resources.activeAnimationFrames).toBe(0);
|
||
expect(resources.maximumCanvasCount).toBeLessThanOrEqual(1);
|
||
expect((await readFaultSnapshot(page)).staticFallbackCount).toBe(1);
|
||
await assertNoEscapedErrors(page, errors.pageErrors);
|
||
});
|
||
}
|
||
|
||
test('F2 enforces the literal five-second first-frame deadline', async ({ page, distUrl }) => {
|
||
test.setTimeout(12_000);
|
||
await installSceneInstrumentation(page);
|
||
const errors = await collectBrowserErrors(page);
|
||
const startedAt = Date.now();
|
||
await page.goto(authorizedFaultUrl(distUrl, 'F2-first-frame-timeout'), { waitUntil: 'domcontentloaded' });
|
||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'static', { timeout: 7_000 });
|
||
expect(Date.now() - startedAt).toBeGreaterThanOrEqual(4_800);
|
||
await expect(page.locator('.journey-world')).toHaveAttribute('data-static-fallback', 'true');
|
||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||
await assertSemanticFaultOracle(page);
|
||
await assertNoEscapedErrors(page, errors.pageErrors);
|
||
});
|
||
|
||
test('F3 contains a post-first-frame exception on the next frame', async ({ page, distUrl }) => {
|
||
await installSceneInstrumentation(page);
|
||
const errors = await collectBrowserErrors(page);
|
||
await page.goto(authorizedFaultUrl(distUrl, 'F3-post-first-frame-exception'), { waitUntil: 'domcontentloaded' });
|
||
|
||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'static');
|
||
await expect(page.locator('.journey-world')).toHaveAttribute('data-static-fallback', 'true');
|
||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||
await assertSemanticFaultOracle(page);
|
||
expect((await readSceneInstrumentation(page)).activeAnimationFrames).toBe(0);
|
||
await assertNoEscapedErrors(page, errors.pageErrors);
|
||
});
|
||
});
|