import { expect, test, type Page } from '@playwright/test'; const disableWebGpu = async (page: Page) => { await page.addInitScript(() => { Object.defineProperty(navigator, 'gpu', { configurable: true, value: undefined, }); }); }; const getFirstSwatchColor = (page: Page) => page .locator('.color-swatch') .first() .evaluate((element) => getComputedStyle(element).backgroundColor); const getGardenBackground = (page: Page) => page.evaluate(() => document.documentElement.style.getPropertyValue('--garden-background').trim() ); 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('/'); await expect(page).toHaveTitle('Fleeting Garden'); await expect( page.getByRole('img', { name: 'Interactive generative garden canvas' }) ).toBeVisible(); await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible(); await expect(page.locator('body')).not.toHaveClass(/is-loading/); await expect(page.getByRole('alert')).toContainText('Fleeting Garden needs WebGPU'); 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 }) => { await disableWebGpu(page); await page.goto('/'); await expect(page.locator('body')).not.toHaveClass(/is-loading/); const aboutButton = page.getByRole('button', { name: 'About' }); const aboutPanel = page.locator('#info-panel'); await expect(aboutButton).toHaveAttribute('aria-expanded', 'false'); await aboutButton.click(); await expect(aboutButton).toHaveAttribute('aria-expanded', 'true'); await expect(aboutPanel).toHaveAttribute('aria-hidden', 'false'); await expect(aboutPanel).not.toHaveAttribute('inert', ''); await expect(page.getByRole('heading', { name: 'Fleeting Garden' })).toBeVisible(); await page.keyboard.press('Escape'); await expect(aboutButton).toHaveAttribute('aria-expanded', 'false'); await expect(aboutPanel).toHaveAttribute('aria-hidden', 'true'); await expect(aboutPanel).toHaveAttribute('inert', ''); 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(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'); await soundButton.click(); await expect(soundButton).toHaveAttribute('aria-pressed', 'true'); await expect(soundButton).toHaveAttribute('aria-label', 'Unmute audio'); await page.reload(); await expect(page.locator('body')).not.toHaveClass(/is-loading/); await expect(page.locator('button.sound')).toHaveAttribute('aria-pressed', 'true'); const initialSwatchColor = await getFirstSwatchColor(page); const initialBackground = await getGardenBackground(page); await page.getByRole('button', { name: 'Next vibe' }).click(); await expect.poll(() => getFirstSwatchColor(page)).not.toBe(initialSwatchColor); await expect.poll(() => getGardenBackground(page)).not.toBe(initialBackground); await page.getByRole('button', { name: 'Draw colour 2' }).click(); await expect(page.locator('.color-swatch').nth(1)).toHaveClass(/active/); await expect(page.locator('.color-swatch').first()).not.toHaveClass(/active/); const mirrorSlider = page.locator('.mirror-segment-slider'); await mirrorSlider.evaluate((input) => { const slider = input as HTMLInputElement; slider.value = '3'; slider.dispatchEvent(new Event('input', { bubbles: true })); }); await expect(page.locator('.mirror-segment-control')).toHaveAttribute( 'title', '3 thirds' ); await expect(page.locator('.mirror-segment-control')).toHaveClass(/active/); }); 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/); 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); }); test('hides the bottom dock after the cursor leaves fullscreen controls', async ({ page, }) => { await disableWebGpu(page); await page.goto('/'); await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible(); await page.getByRole('button', { name: 'Enter fullscreen' }).click(); await expect .poll(() => page.evaluate(() => Boolean(document.fullscreenElement))) .toBe(true); await page.mouse.move(640, 120); await page.evaluate(() => { if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } }); await expect(page.locator('aside.control-dock')).toHaveClass(/menu-hidden/, { timeout: 6000, }); await expect(page.locator('.garden-controls')).not.toBeVisible(); await expect .poll(() => page .locator('aside.control-dock') .evaluate((dock) => dock.getBoundingClientRect().top >= window.innerHeight) ) .toBe(true); await page.mouse.move(640, 700); await expect(page.locator('aside.control-dock')).not.toHaveClass(/menu-hidden/); await expect(page.locator('.garden-controls')).toBeVisible(); await expect .poll(() => page .locator('aside.control-dock') .evaluate((dock) => dock.getBoundingClientRect().bottom <= window.innerHeight) ) .toBe(true); }); test('keeps the bottom dock visible in mobile fullscreen', async ({ page }) => { await page.setViewportSize({ height: 844, width: 390 }); await disableWebGpu(page); await page.goto('/'); await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible(); await page.getByRole('button', { name: 'Enter fullscreen' }).click(); await expect .poll(() => page.evaluate(() => Boolean(document.fullscreenElement))) .toBe(true); await page.mouse.move(195, 120); await page.evaluate(() => { if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } }); await page.waitForTimeout(5200); await expect(page.locator('aside.control-dock')).not.toHaveClass(/menu-hidden/); await expect(page.getByRole('button', { name: 'About' })).toBeVisible(); });