fleeting-garden/e2e/app.spec.ts

217 lines
8.1 KiB
TypeScript

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<string> = [];
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();
});