import { test, expect, type Page } from '@playwright/test'; async function login(page: Page) { await page.goto('/login/'); await page.fill('#lg-username', 'demo'); await page.fill('#lg-password', 'demo'); await page.click('button[type=submit]'); await expect(page.getByTestId('nav-menu')).toBeVisible(); } test('the timeline activates after a run and scrubs the flight', async ({ page }) => { await login(page); // No result yet → the scrubber is inert. const range = page.getByTestId('tl-range'); await expect(range).toBeDisabled(); await page.getByTestId('run-btn').click(); await expect(page.getByTestId('flight-time')).toBeVisible({ timeout: 15000 }); // A result sets the global domain, enabling playback. await expect(range).toBeEnabled(); await expect(page.getByTestId('tl-elapsed')).toHaveText('00:00:00'); // Seeking moves the clock (and with it every visible scenario's cursor). await range.fill('3600000'); // one hour into the flight await expect(page.getByTestId('tl-elapsed')).toHaveText('01:00:00'); // Play flips the control to pause, then pausing restores it. await page.getByTestId('tl-play').click(); await expect(page.getByTestId('tl-pause')).toBeVisible(); await page.getByTestId('tl-pause').click(); await expect(page.getByTestId('tl-play')).toBeVisible(); }); /** * Regression: durations are not whole seconds, so a stepped slider stopped just * short of max and the cursor stayed in its "in flight" state forever. */ test('scrubbing to the very end lands the cursor', async ({ page }) => { await login(page); // A rate that yields a duration which is NOT a whole number of seconds — // with a 1s-stepped slider the thumb stops short of max and never lands. await page.fill('#cp-ascent', '5.3'); await page.getByTestId('run-btn').click(); await expect(page.getByTestId('flight-time')).toBeVisible({ timeout: 15000 }); const cursorLayers = () => page.evaluate(() => { // `_sfMap` is the raw MapLibre instance exposed by in dev builds. const map = (window as unknown as { _sfMap: { getStyle(): { layers: { id: string }[] } } }) ._sfMap; return map .getStyle() .layers.map((l) => l.id) .filter((id) => id.startsWith('cursor/')) .map((id) => id.split('__')[1]); }); const range = page.getByTestId('tl-range'); // The scenario card updates before the renderer publishes the clock domain, // so wait for the scrubber to go live or `max` still reads 0. await expect(range).toBeEnabled(); const max = await range.getAttribute('max'); expect(Number(max)).toBeGreaterThan(0); // Rendering is effect-driven, so poll rather than asserting once. // Mid-flight: the animated (ring + core) marker. await range.fill(String(Number(max) / 2)); await expect.poll(cursorLayers).toEqual(['marker-ring', 'marker-core']); // At the end: swapped for the static landed marker. await range.fill(String(max)); await expect.poll(cursorLayers).toEqual(['marker-core']); });