Files
Portfolio/tests/helpers/test-build-auth.ts

178 lines
7.4 KiB
TypeScript

import { createHash, randomBytes } from 'node:crypto';
import { execFile } from 'node:child_process';
import { copyFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { promisify } from 'node:util';
import { expect, type FullConfig, type Page } from '@playwright/test';
import type { PortfolioFaultId } from '@/testing/fault-hooks';
const execute = promisify(execFile);
const repositoryRoot = process.cwd();
const temporaryClientRoot = resolve(repositoryRoot, '.portfolio-test-client');
const run = async (command: string, args: readonly string[], environment = process.env): Promise<void> => {
await execute(command, [...args], {
cwd: repositoryRoot,
env: environment,
maxBuffer: 16 * 1024 * 1024,
});
};
const firstMatch = (source: string, expression: RegExp, label: string): string => {
const matches = source.match(expression);
if (matches === null || matches.length !== 1) throw new Error(`Test client is missing ${label}.`);
return matches[0];
};
const buildAuthenticatedClient = async (tokenDigest: string): Promise<void> => {
await rm(temporaryClientRoot, { recursive: true, force: true });
await run('npm', ['run', 'build:preview']);
await run(
resolve(repositoryRoot, 'node_modules/.bin/vite'),
['build', '--outDir', temporaryClientRoot, '--manifest'],
{
...process.env,
PORTFOLIO_TEST_BUILD: '1',
PORTFOLIO_TEST_TOKEN_DIGEST: tokenDigest,
},
);
const productionIndexPath = resolve(repositoryRoot, 'dist/index.html');
const testIndexPath = resolve(temporaryClientRoot, 'index.html');
const [productionIndex, testIndex] = await Promise.all([
readFile(productionIndexPath, 'utf8'),
readFile(testIndexPath, 'utf8'),
]);
const testModule = firstMatch(testIndex, /<script type="module"[^>]+><\/script>/u, 'entry module');
const testStyle = firstMatch(testIndex, /<link rel="stylesheet"[^>]+>/u, 'stylesheet');
const authenticatedIndex = productionIndex
.replace(/\s*<link rel="modulepreload"[^>]+>/gu, '')
.replace(/<script type="module"[^>]+><\/script>/u, testModule)
.replace(/<link rel="stylesheet"[^>]+>/u, testStyle);
const destinationAssets = resolve(repositoryRoot, 'dist/assets');
await mkdir(destinationAssets, { recursive: true });
for (const entry of await readdir(resolve(temporaryClientRoot, 'assets'), { withFileTypes: true })) {
if (!entry.isFile()) throw new Error('The authenticated test client emitted an unexpected nested asset.');
await copyFile(
resolve(temporaryClientRoot, 'assets', entry.name),
resolve(destinationAssets, entry.name),
);
}
await writeFile(productionIndexPath, authenticatedIndex);
await rm(temporaryClientRoot, { recursive: true, force: true });
};
export default async function globalSetup(_config: FullConfig): Promise<() => Promise<void>> {
const token = randomBytes(32).toString('hex');
const tokenDigest = createHash('sha256').update(token).digest('hex');
process.env.PORTFOLIO_TEST_TOKEN = token;
await buildAuthenticatedClient(tokenDigest);
return async () => {
await rm(temporaryClientRoot, { recursive: true, force: true });
delete process.env.PORTFOLIO_TEST_BUILD;
delete process.env.PORTFOLIO_TEST_TOKEN_DIGEST;
await run('npm', ['run', 'build:preview']);
};
}
const withFault = (origin: string, token: string | null, fault: PortfolioFaultId): string => {
const url = new URL(origin);
url.searchParams.set('__portfolio_fault', fault);
if (token !== null) url.searchParams.set('__portfolio_test_token', token);
return url.href;
};
export const authorizedFaultUrl = (origin: string, fault: PortfolioFaultId): string => {
const token = process.env.PORTFOLIO_TEST_TOKEN;
if (token === undefined) throw new Error('The per-run portfolio test token is unavailable.');
return withFault(origin, token, fault);
};
export const missingTokenFaultUrl = (origin: string, fault: PortfolioFaultId): string => withFault(origin, null, fault);
export const wrongTokenFaultUrl = (origin: string, fault: PortfolioFaultId): string => withFault(
origin,
'00'.repeat(32),
fault,
);
export const authorizedPerformanceUrl = (origin: string, tier: 'high' | 'low'): string => {
const token = process.env.PORTFOLIO_TEST_TOKEN;
if (token === undefined) throw new Error('The per-run portfolio test token is unavailable.');
const url = new URL(origin);
url.searchParams.set('__portfolio_performance', 'drive-trace.v1');
url.searchParams.set('__portfolio_test_token', token);
url.searchParams.set('__portfolio_tier', tier);
return url.href;
};
export interface BrowserErrorCollector {
readonly pageErrors: string[];
}
export const collectBrowserErrors = async (page: Page): Promise<BrowserErrorCollector> => {
const pageErrors: string[] = [];
page.on('pageerror', (error) => pageErrors.push(error.message));
await page.addInitScript(() => {
const target = window as unknown as { __faultUnhandledRejections?: string[] };
target.__faultUnhandledRejections = [];
window.addEventListener('unhandledrejection', (event) => {
target.__faultUnhandledRejections!.push(event.reason instanceof Error ? event.reason.message : String(event.reason));
});
});
return { pageErrors };
};
export const readUnhandledRejections = (page: Page): Promise<string[]> => page.evaluate(() => (
[...((window as unknown as { __faultUnhandledRejections?: string[] }).__faultUnhandledRejections ?? [])]
));
export const assertSemanticFaultOracle = async (page: Page): Promise<void> => {
await expect(page.getByRole('heading', { level: 1, name: 'Luca Pîslaru' })).toHaveCount(1);
await expect(page.locator('main > section')).toHaveCount(8);
await expect(page.locator('nav[aria-label="Portfolio sections"] a')).toHaveCount(8);
await expect(page.locator('nav a[href="#contact"]')).toBeVisible();
const email = page.locator('#contact :is(a, summary):text-is("Email Me")');
await expect(email).toBeVisible();
for (const title of [
'Private self-hosted medication tracker',
'Private healthcare platform',
'Private game prototype',
]) {
await expect(page.locator('article', { hasText: title }).locator('a.project-link')).toHaveCount(0);
}
await page.locator('nav a[href="#contact"]').focus();
await expect(page.locator('nav a[href="#contact"]')).toBeFocused();
await expect(page.locator('.journey-world')).toHaveCount(1);
};
export interface TestHookSnapshot {
fault: PortfolioFaultId;
events: string[];
sceneState: string | null;
canvasCount: number;
posterCount: number;
staticFallbackCount: number;
audioStatus: string | null;
videoStatus: string | null;
}
export const readFaultSnapshot = (page: Page): Promise<TestHookSnapshot> => page.evaluate(() => {
const hooks = (window as unknown as {
__PORTFOLIO_TEST_HOOKS__?: { snapshot(): TestHookSnapshot };
}).__PORTFOLIO_TEST_HOOKS__;
if (hooks === undefined) throw new Error('Authenticated portfolio fault hooks were not installed.');
return hooks.snapshot();
});
export const triggerFaultAction = (page: Page, action: 'context-loss' | 'context-restore' | 'audio-interruption') => (
page.evaluate((nextAction) => {
const hooks = (window as unknown as {
__PORTFOLIO_TEST_HOOKS__?: { trigger(action: typeof nextAction): void };
}).__PORTFOLIO_TEST_HOOKS__;
if (hooks === undefined) throw new Error('Authenticated portfolio fault hooks were not installed.');
hooks.trigger(nextAction);
}, action)
);