import { expect, test } from '@playwright/test'; import { captureUnexpectedConsole, expectNoHorizontalOverflow, findAnyVisible, isMachineUrl, machineCardLinks, openCatalogue, openFirstMachineFromCatalogue, visibleFocusSnapshot, waitForAnyVisible, waitForAppShell, waitForViewerSurface, } from './helpers'; test.describe('production regression: keyboard navigation and viewer controls', () => { test.use({ viewport: { width: 1440, height: 900 }, hasTouch: false, isMobile: false, reducedMotion: 'no-preference', }); test('keeps primary viewer controls keyboard reachable and exposes shortcut help', async ({ page }) => { const consoleCapture = captureUnexpectedConsole(page); try { await openFirstMachineFromCatalogue(page); await waitForViewerSurface(page); const focusSnapshots = []; for (let index = 0; index < 18; index += 1) { await page.keyboard.press('Tab'); const snapshot = await visibleFocusSnapshot(page); if (snapshot?.visible) { focusSnapshots.push(snapshot); } if (focusSnapshots.length >= 3) { break; } } expect(focusSnapshots.length).toBeGreaterThan(0); const cameraPresetButton = await waitForAnyVisible( page, [ page.getByRole('button', { name: /isometric|front|back|left|right|top|camera|reset camera/i, }), page.locator( 'button[aria-label*="camera" i], button[title*="camera" i], button[aria-label*="isometric" i], button[title*="isometric" i]', ), ], 'camera preset or reset button', 10_000, ); await cameraPresetButton.focus(); await expect(cameraPresetButton).toBeFocused(); await page.keyboard.press('Enter'); await page.keyboard.press('Shift+/'); let shortcutHelp = await findAnyVisible( page, [ page.getByRole('dialog', { name: /keyboard|shortcut|help/i }), page.getByText(/keyboard shortcuts/i), page.getByText(/\bspace\b.*play/i), ], 'keyboard shortcuts help after ? shortcut', 1_500, ); if (!shortcutHelp) { const helpButton = await findAnyVisible( page, [ page.getByRole('button', { name: /keyboard shortcuts|shortcuts|help/i }), page.locator( 'button[aria-label*="keyboard" i], button[title*="keyboard" i], button[aria-label*="help" i], button[title*="help" i]', ), ], 'keyboard shortcuts help button', 5_000, ); if (!helpButton) { throw new Error('Keyboard shortcuts help was not reachable by shortcut or visible button.'); } await helpButton.click(); shortcutHelp = await waitForAnyVisible( page, [ page.getByRole('dialog', { name: /keyboard|shortcut|help/i }), page.getByText(/keyboard shortcuts/i), page.getByText(/\bspace\b.*play/i), ], 'keyboard shortcuts help panel', 5_000, ); } expect(shortcutHelp).not.toBeNull(); await page.keyboard.press('Escape'); await consoleCapture.expectNoErrors(); } finally { consoleCapture.dispose(); } }); }); test.describe('production regression: reduced motion', () => { test.use({ viewport: { width: 1280, height: 800 }, reducedMotion: 'reduce', hasTouch: false, isMobile: false, }); test('loads catalogue and viewer while respecting reduced-motion media preferences', async ({ page }) => { const consoleCapture = captureUnexpectedConsole(page); try { await openCatalogue(page); expect( await page.evaluate(() => window.matchMedia('(prefers-reduced-motion: reduce)').matches), ).toBe(true); const motionPreferenceControl = await findAnyVisible( page, [ page.getByRole('button', { name: /motion|animation/i }), page.getByRole('switch', { name: /motion|animation/i }), page.locator( 'button[aria-label*="motion" i], button[title*="motion" i], [role="switch"][aria-label*="motion" i]', ), ], 'optional motion preference control', 2_000, ); if (motionPreferenceControl) { await motionPreferenceControl.click().catch(() => undefined); await page.keyboard.press('Escape').catch(() => undefined); } await openFirstMachineFromCatalogue(page); await waitForViewerSurface(page); const longRunningInfiniteAnimations = await page.evaluate(() => { return document .getAnimations({ subtree: true }) .filter((animation) => { const timing = animation.effect?.getTiming(); if (!timing) { return false; } const duration = typeof timing.duration === 'number' ? timing.duration : Number(timing.duration); const iterations = typeof timing.iterations === 'number' ? timing.iterations : Number(timing.iterations); return ( animation.playState === 'running' && Number.isFinite(duration) && duration > 1_000 && iterations === Infinity ); }).length; }); expect(longRunningInfiniteAnimations).toBe(0); await consoleCapture.expectNoErrors(); } finally { consoleCapture.dispose(); } }); }); test.describe('production regression: mobile layout', () => { test.use({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 3, hasTouch: true, isMobile: true, reducedMotion: 'reduce', }); test('avoids horizontal overflow and keeps catalogue-to-viewer navigation usable on phones', async ({ page, }) => { const consoleCapture = captureUnexpectedConsole(page); try { await openCatalogue(page); await expectNoHorizontalOverflow(page, 4); const machineLink = await waitForAnyVisible( page, [machineCardLinks(page)], 'mobile machine catalogue card link', 15_000, ); await machineLink.click(); await page.waitForURL((url) => isMachineUrl(url), { timeout: 15_000 }); await waitForAppShell(page); await waitForViewerSurface(page); await expectNoHorizontalOverflow(page, 4); const mobilePanelControl = await findAnyVisible( page, [ page.getByRole('button', { name: /controls|settings|components|parts|menu|panel/i }), page.locator( 'button[aria-label*="controls" i], button[aria-label*="settings" i], button[aria-label*="components" i], button[aria-label*="menu" i]', ), ], 'optional mobile panel/menu control', 5_000, ); if (mobilePanelControl) { await mobilePanelControl.click().catch(() => undefined); await page.keyboard.press('Escape').catch(() => undefined); } await consoleCapture.expectNoErrors(); } finally { consoleCapture.dispose(); } }); });