diff --git a/assets/fonts/open-sans-v34-latin-regular.woff b/assets/fonts/open-sans-v34-latin-regular.woff deleted file mode 100644 index b083626..0000000 Binary files a/assets/fonts/open-sans-v34-latin-regular.woff and /dev/null differ diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts index f0381bf..c641649 100644 --- a/e2e/app.spec.ts +++ b/e2e/app.spec.ts @@ -1,7 +1,5 @@ import { expect, test, type Page } from '@playwright/test'; -type WebGpuFailureMode = 'adapter-null' | 'adapter-rejects' | 'device-rejects'; - const disableWebGpu = async (page: Page) => { await page.addInitScript(() => { Object.defineProperty(navigator, 'gpu', { @@ -11,54 +9,6 @@ const disableWebGpu = async (page: Page) => { }); }; -const emulateWebGpuFailure = async (page: Page, mode: WebGpuFailureMode) => { - await page.addInitScript((failureMode) => { - const limits = { - maxBufferSize: 256 * 1024 * 1024, - maxComputeWorkgroupsPerDimension: 65_535, - maxStorageBufferBindingSize: 128 * 1024 * 1024, - }; - const adapter = { - features: new Set(), - info: { - architecture: 'test', - description: 'Playwright fake adapter', - device: 'test-device', - isFallbackAdapter: false, - subgroupMaxSize: 0, - subgroupMinSize: 0, - vendor: 'test-vendor', - }, - limits, - requestDevice: async () => { - if (failureMode === 'device-rejects') { - throw new Error('Playwright fake device failure'); - } - - return {}; - }, - }; - - Object.defineProperty(navigator, 'gpu', { - configurable: true, - value: { - getPreferredCanvasFormat: () => 'rgba8unorm', - requestAdapter: async () => { - if (failureMode === 'adapter-null') { - return null; - } - - if (failureMode === 'adapter-rejects') { - throw new Error('Playwright fake adapter failure'); - } - - return adapter; - }, - }, - }); - }, mode); -}; - const getFirstSwatchColor = (page: Page) => page .locator('.color-swatch') @@ -71,6 +21,18 @@ const getGardenBackground = (page: Page) => ); test('loads the app shell and WebGPU fallback in Chromium', async ({ page }) => { + const browserFailures: Array = []; + + page.on('requestfailed', (request) => { + const failure = request.failure(); + browserFailures.push(`${request.method()} ${request.url()} ${failure?.errorText}`); + }); + page.on('response', (response) => { + if (response.status() >= 400) { + browserFailures.push(`${response.status()} ${response.url()}`); + } + }); + await disableWebGpu(page); await page.goto('/'); @@ -85,6 +47,7 @@ test('loads the app shell and WebGPU fallback in Chromium', async ({ page }) => await page.getByRole('button', { name: 'About' }).click(); await expect(page.getByRole('heading', { name: 'Fleeting Garden' })).toBeVisible(); + expect(browserFailures).toEqual([]); }); test('keeps fallback controls interactive and accessible', async ({ page }) => { @@ -106,13 +69,21 @@ test('keeps fallback controls interactive and accessible', async ({ page }) => { await expect(aboutPanel).toHaveAttribute('aria-hidden', 'true'); await expect(aboutPanel).toHaveAttribute('inert', ''); - const settingsButton = page.getByRole('button', { name: 'Show config overlay' }); + const settingsButton = page.locator('button.settings'); + await expect(settingsButton).toHaveAttribute('aria-label', 'Show config overlay'); + await expect(settingsButton).toHaveAttribute('aria-expanded', 'false'); await settingsButton.click(); - await expect(page.getByRole('button', { name: 'Hide config overlay' })).toHaveAttribute( - 'aria-expanded', - 'true' - ); + await expect(settingsButton).toHaveAttribute('aria-expanded', 'true'); + await expect(settingsButton).toHaveAttribute('aria-label', 'Hide config overlay'); await expect(page.locator('.config-pane')).toBeVisible(); + await expect(page.locator('.config-pane')).toContainText('Runtime'); + await expect(page.locator('.color-reaction-matrix')).toBeVisible(); + + const colorReaction = page.getByLabel('Color 1 agents reacting to color 2'); + await colorReaction.selectOption('-1'); + await expect(colorReaction).toHaveValue('-1'); + await settingsButton.click(); + await expect(settingsButton).toHaveAttribute('aria-expanded', 'false'); const soundButton = page.locator('button.sound'); await expect(soundButton).toHaveAttribute('aria-pressed', 'false'); @@ -146,98 +117,35 @@ test('keeps fallback controls interactive and accessible', async ({ page }) => { await expect(page.locator('.mirror-segment-control')).toHaveClass(/active/); }); -( - [ - { - expectedCode: 'webgpu-adapter-unavailable', - expectedMessage: - 'WebGPU is available, but this browser could not provide a compatible GPU adapter.', - mode: 'adapter-null', - }, - { - expectedCode: 'webgpu-adapter-unavailable', - expectedMessage: 'Could not request a WebGPU adapter.', - mode: 'adapter-rejects', - }, - { - expectedCode: 'webgpu-device-unavailable', - expectedMessage: 'Could not create a WebGPU device for this adapter.', - mode: 'device-rejects', - }, - ] satisfies Array<{ - expectedCode: string; - expectedMessage: string; - mode: WebGpuFailureMode; - }> -).forEach(({ expectedCode, expectedMessage, mode }) => { - test(`reports ${mode} startup failures without leaving the shell loading`, async ({ - page, - }) => { - await emulateWebGpuFailure(page, mode); - - await page.goto('/'); - - await expect(page.locator('body')).not.toHaveClass(/is-loading/); - await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible(); - await expect(page.getByRole('alert')).toContainText(expectedMessage); - await expect(page.getByRole('alert')).toContainText(expectedCode); - }); -}); - -test('serves the production bundle without missing browser assets', async ({ page }) => { - const browserFailures: Array = []; - - page.on('requestfailed', (request) => { - const failure = request.failure(); - browserFailures.push(`${request.method()} ${request.url()} ${failure?.errorText}`); - }); - page.on('response', (response) => { - if (response.status() >= 400) { - browserFailures.push(`${response.status()} ${response.url()}`); - } - }); - +test('keeps the fallback shell usable on mobile', async ({ page }) => { + await page.setViewportSize({ height: 844, width: 390 }); await disableWebGpu(page); + await page.goto('/'); await expect(page.locator('body')).not.toHaveClass(/is-loading/); - expect(browserFailures).toEqual([]); -}); + const canvasBox = await page + .getByRole('img', { name: 'Interactive generative garden canvas' }) + .boundingBox(); + expect(canvasBox?.width).toBeGreaterThan(0); + expect(canvasBox?.height).toBeGreaterThan(0); + await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'About' })).toBeVisible(); + await expect(page.getByRole('alert')).toContainText('Fleeting Garden needs WebGPU'); -[ - { height: 720, name: 'desktop', width: 1280 }, - { height: 844, name: 'mobile', width: 390 }, -].forEach(({ height, name, width }) => { - test(`keeps the fallback shell usable on ${name}`, async ({ page }) => { - await page.setViewportSize({ height, width }); - await disableWebGpu(page); + const aboutButtonReceivesPointer = await page + .getByRole('button', { name: 'About' }) + .evaluate((button) => { + const rect = button.getBoundingClientRect(); + const target = document.elementFromPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2 + ); - await page.goto('/'); - await expect(page.locator('body')).not.toHaveClass(/is-loading/); + return button === target || button.contains(target); + }); - const canvasBox = await page - .getByRole('img', { name: 'Interactive generative garden canvas' }) - .boundingBox(); - expect(canvasBox?.width).toBeGreaterThan(0); - expect(canvasBox?.height).toBeGreaterThan(0); - await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'About' })).toBeVisible(); - await expect(page.getByRole('alert')).toContainText('Fleeting Garden needs WebGPU'); - - const aboutButtonReceivesPointer = await page - .getByRole('button', { name: 'About' }) - .evaluate((button) => { - const rect = button.getBoundingClientRect(); - const target = document.elementFromPoint( - rect.left + rect.width / 2, - rect.top + rect.height / 2 - ); - - return button === target || button.contains(target); - }); - - expect(aboutButtonReceivesPointer).toBe(true); - }); + expect(aboutButtonReceivesPointer).toBe(true); }); test('hides the bottom dock after the cursor leaves fullscreen controls', async ({ @@ -305,5 +213,5 @@ test('keeps the bottom dock visible in mobile fullscreen', async ({ page }) => { await page.waitForTimeout(5200); await expect(page.locator('aside.control-dock')).not.toHaveClass(/menu-hidden/); - await expect(page.getByRole('button', { name: 'Show config overlay' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'About' })).toBeVisible(); }); diff --git a/index.html b/index.html index 1957196..0a66ab3 100644 --- a/index.html +++ b/index.html @@ -22,10 +22,9 @@ - - - - + + + Fleeting Garden @@ -41,13 +40,13 @@

Fleeting Garden is a pointer-driven WebGPU drawing canvas. Drag or touch the scene - to paint coloured paths, then use the toolbar to change colours, erase, adjust the - config overlay, export, restart, or open more information. + to paint coloured paths, then use the toolbar to change colours, erase, export, + adjust the config overlay, restart, or open more information.

-
+
+ >
@@ -72,7 +69,15 @@