From b27aaa22473ee8b0f009b2974ff8dc3ca10573c7 Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 30 Sep 2026 07:21:02 +0200 Subject: [PATCH] test(e2e): drive the real hero rotation animation The unit specs dispatch `animationend` on the dot span by hand, so a fill whose real event no longer reached the handler would still pass. The new Electron spec shortens `--hero-rotation-ms` and checks that the running `::before` fill advances the slide, that pause holds it and that Play resumes it. Co-Authored-By: Claude Opus 5.5 --- .../src/dashboard-hero-rotation.e2e.ts | 80 +++++++++++++++++++ docs/architecture/workspace-dashboard.md | 3 + 2 files changed, 83 insertions(+) create mode 100644 apps/electron-backend-e2e/src/dashboard-hero-rotation.e2e.ts diff --git a/apps/electron-backend-e2e/src/dashboard-hero-rotation.e2e.ts b/apps/electron-backend-e2e/src/dashboard-hero-rotation.e2e.ts new file mode 100644 index 000000000..11434ab03 --- /dev/null +++ b/apps/electron-backend-e2e/src/dashboard-hero-rotation.e2e.ts @@ -0,0 +1,80 @@ +import { Page } from '@playwright/test'; +import { + addXtreamPortal, + closeElectronApp, + expect, + goToDashboard, + launchElectronApp, + resetMockServers, + test, + waitForXtreamWorkspaceReady, +} from './electron-test-fixtures'; + +/** Index of the dot marked `aria-current`, i.e. the slide on screen. */ +async function activeDotIndex(page: Page): Promise { + return page + .getByTestId('dashboard-hero-dot') + .evaluateAll((dots) => + dots.findIndex((dot) => dot.getAttribute('aria-current') === 'true') + ); +} + +test.describe('Dashboard hero rotation', () => { + // The unit tests dispatch `animationend` by hand. This drives the real + // CSS fill animation, whose `animationend` comes from the fill's + // `::before` and must still reach the span's handler. + test('advances when the dot fill animation ends and holds while paused', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['xtream']); + const app = await launchElectronApp(dataDir); + const page = app.mainWindow; + + try { + // Xtream "recently added" titles alone give the hero several slides. + await addXtreamPortal(page); + await waitForXtreamWorkspaceReady(page); + await goToDashboard(page); + + const hero = page.getByTestId('dashboard-hero'); + await expect(hero).toBeVisible(); + await expect + .poll(() => page.getByTestId('dashboard-hero-dot').count()) + .toBeGreaterThanOrEqual(2); + // Keep the pointer off the hero: hovering it pauses the rotation. + await page.mouse.move(1, 1); + // A short interval so the test does not wait out 8 s per slide. + await hero.evaluate((element) => + (element as HTMLElement).style.setProperty( + '--hero-rotation-ms', + '600ms' + ) + ); + + const first = await activeDotIndex(page); + expect(first).toBeGreaterThanOrEqual(0); + await expect + .poll(() => activeDotIndex(page), { timeout: 10_000 }) + .not.toBe(first); + + const pause = page.getByTestId('dashboard-hero-pause'); + await pause.click(); + await expect(pause).toHaveAttribute('aria-pressed', 'true'); + await page.mouse.move(1, 1); + const paused = await activeDotIndex(page); + // Several intervals: a running fill would have advanced by now. + await page.waitForTimeout(2_000); + expect(await activeDotIndex(page)).toBe(paused); + + await pause.click(); + await expect(pause).toHaveAttribute('aria-pressed', 'false'); + await page.mouse.move(1, 1); + await expect + .poll(() => activeDotIndex(page), { timeout: 10_000 }) + .not.toBe(paused); + } finally { + await closeElectronApp(app); + } + }); +}); diff --git a/docs/architecture/workspace-dashboard.md b/docs/architecture/workspace-dashboard.md index c7b8c01f1..dfb498f74 100644 --- a/docs/architecture/workspace-dashboard.md +++ b/docs/architecture/workspace-dashboard.md @@ -137,6 +137,9 @@ by id, so a late live slide never moves the user off the current one. Test hooks: `dashboard-hero`, `dashboard-hero-slide` (`data-hero-kind`), `dashboard-hero-dot`, `dashboard-hero-pause`, `dashboard-hero-primary-action`, `dashboard-hero-secondary-action`. +`dashboard-hero-rotation.e2e.ts` drives the real fill animation (with a +shortened `--hero-rotation-ms`) to prove its `animationend` still advances +and that pause holds the slide. ## Rail Contract