feat: introduce the compact vehicle HUD
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import { expect, expectInsetFocusIndicator, test } from '../helpers/serve-dist';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
@@ -30,15 +30,17 @@ test('starts Web Audio synchronously from the visitor gesture and exposes access
|
||||
});
|
||||
});
|
||||
|
||||
await expect(page.getByRole('button', { name: 'START THE DRIVE' })).toBeVisible();
|
||||
await expect(page.getByText('Enables the interactive soundtrack.')).toBeVisible();
|
||||
await expect(page.getByLabel('Sound volume')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Start the drive' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Mute sound' })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Sound volume' })).toHaveCount(0);
|
||||
await expect.poll(() => page.evaluate(() => (window as unknown as { __audioConstructed: number }).__audioConstructed)).toBe(0);
|
||||
|
||||
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
|
||||
await page.getByRole('button', { name: 'Start the drive' }).click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Mute sound' })).toBeVisible();
|
||||
await expect(page.getByLabel('Sound volume')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sound volume' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Pause motion' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Drive settings' })).toBeVisible();
|
||||
await expect.poll(() => page.evaluate(() => (window as unknown as { __constructedDuringGesture: number }).__constructedDuringGesture)).toBe(1);
|
||||
await expect.poll(() => page.evaluate(() => (window as unknown as { __audioConstructed: number }).__audioConstructed)).toBe(1);
|
||||
|
||||
@@ -46,24 +48,60 @@ test('starts Web Audio synchronously from the visitor gesture and exposes access
|
||||
await expect(page.getByRole('button', { name: 'Unmute sound' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('keeps the compact controls usable and dismisses the upward panels', async ({ page, distUrl }) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: 'Start the drive' }).click();
|
||||
|
||||
const controls = page.getByRole('group', { name: 'Drive controls' }).getByRole('button');
|
||||
const targets = await controls.evaluateAll((buttons) => buttons.map((button) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
return { width: rect.width, height: rect.height };
|
||||
}));
|
||||
expect(targets.every((target) => target.width >= 44 && target.height >= 44)).toBe(true);
|
||||
|
||||
const settings = page.getByRole('button', { name: 'Drive settings' });
|
||||
await settings.focus();
|
||||
await expectInsetFocusIndicator(page, 'button[aria-label="Drive settings"]');
|
||||
await settings.click();
|
||||
await expect(page.getByRole('dialog', { name: 'Drive settings' })).toBeVisible();
|
||||
const surfaces = await page.locator('.journey-hud, .drive-controls__dock, .drive-popover').evaluateAll((elements) =>
|
||||
elements.map((element) => {
|
||||
const style = getComputedStyle(element);
|
||||
const alpha = Number.parseFloat(style.backgroundColor.match(/[\d.]+(?=\))/u)?.[0] ?? '1');
|
||||
return { alpha, backdropFilter: style.backdropFilter };
|
||||
}));
|
||||
expect(surfaces.every((surface) => surface.alpha >= 0.95 && surface.backdropFilter === 'none')).toBe(true);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.getByRole('dialog', { name: 'Drive settings' })).toHaveCount(0);
|
||||
await expect(settings).toBeFocused();
|
||||
|
||||
await settings.click();
|
||||
await page.locator('main#portfolio-content').click({ position: { x: 8, y: 8 } });
|
||||
await expect(page.getByRole('dialog', { name: 'Drive settings' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('persists native volume and keeps sound independent from Static motion', async ({ page, distUrl }) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
|
||||
await page.getByRole('button', { name: 'Start the drive' }).click();
|
||||
|
||||
const volume = page.getByLabel('Sound volume');
|
||||
await page.getByRole('button', { name: 'Sound volume' }).click();
|
||||
const volume = page.getByLabel('Sound volume level');
|
||||
await volume.fill('0.65');
|
||||
await expect(volume).toHaveValue('0.65');
|
||||
await expect.poll(() => page.evaluate(() => localStorage.getItem('portfolio.audio.volume'))).toBe('0.65');
|
||||
|
||||
await page.getByRole('button', { name: 'Drive settings' }).click();
|
||||
await page.getByLabel('Quality control').selectOption('static');
|
||||
await expect(page.getByRole('button', { name: 'Unmute sound' })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Mute sound' })).toBeVisible();
|
||||
await expect(volume).toHaveCount(0);
|
||||
await page.getByRole('button', { name: 'Sound volume' }).click();
|
||||
await expect(volume).toBeVisible();
|
||||
});
|
||||
|
||||
test('requires a direct visitor action after browser interruption without creating another graph', async ({ page, distUrl }) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
|
||||
await page.getByRole('button', { name: 'Start the drive' }).click();
|
||||
|
||||
await page.evaluate(() => {
|
||||
const context = (window as unknown as { __audioContexts: AudioContext[] }).__audioContexts[0];
|
||||
@@ -88,7 +126,7 @@ test('shows a concise non-blocking message when initial audio start is rejected'
|
||||
});
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
|
||||
await page.getByRole('button', { name: 'Start the drive' }).click();
|
||||
|
||||
await expect(page.getByText('Audio unavailable. Continue without sound.')).toBeVisible();
|
||||
await expect(page.locator('main#portfolio-content')).toBeVisible();
|
||||
|
||||
@@ -8,6 +8,7 @@ const stylesheetFiles = [
|
||||
'src/styles/base.css',
|
||||
'src/styles/layout.css',
|
||||
'src/styles/components.css',
|
||||
'src/styles/world.css',
|
||||
'src/styles/print.css',
|
||||
];
|
||||
|
||||
@@ -151,6 +152,51 @@ test('uses a stable tablet rhythm and a single readable mobile column with usabl
|
||||
await capture(page, testInfo, 'information-layer-mobile.png');
|
||||
});
|
||||
|
||||
test('keeps the compact HUD in one safe lower row without covering the road corridor', async ({ page }) => {
|
||||
for (const viewport of [
|
||||
{ width: 320, height: 800 },
|
||||
{ width: 1440, height: 900 },
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
await loadInformationLayer(page);
|
||||
const geometry = await page.evaluate(() => {
|
||||
const controls = document.querySelector<HTMLElement>('.drive-controls');
|
||||
const hud = document.querySelector<HTMLElement>('.journey-hud');
|
||||
if (!controls || !hud) throw new Error('Missing vehicle HUD');
|
||||
const controlRect = controls.getBoundingClientRect();
|
||||
const hudRect = hud.getBoundingClientRect();
|
||||
return {
|
||||
aligned: Math.abs(controlRect.bottom - hudRect.bottom) <= 1,
|
||||
separate: controlRect.right <= hudRect.left,
|
||||
controlsTargetSize: [...controls.querySelectorAll<HTMLElement>('button')]
|
||||
.every((button) => button.getBoundingClientRect().height >= 44),
|
||||
centerClear: controlRect.right < window.innerWidth * 0.42 && hudRect.left > window.innerWidth * 0.58,
|
||||
};
|
||||
});
|
||||
expect(geometry.aligned).toBe(true);
|
||||
expect(geometry.separate).toBe(true);
|
||||
expect(geometry.controlsTargetSize).toBe(true);
|
||||
if (viewport.width >= 768) expect(geometry.centerClear).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('moves drive controls into document flow in short landscape', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 800, height: 390 });
|
||||
await loadInformationLayer(page);
|
||||
const geometry = await page.evaluate(() => {
|
||||
const controls = document.querySelector<HTMLElement>('.drive-controls');
|
||||
const contact = document.querySelector<HTMLElement>('#contact');
|
||||
if (!controls || !contact) throw new Error('Missing short-landscape controls');
|
||||
const controlsRect = controls.getBoundingClientRect();
|
||||
const contactRect = contact.getBoundingClientRect();
|
||||
return {
|
||||
position: getComputedStyle(controls).position,
|
||||
afterContact: controlsRect.top >= contactRect.bottom,
|
||||
};
|
||||
});
|
||||
expect(geometry).toEqual({ position: 'relative', afterContact: true });
|
||||
});
|
||||
|
||||
test('keeps actual standalone project and contact destinations at least 44 by 44px on mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 320, height: 800 });
|
||||
await loadInformationLayer(page);
|
||||
|
||||
@@ -81,10 +81,13 @@ test('keeps a scrolled content panel in a geometry-safe paint layer above the wo
|
||||
test('provides native motion and quality controls with visible state', async ({ page, distUrl }) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.getByRole('button', { name: 'Drive settings' }).click();
|
||||
const quality = page.getByLabel('Quality control');
|
||||
await expect(quality).toBeVisible();
|
||||
await expect(page.getByRole('status')).toHaveCount(0);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('button', { name: 'Start the drive' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Pause motion' })).toBeVisible();
|
||||
await expect(page.getByRole('status')).toContainText('Motion:');
|
||||
const canvas = page.locator('.journey-canvas');
|
||||
const originalCanvas = await canvas.elementHandle();
|
||||
const originalGeneration = await page.locator('.journey-world').getAttribute('data-scene-generation');
|
||||
@@ -101,7 +104,8 @@ test('provides native motion and quality controls with visible state', async ({
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-generation', originalGeneration ?? '');
|
||||
expect(await originalCanvas?.evaluate((element) => element.isConnected)).toBe(true);
|
||||
|
||||
await quality.selectOption('static');
|
||||
await page.getByRole('button', { name: 'Drive settings' }).click();
|
||||
await page.getByLabel('Quality control').selectOption('static');
|
||||
await expect(page.getByRole('button', { name: 'Enable motion' })).toBeVisible();
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await expect(page.getByRole('status')).toContainText('Motion: static');
|
||||
@@ -112,6 +116,7 @@ test('never creates a canvas when the system requests reduced motion', async ({
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await page.getByRole('button', { name: 'Drive settings' }).click();
|
||||
await expect(page.getByRole('status')).toContainText('reduced motion requested');
|
||||
await expect(page.getByLabel('Quality control')).toBeVisible();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user