fix(#73): consume canonical Weekend Context contract and repair navigation

Address the independent review blockers on PR #80 after rebasing onto the
authoritative #72 Weekend Context API.

- Replace the invented frontend WeekendContext with the exact backend contract
  (temporal_state, previous/focus/next meetings, previous_completed/active/next/
  default_analysis sessions with availability). Every valid canonical payload now
  maps to a designed state via a total resolveViewState; a well-formed response
  can never fall through to the limited-data placeholder.
- Make the canonical read the single source of truth: useWeekendContext no longer
  fans out to season/meetings/per-weekend/OpenF1/live queries. Only supplementary
  championship + news reads run, and only once the canonical context resolves.
- Fix the Prepare/analysis flow: /preview is a stable alias that renders the
  preparation surface (PreSessionView) instead of redirecting back to the same
  between-races screen.
- One primary navigation system per breakpoint: the mobile top-bar links are
  hidden so the bottom bar is the sole primary nav, and Admin is moved out of
  every Primary landmark into an operator-utilities toolbar.
- Add Vitest coverage for the contract mapping, every temporal state, loading/
  error/limited surfaces, the no-fanout guarantee, the /preview CTA, and the nav
  hierarchy; add hermetic Playwright journeys (seeded + injected canonical
  payloads), 390/768/1440 overflow checks, and Weekend visual snapshots. Retire
  the stale Command Center specs/snapshots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 18:38:08 -04:00
parent 06223d992f
commit 07e5857760
30 changed files with 1468 additions and 811 deletions

View File

@@ -1,77 +0,0 @@
import { test, expect } from '@playwright/test'
const FULL_SESSION = 9472
test.describe('Command Center', () => {
test('loads as default route with weekend identity band', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('command-center')).toBeVisible()
await expect(page.getByTestId('cc-focus')).toBeVisible()
await expect(page.getByTestId('cc-session-9472')).toBeVisible()
await expect(page.getByTestId('hero-last-race-link')).toBeVisible()
})
test('nav link reaches command center from race hub', async ({ page }) => {
await page.goto(`/race-hub?session_key=${FULL_SESSION}`)
await page.getByRole('link', { name: 'Command' }).click()
await expect(page).toHaveURL('/')
await expect(page.getByTestId('command-center')).toBeVisible()
})
test('open analysis action opens race hub for the focus session', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('hero-last-race-link')).toBeVisible()
await page.getByTestId('hero-last-race-link').click()
await expect(page).toHaveURL(new RegExp(`/race-hub\\?session_key=${FULL_SESSION}`))
await expect(page.getByTestId('rh-identity')).toBeVisible()
await expect(page.getByTestId(`rh-session-${FULL_SESSION}`)).toBeVisible()
})
test('archived live snapshot does not mark command center live', async ({ page }) => {
await page.route('**/api/v1/live/state', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
is_live: false,
data: null,
last_snapshot: {
Drivers: { '1': { RacingNumber: '1', Position: 1 } },
DriverInfo: { '1': { RacingNumber: '1', Tla: 'VER', TeamColour: '3671C6' } },
Tyres: {},
RCMessages: [],
Weather: {},
Session: { MeetingName: 'Archived GP', SessionName: 'Race', SessionType: 'Race' },
TeamRadio: [],
SessionStatus: 'Finished',
TrackStatus: '1',
CurrentLap: 57,
TotalLaps: 57,
Clock: '',
ClockRefTime: '',
ClockExtrapolating: false,
Stints: {},
},
last_snapshot_at: '2026-07-04T14:00:00Z',
}),
}),
)
await page.goto('/')
await expect(page.getByTestId('command-center')).toBeVisible()
await expect(page.getByTestId('cc-live-status')).toContainText('No live session')
await expect(page.getByTestId('cc-live-status')).not.toContainText('Live session active')
})
test('existing routes continue to work', async ({ page }) => {
await page.goto(`/race-hub?session_key=${FULL_SESSION}`)
await expect(page.getByTestId('race-hub')).toBeVisible()
await expect(page.getByTestId('rh-overview')).toBeVisible()
await page.goto('/admin')
await expect(page.getByTestId('data-library')).toBeVisible()
await page.goto('/live')
await expect(page.getByTestId('live-empty')).toBeVisible()
})
})

View File

@@ -3,11 +3,11 @@ import { test, expect } from '@playwright/test'
const FULL_SESSION = 9472
test.describe('Production serving (Go + built React)', () => {
test('serves command center as default route', async ({ page }) => {
test('serves the Weekend home as default route', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('command-center')).toBeVisible()
await expect(page.getByTestId('cc-session-9472')).toBeVisible()
await expect(page.getByTestId('weekend-page')).toBeVisible()
await expect(page.getByTestId('weekend-between-races')).toBeVisible()
})
test('serves race hub workspace from built assets', async ({ page }) => {
@@ -44,16 +44,21 @@ test.describe('Production serving (Go + built React)', () => {
await expect(page.getByText('No live session active')).toBeVisible()
})
test('nav links work from built SPA', async ({ page }) => {
test('primary nav links and Admin utility work from built SPA', async ({ page }) => {
await page.goto('/')
await page.locator('.app-nav').getByRole('link', { name: 'Race Hub' }).click()
await expect(page).toHaveURL(/\/race-hub/)
const primary = page.getByRole('navigation', { name: 'Primary' }).first()
await page.locator('.app-nav').getByRole('link', { name: 'Admin', exact: true }).click()
await primary.getByRole('link', { name: 'Championship', exact: true }).click()
await expect(page).toHaveURL(/\/championship/)
await primary.getByRole('link', { name: 'Explore', exact: true }).click()
await expect(page).toHaveURL(/\/explore/)
await primary.getByRole('link', { name: 'Weekend', exact: true }).click()
await expect(page).toHaveURL('/')
// Admin is an operator utility outside the Primary landmark.
await page.getByRole('toolbar', { name: 'Operator utilities' }).getByRole('link', { name: 'Admin' }).click()
await expect(page).toHaveURL(/\/admin/)
await page.getByRole('link', { name: 'Live', exact: true }).click()
await expect(page).toHaveURL(/\/live/)
await expect(page.getByTestId('live-empty')).toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -14,16 +14,13 @@ export async function waitForScreenshotReady(page: Page): Promise<void> {
await page.waitForTimeout(150)
}
export async function gotoCommandCenterReady(page: Page): Promise<void> {
export async function gotoWeekendReady(page: Page): Promise<void> {
await page.goto('/')
await expect(page.getByTestId('command-center')).toBeVisible()
await expect(page.getByTestId('cc-focus')).toBeVisible()
await expect(page.getByTestId('cc-session-9472')).toBeVisible()
// The e2e stack runs with an unreachable OpenF1 base URL, so wait for the
// season-calendar query to settle on its local fallback before screenshotting.
await expect(
page.getByText('Using local meetings because the full calendar could not load.'),
).toBeVisible()
await expect(page.getByTestId('weekend-page')).toBeVisible()
// The seeded hermetic DB (Monaco 2025, completed) resolves to season_complete,
// rendered by the between-races surface.
await expect(page.getByTestId('weekend-between-races')).toBeVisible()
await expect(page.getByTestId('wk-last-event')).toBeVisible()
await waitForScreenshotReady(page)
}

View File

@@ -1,6 +1,6 @@
import { test } from '@playwright/test'
import {
gotoCommandCenterReady,
gotoWeekendReady,
gotoDataLibraryReady,
gotoLiveEmptyReady,
gotoRaceHubReady,
@@ -8,9 +8,9 @@ import {
} from './helpers'
test.describe('MVP visual regression', () => {
test('command-center', async ({ page }) => {
await gotoCommandCenterReady(page)
await screenshotPage(page, 'command-center')
test('weekend', async ({ page }) => {
await gotoWeekendReady(page)
await screenshotPage(page, 'weekend')
})
test('race-hub', async ({ page }) => {

View File

@@ -0,0 +1,156 @@
import { test, expect, type Page } from '@playwright/test'
// These journeys inject the canonical /api/v1/weekend-context payload directly so
// each temporal state is exercised deterministically and hermetically, regardless
// of the seeded database clock. The payloads mirror internal/query.WeekendContext.
const availability = {
schedule: 'available',
live_transport: 'unknown',
live_session: 'inactive',
archive: 'unavailable',
local_analysis: 'complete',
freshness: 'fresh',
limitations: [],
}
function meeting(key: number, name: string, start: string) {
return {
meeting_key: key,
meeting_name: name,
meeting_official_name: name,
location: name,
country_code: 'GBR',
country_name: 'United Kingdom',
country_flag: '',
circuit_key: key,
circuit_short_name: name,
date_start: start,
date_end: start,
year: 2026,
}
}
function ctxSession(key: number, name: string, start: string, meetingKey: number) {
return {
session: {
session_key: key,
session_name: name,
session_type: name,
meeting_key: meetingKey,
date_start: start,
date_end: start,
gmt_offset: '',
},
meeting: meeting(meetingKey, 'British Grand Prix', start),
availability,
}
}
async function stubContext(page: Page, body: Record<string, unknown>): Promise<void> {
await page.route('**/api/v1/weekend-context', (route) =>
route.fulfill({ contentType: 'application/json', body: JSON.stringify(body) }),
)
// Keep supplementary reads hermetic and empty.
await page.route('**/api/v1/news**', (route) =>
route.fulfill({ contentType: 'application/json', body: '[]' }),
)
}
test.describe('Weekend canonical temporal states (injected)', () => {
test('between_weekends pairs the last completed event with the next-event countdown and Prepare CTA', async ({ page }) => {
await stubContext(page, {
season: 2026,
temporal_state: 'between_weekends',
previous_meeting: meeting(1, 'British Grand Prix', '2026-07-05T14:00:00Z'),
previous_completed_session: ctxSession(11, 'Race', '2026-07-05T14:00:00Z', 1),
default_analysis_session: ctxSession(11, 'Race', '2026-07-05T14:00:00Z', 1),
next_meeting: meeting(2, 'Hungarian Grand Prix', '2026-07-24T09:00:00Z'),
next_session: ctxSession(21, 'Practice 1', '2026-07-24T09:00:00Z', 2),
focus_meeting: meeting(2, 'Hungarian Grand Prix', '2026-07-24T09:00:00Z'),
championship_round: 12,
total_championship_rounds: 24,
})
await page.goto('/')
await expect(page.getByTestId('weekend-between-races')).toBeVisible()
await expect(page.getByTestId('wk-last-event')).toBeVisible()
await expect(page.getByTestId('wk-next-event')).toBeVisible()
await expect(page.getByTestId('wk-prepare')).toHaveAttribute('href', '/preview')
await expect(page.getByTestId('wk-season-nav')).toContainText('Round 12 of 24')
})
test('between_races Prepare CTA folds into the preparation surface instead of looping', async ({ page }) => {
await stubContext(page, {
season: 2026,
temporal_state: 'between_weekends',
next_meeting: meeting(2, 'Hungarian Grand Prix', '2026-07-24T09:00:00Z'),
next_session: ctxSession(21, 'Practice 1', '2026-07-24T09:00:00Z', 2),
championship_round: 12,
total_championship_rounds: 24,
})
await page.goto('/')
await page.getByTestId('wk-prepare').click()
await expect(page).toHaveURL(/\/preview$/)
await expect(page.getByTestId('weekend-pre-session')).toBeVisible()
await expect(page.getByTestId('weekend-between-races')).toHaveCount(0)
})
test('pre_session surfaces the preview and the next-session countdown', async ({ page }) => {
await stubContext(page, {
season: 2026,
temporal_state: 'pre_session',
next_meeting: meeting(2, 'Hungarian Grand Prix', '2026-07-24T09:00:00Z'),
next_session: ctxSession(21, 'Practice 1', '2026-07-24T09:00:00Z', 2),
focus_meeting: meeting(2, 'Hungarian Grand Prix', '2026-07-24T09:00:00Z'),
championship_round: 12,
total_championship_rounds: 24,
})
await page.goto('/')
await expect(page.getByTestId('weekend-pre-session')).toBeVisible()
await expect(page.getByTestId('wk-pre-head')).toBeVisible()
})
test('between_sessions shows the last result recap and the next-session countdown together', async ({ page }) => {
await stubContext(page, {
season: 2026,
temporal_state: 'between_sessions',
focus_meeting: meeting(1, 'British Grand Prix', '2026-07-04T10:00:00Z'),
previous_completed_session: ctxSession(11, 'Sprint', '2026-07-04T10:00:00Z', 1),
default_analysis_session: ctxSession(11, 'Sprint', '2026-07-04T10:00:00Z', 1),
next_session: ctxSession(12, 'Race', '2026-07-05T14:00:00Z', 1),
championship_round: 12,
total_championship_rounds: 24,
})
await page.goto('/')
await expect(page.getByTestId('weekend-between-sessions')).toBeVisible()
await expect(page.getByTestId('wk-last-session')).toBeVisible()
await expect(page.getByTestId('wk-next-session')).toBeVisible()
})
test('session_live hands off to live timing', async ({ page }) => {
await stubContext(page, {
season: 2026,
temporal_state: 'session_live',
focus_meeting: meeting(1, 'British Grand Prix', '2026-07-05T14:00:00Z'),
active_session: ctxSession(11, 'Race', '2026-07-05T14:00:00Z', 1),
championship_round: 12,
total_championship_rounds: 24,
})
await page.goto('/')
await expect(page.getByTestId('weekend-live')).toBeVisible()
await expect(page.getByTestId('wk-watch-live')).toHaveAttribute('href', '/live')
})
test('an error from the canonical endpoint shows an explicit error surface', async ({ page }) => {
await page.route('**/api/v1/weekend-context', (route) =>
route.fulfill({ status: 500, contentType: 'application/json', body: '{"error":"boom"}' }),
)
await page.goto('/')
await expect(page.getByTestId('weekend-error')).toBeVisible()
})
})

118
tests/weekend.spec.ts Normal file
View File

@@ -0,0 +1,118 @@
import { test, expect, type Page } from '@playwright/test'
const FULL_SESSION = 9472
async function documentOverflow(page: Page): Promise<number> {
return page.evaluate(() => {
const doc = document.documentElement
return doc.scrollWidth - doc.clientWidth
})
}
test.describe('Weekend home (seeded canonical context)', () => {
test('renders the Weekend home from the canonical endpoint, not the limited fallback', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('weekend-page')).toBeVisible()
// The seeded DB (Monaco 2025, completed) resolves to season_complete, driven
// by the canonical /api/v1/weekend-context endpoint.
await expect(page.getByTestId('weekend-page')).toHaveAttribute('data-temporal-state', 'season_complete')
await expect(page.getByTestId('weekend-between-races')).toBeVisible()
await expect(page.getByTestId('weekend-limited')).toHaveCount(0)
// No ingest/dataset terminology on the Weekend surface.
await expect(page.getByText(/ingest/i)).toHaveCount(0)
})
test('the completed-event CTA opens the canonical analysis session in Race Hub', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('wk-last-event')).toBeVisible()
await page.getByTestId('wk-explore-race-story').click()
await expect(page).toHaveURL(/\/race-hub\?session_key=\d+/)
await expect(page.getByTestId('race-hub')).toBeVisible()
})
test('/preview alias resolves without looping back to the same screen', async ({ page }) => {
await page.goto('/preview')
// In the seeded season-complete state there is no next event, so /preview
// resolves to the Weekend home rather than a redirect loop.
await expect(page.getByTestId('weekend-page')).toBeVisible()
await expect(page).toHaveURL(/\/preview$/)
})
test('existing deep-link routes remain valid', async ({ page }) => {
await page.goto(`/race-hub?session_key=${FULL_SESSION}`)
await expect(page.getByTestId('race-hub')).toBeVisible()
await expect(page.getByTestId('rh-overview')).toBeVisible()
await page.goto('/admin')
await expect(page.getByTestId('data-library')).toBeVisible()
await page.goto('/live')
await expect(page.getByTestId('live-empty')).toBeVisible()
await page.goto('/explore')
await expect(page.getByTestId('explore-page')).toBeVisible()
})
})
test.describe('Weekend navigation hierarchy', () => {
test('all four destinations are reachable from primary navigation', async ({ page }) => {
await page.goto('/')
const primary = page.getByRole('navigation', { name: 'Primary' }).first()
for (const [label, url] of [
['Championship', /\/championship/],
['Briefing', /\/briefing/],
['Explore', /\/explore/],
['Weekend', /\/$/],
] as const) {
await primary.getByRole('link', { name: label, exact: true }).click()
await expect(page).toHaveURL(url)
}
})
test('Admin is an operator utility outside the Primary landmark', async ({ page }) => {
await page.goto('/')
const primaries = page.getByRole('navigation', { name: 'Primary' })
await expect(primaries.first()).toBeVisible()
// Admin must not appear inside any Primary landmark.
await expect(primaries.getByRole('link', { name: /Admin/i })).toHaveCount(0)
await expect(
page.getByRole('toolbar', { name: 'Operator utilities' }).getByRole('link', { name: 'Admin' }),
).toBeVisible()
})
})
test.describe('Weekend responsive — no global horizontal overflow', () => {
for (const { name, width, height } of [
{ name: 'mobile 390', width: 390, height: 844 },
{ name: 'tablet 768', width: 768, height: 1024 },
{ name: 'desktop 1440', width: 1440, height: 900 },
]) {
test(`no document overflow at ${name}`, async ({ page }) => {
await page.setViewportSize({ width, height })
await page.goto('/')
await expect(page.getByTestId('weekend-between-races')).toBeVisible()
expect(await documentOverflow(page)).toBeLessThanOrEqual(1)
})
}
test('mobile shows exactly one visible primary navigation (bottom bar)', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/')
await expect(page.getByTestId('weekend-page')).toBeVisible()
// The bottom bar is the single visible primary nav; the top bar's links are
// hidden by CSS at this breakpoint.
const bottom = page.locator('.app-bottom-nav')
await expect(bottom).toBeVisible()
await expect(bottom.getByRole('link')).toHaveCount(4)
const topLinks = page.locator('.app-nav .nav-links')
await expect(topLinks).toBeHidden()
// All four destinations remain reachable via the bottom bar.
for (const label of ['Weekend', 'Championship', 'Briefing', 'Explore']) {
await expect(bottom.getByRole('link', { name: new RegExp(label) })).toBeVisible()
}
})
})