fleeting-garden/e2e/app.spec.ts
2026-05-17 17:21:49 +01:00

135 lines
3.9 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
const canvasName = 'Interactive generative garden canvas';
const isLocalUrl = (url: string) => {
const { hostname } = new URL(url);
return hostname === '127.0.0.1' || hostname === 'localhost';
};
const collectLocalBrowserFailures = (page: Page) => {
const failures: Array<string> = [];
page.on('requestfailed', (request) => {
if (!isLocalUrl(request.url())) {
return;
}
const failure = request.failure();
failures.push(`${request.method()} ${request.url()} ${failure?.errorText}`);
});
page.on('response', (response) => {
if (response.status() < 400 || !isLocalUrl(response.url())) {
return;
}
failures.push(`${response.status()} ${response.url()}`);
});
return failures;
};
const disableWebGpu = async (page: Page) => {
await page.addInitScript(() => {
Object.defineProperty(navigator, 'gpu', {
configurable: true,
value: undefined,
});
});
};
test('starts the WebGPU garden and accepts drawing input', async ({ page }) => {
const browserFailures = collectLocalBrowserFailures(page);
const consoleErrors: Array<string> = [];
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
await page.addInitScript((expectedCanvasName) => {
const captureState = { count: 0 };
Object.defineProperty(window, '__fleetingGardenPointerCaptures', {
configurable: true,
value: captureState,
});
const originalSetPointerCapture = Element.prototype.setPointerCapture;
Element.prototype.setPointerCapture = function setPointerCapture(pointerId) {
if (
this instanceof HTMLCanvasElement &&
this.getAttribute('aria-label') === expectedCanvasName
) {
captureState.count += 1;
}
return originalSetPointerCapture.call(this, pointerId);
};
}, canvasName);
await page.goto('/');
await expect(page.locator('body')).not.toHaveClass(/is-loading/, {
timeout: 30_000,
});
await expect(page.getByRole('alert')).toHaveCount(0);
await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible();
const canvas = page.getByRole('img', { name: canvasName });
await expect(canvas).toBeVisible();
const canvasSize = await canvas.evaluate((element) => {
const canvasElement = element as HTMLCanvasElement;
return {
height: canvasElement.height,
width: canvasElement.width,
};
});
expect(canvasSize.width).toBeGreaterThan(0);
expect(canvasSize.height).toBeGreaterThan(0);
const box = await canvas.boundingBox();
expect(box).not.toBeNull();
if (!box) {
return;
}
await page.mouse.move(box.x + box.width * 0.2, box.y + box.height * 0.5);
await page.mouse.down();
await page.mouse.move(box.x + box.width * 0.8, box.y + box.height * 0.5, {
steps: 16,
});
await page.mouse.up();
await expect
.poll(() =>
page.evaluate(
() =>
(
window as unknown as {
__fleetingGardenPointerCaptures?: { count: number };
}
).__fleetingGardenPointerCaptures?.count ?? 0
)
)
.toBeGreaterThan(0);
expect(consoleErrors).toEqual([]);
expect(browserFailures).toEqual([]);
});
test('shows a clear fallback when WebGPU is unavailable', async ({ page }) => {
const browserFailures = collectLocalBrowserFailures(page);
await disableWebGpu(page);
await page.goto('/');
await expect(page).toHaveTitle('Fleeting Garden');
await expect(page.getByRole('img', { name: canvasName })).toBeVisible();
await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible();
await expect(page.locator('body')).not.toHaveClass(/is-loading/);
const fallback = page.getByRole('alert');
await expect(fallback).toContainText('Fleeting Garden needs WebGPU');
await expect(fallback).toContainText('webgpu-unsupported');
expect(browserFailures).toEqual([]);
});