mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 11:54:59 -04:00
fix(#98): preserve bare race hub handoff
This commit is contained in:
@@ -53,14 +53,6 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
enabled: preSession && preSessionMeetingKey != null && preSessionMeetingKey > 0,
|
enabled: preSession && preSessionMeetingKey != null && preSessionMeetingKey > 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sessionKey !== 0) return
|
|
||||||
const target = context?.race_hub_default_session?.session.session_key
|
|
||||||
if (target && !context?.race_hub_pre_session) {
|
|
||||||
navigate({ to: '/race-hub', search: { session_key: target }, replace: true })
|
|
||||||
}
|
|
||||||
}, [sessionKey, context, navigate])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionKey !== 0) return
|
if (sessionKey !== 0) return
|
||||||
const delay = refreshDeadlineDelay(context?.race_hub_refresh_at)
|
const delay = refreshDeadlineDelay(context?.race_hub_refresh_at)
|
||||||
@@ -75,11 +67,15 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
return () => window.clearInterval(timer)
|
return () => window.clearInterval(timer)
|
||||||
}, [preSession])
|
}, [preSession])
|
||||||
|
|
||||||
|
// A bare route retains canonical context ownership while rendering its
|
||||||
|
// completed analysis selection. Explicit URLs remain user-owned.
|
||||||
|
const selectedSessionKey = sessionKey || context?.race_hub_default_session?.session.session_key || 0
|
||||||
|
|
||||||
// ─── Active session payload ───
|
// ─── Active session payload ───
|
||||||
const raceHubQuery = useQuery({
|
const raceHubQuery = useQuery({
|
||||||
queryKey: ['race-hub', sessionKey],
|
queryKey: ['race-hub', selectedSessionKey],
|
||||||
queryFn: () => fetchRaceHub(sessionKey),
|
queryFn: () => fetchRaceHub(selectedSessionKey),
|
||||||
enabled: sessionKey > 0,
|
enabled: selectedSessionKey > 0 && (sessionKey > 0 || !preSession),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -108,7 +104,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
if (preSession && preSessionRef) {
|
if (preSession && preSessionRef) {
|
||||||
return <RaceHubPreSession session={preSessionRef} weekend={preSessionWeekendQuery.data} now={now} />
|
return <RaceHubPreSession session={preSessionRef} weekend={preSessionWeekendQuery.data} now={now} />
|
||||||
}
|
}
|
||||||
if (!context?.race_hub_default_session) {
|
if (!selectedSessionKey) {
|
||||||
return (
|
return (
|
||||||
<div className="rh-page rh-empty" data-testid="race-hub-empty" style={accentStyle}>
|
<div className="rh-page rh-empty" data-testid="race-hub-empty" style={accentStyle}>
|
||||||
<div className="rh-empty-band">
|
<div className="rh-empty-band">
|
||||||
@@ -126,18 +122,13 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<div className="rh-page" style={accentStyle}>
|
|
||||||
<div className="loading-state">resolving latest local weekend…</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Loading / error for the requested session_key ───
|
// ─── Loading / error for the selected session ───
|
||||||
if (raceHubQuery.isLoading) {
|
if (raceHubQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="rh-page" style={accentStyle}>
|
<div className="rh-page" style={accentStyle}>
|
||||||
<div className="loading-state">loading session {sessionKey}…</div>
|
<div className="loading-state">loading session {selectedSessionKey}…</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -147,7 +138,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
<div className="error-box">
|
<div className="error-box">
|
||||||
{raceHubQuery.error instanceof Error
|
{raceHubQuery.error instanceof Error
|
||||||
? raceHubQuery.error.message
|
? raceHubQuery.error.message
|
||||||
: `Failed to load session ${sessionKey}.`}
|
: `Failed to load session ${selectedSessionKey}.`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -158,7 +149,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
const sessionMeta = weekend
|
const sessionMeta = weekend
|
||||||
? Object.fromEntries(weekend.sessions.map((w) => [w.session.session_key, w]))
|
? Object.fromEntries(weekend.sessions.map((w) => [w.session.session_key, w]))
|
||||||
: {}
|
: {}
|
||||||
const activeSessionMeta = sessionMeta[sessionKey]
|
const activeSessionMeta = sessionMeta[selectedSessionKey]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rh-page" data-testid="race-hub" style={accentStyle}>
|
<div className="rh-page" data-testid="race-hub" style={accentStyle}>
|
||||||
@@ -184,7 +175,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
{switcherOpen && (
|
{switcherOpen && (
|
||||||
<WeekendSwitcher
|
<WeekendSwitcher
|
||||||
currentMeetingKey={meetingKey}
|
currentMeetingKey={meetingKey}
|
||||||
currentSessionKey={sessionKey}
|
currentSessionKey={selectedSessionKey}
|
||||||
onClose={() => setSwitcherOpen(false)}
|
onClose={() => setSwitcherOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -215,7 +206,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
<nav className="rh-session-rail" aria-label="Weekend sessions" data-testid="rh-session-rail">
|
<nav className="rh-session-rail" aria-label="Weekend sessions" data-testid="rh-session-rail">
|
||||||
{sessions.map((session) => {
|
{sessions.map((session) => {
|
||||||
const meta = sessionMeta[session.session_key]
|
const meta = sessionMeta[session.session_key]
|
||||||
const active = session.session_key === sessionKey
|
const active = session.session_key === selectedSessionKey
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={session.session_key}
|
key={session.session_key}
|
||||||
@@ -268,7 +259,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
{formatCoverageHint(activeSessionMeta.datasets)} datasets local
|
{formatCoverageHint(activeSessionMeta.datasets)} datasets local
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="rh-active-key mono">key {sessionKey}</span>
|
<span className="rh-active-key mono">key {selectedSessionKey}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -304,7 +295,7 @@ export function RaceHubPage({ sessionKey }: Props) {
|
|||||||
<span className="sec-title">Driver Compare</span>
|
<span className="sec-title">Driver Compare</span>
|
||||||
</div>
|
</div>
|
||||||
<CompareView
|
<CompareView
|
||||||
sessionKey={sessionKey}
|
sessionKey={selectedSessionKey}
|
||||||
results={data.results}
|
results={data.results}
|
||||||
drivers={data.drivers}
|
drivers={data.drivers}
|
||||||
/>
|
/>
|
||||||
@@ -364,6 +355,7 @@ function RaceHubPreSession({ session, weekend, now }: { session: ContextSession;
|
|||||||
const sessions = sortSessionsByStart((weekend?.sessions ?? []).map((entry) => entry.session))
|
const sessions = sortSessionsByStart((weekend?.sessions ?? []).map((entry) => entry.session))
|
||||||
const target = new Date(session.session.date_start)
|
const target = new Date(session.session.date_start)
|
||||||
const accent = countryAccent(meeting ?? null)
|
const accent = countryAccent(meeting ?? null)
|
||||||
|
const pendingLiveEvidence = target.getTime() <= now
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rh-page rh-empty" data-testid="race-hub-pre-session" style={{ '--gp-accent': accent } as React.CSSProperties}>
|
<div className="rh-page rh-empty" data-testid="race-hub-pre-session" style={{ '--gp-accent': accent } as React.CSSProperties}>
|
||||||
@@ -371,7 +363,9 @@ function RaceHubPreSession({ session, weekend, now }: { session: ContextSession;
|
|||||||
<span className="rh-empty-eyebrow mono">box-box · race hub</span>
|
<span className="rh-empty-eyebrow mono">box-box · race hub</span>
|
||||||
<h1 className="rh-empty-title">{meeting?.meeting_name ?? 'Next race weekend'}</h1>
|
<h1 className="rh-empty-title">{meeting?.meeting_name ?? 'Next race weekend'}</h1>
|
||||||
<p className="rh-empty-sub">
|
<p className="rh-empty-sub">
|
||||||
{session.session.session_name} begins in <span className="mono">{formatCountdown(target, new Date(now))}</span>
|
{pendingLiveEvidence
|
||||||
|
? `${session.session.session_name} is scheduled; awaiting live timing.`
|
||||||
|
: <>{session.session.session_name} begins in <span className="mono">{formatCountdown(target, new Date(now))}</span></>}
|
||||||
</p>
|
</p>
|
||||||
{sessions.length > 0 && (
|
{sessions.length > 0 && (
|
||||||
<div className="preview-schedule" data-testid="rh-pre-session-schedule">
|
<div className="preview-schedule" data-testid="rh-pre-session-schedule">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
|
import { act, render, screen, waitFor, fireEvent } from '@testing-library/react'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
Outlet,
|
Outlet,
|
||||||
@@ -215,18 +215,18 @@ function renderRaceHub(sessionKey: number) {
|
|||||||
return <RaceHubPage sessionKey={session_key ?? 0} />
|
return <RaceHubPage sessionKey={session_key ?? 0} />
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
window.history.pushState({}, '', sessionKey ? `/race-hub?session_key=${sessionKey}` : '/race-hub')
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree: rootRoute.addChildren([raceHubRoute]),
|
routeTree: rootRoute.addChildren([raceHubRoute]),
|
||||||
history: undefined,
|
history: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Navigate to the URL before mounting
|
return { queryClient, ...render(<RouterProvider router={router} />) }
|
||||||
router.navigate({ to: '/race-hub', search: sessionKey ? { session_key: sessionKey } : {} })
|
|
||||||
return render(<RouterProvider router={router} />)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('RaceHubPage', () => {
|
describe('RaceHubPage', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockFetchSeasons.mockResolvedValue([2025])
|
mockFetchSeasons.mockResolvedValue([2025])
|
||||||
mockFetchLocalMeetings.mockResolvedValue([meeting])
|
mockFetchLocalMeetings.mockResolvedValue([meeting])
|
||||||
@@ -235,6 +235,10 @@ describe('RaceHubPage', () => {
|
|||||||
mockFetchWeekendContext.mockResolvedValue(analysisContext)
|
mockFetchWeekendContext.mockResolvedValue(analysisContext)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
it('renders the workspace identity band, session rail, and overview for a known session', async () => {
|
it('renders the workspace identity band, session rail, and overview for a known session', async () => {
|
||||||
renderRaceHub(9472)
|
renderRaceHub(9472)
|
||||||
|
|
||||||
@@ -280,7 +284,7 @@ describe('RaceHubPage', () => {
|
|||||||
expect(await screen.findByTestId('rh-switcher')).toBeInTheDocument()
|
expect(await screen.findByTestId('rh-switcher')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the server-selected completed analysis session for bare Race Hub', async () => {
|
it('uses the server-selected completed analysis session for bare Race Hub without changing the URL', async () => {
|
||||||
renderRaceHub(0)
|
renderRaceHub(0)
|
||||||
|
|
||||||
await waitFor(() => expect(mockFetchRaceHub).toHaveBeenCalledWith(9472))
|
await waitFor(() => expect(mockFetchRaceHub).toHaveBeenCalledWith(9472))
|
||||||
@@ -319,4 +323,42 @@ describe('RaceHubPage', () => {
|
|||||||
expect(await screen.findByTestId('race-hub-empty')).toHaveTextContent('No completed local analysis yet')
|
expect(await screen.findByTestId('race-hub-empty')).toHaveTextContent('No completed local analysis yet')
|
||||||
expect(mockFetchRaceHub).not.toHaveBeenCalled()
|
expect(mockFetchRaceHub).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('hands a bare route from completed analysis to pre-session at the supplied refresh boundary', async () => {
|
||||||
|
vi.useFakeTimers({ shouldAdvanceTime: true })
|
||||||
|
const handoff = new Date(Date.now() + 10_000).toISOString()
|
||||||
|
const pendingContext: WeekendContext = {
|
||||||
|
...analysisContext,
|
||||||
|
race_hub_default_session: {
|
||||||
|
session: { ...raceSession, session_key: 9473, session_name: 'Practice 1', session_type: 'Practice', date_start: handoff },
|
||||||
|
meeting,
|
||||||
|
availability,
|
||||||
|
},
|
||||||
|
race_hub_pre_session: true,
|
||||||
|
race_hub_refresh_at: new Date(Date.now() + 16_000).toISOString(),
|
||||||
|
}
|
||||||
|
mockFetchWeekendContext
|
||||||
|
.mockResolvedValueOnce({ ...analysisContext, race_hub_refresh_at: handoff })
|
||||||
|
.mockResolvedValueOnce(pendingContext)
|
||||||
|
|
||||||
|
renderRaceHub(0)
|
||||||
|
await screen.findByTestId('race-hub')
|
||||||
|
await act(async () => { await vi.advanceTimersByTimeAsync(10_000) })
|
||||||
|
|
||||||
|
expect(await screen.findByTestId('race-hub-pre-session')).toBeInTheDocument()
|
||||||
|
expect(mockFetchWeekendContext).toHaveBeenCalledTimes(2)
|
||||||
|
expect(mockFetchRaceHub).toHaveBeenCalledWith(9472)
|
||||||
|
expect(mockFetchRaceHub).not.toHaveBeenCalledWith(9473)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps an explicit session URL stable across the canonical refresh boundary', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
renderRaceHub(9472)
|
||||||
|
|
||||||
|
await act(async () => { await vi.advanceTimersByTimeAsync(60_000) })
|
||||||
|
|
||||||
|
expect(mockFetchWeekendContext).not.toHaveBeenCalled()
|
||||||
|
expect(mockFetchRaceHub).toHaveBeenCalledWith(9472)
|
||||||
|
expect(mockFetchRaceHub).not.toHaveBeenCalledWith(9473)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const (
|
|||||||
|
|
||||||
preSessionWindow = 48 * time.Hour
|
preSessionWindow = 48 * time.Hour
|
||||||
postWeekendWindow = 48 * time.Hour
|
postWeekendWindow = 48 * time.Hour
|
||||||
|
raceHubPendingPollInterval = 15 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// LiveEvidence is the small, transport-independent subset of FIA state needed
|
// LiveEvidence is the small, transport-independent subset of FIA state needed
|
||||||
@@ -153,7 +154,7 @@ func (s *Service) ResolveWeekendContext(evidence LiveEvidence) (WeekendContext,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var previous, next, defaultAnalysis *contextCandidate
|
var previous, next, defaultAnalysis, pending *contextCandidate
|
||||||
for i := range candidates {
|
for i := range candidates {
|
||||||
c := &candidates[i]
|
c := &candidates[i]
|
||||||
isActive := active != nil && active.session.SessionKey != 0 && c.session.SessionKey == active.session.SessionKey
|
isActive := active != nil && active.session.SessionKey != 0 && c.session.SessionKey == active.session.SessionKey
|
||||||
@@ -167,6 +168,10 @@ func (s *Service) ResolveWeekendContext(evidence LiveEvidence) (WeekendContext,
|
|||||||
if !isActive && !c.start.IsZero() && !c.start.Before(now) && (next == nil || c.start.Before(next.start)) {
|
if !isActive && !c.start.IsZero() && !c.start.Before(now) && (next == nil || c.start.Before(next.start)) {
|
||||||
next = c
|
next = c
|
||||||
}
|
}
|
||||||
|
if !isActive && !c.complete && !c.start.IsZero() && !c.start.After(now) &&
|
||||||
|
(c.end.IsZero() || now.Before(c.end)) && (pending == nil || c.start.After(pending.start)) {
|
||||||
|
pending = c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if previous != nil {
|
if previous != nil {
|
||||||
@@ -191,14 +196,14 @@ func (s *Service) ResolveWeekendContext(evidence LiveEvidence) (WeekendContext,
|
|||||||
if out.FocusMeeting != nil {
|
if out.FocusMeeting != nil {
|
||||||
out.ChampionshipRound = championshipRound(champMeetings, int(out.FocusMeeting.MeetingKey))
|
out.ChampionshipRound = championshipRound(champMeetings, int(out.FocusMeeting.MeetingKey))
|
||||||
}
|
}
|
||||||
applyRaceHubDefault(&out, active, defaultAnalysis, next, now)
|
applyRaceHubDefault(&out, active, defaultAnalysis, next, pending, now)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyRaceHubDefault is deliberately distinct from TemporalPreSession. Other
|
// applyRaceHubDefault is deliberately distinct from TemporalPreSession. Other
|
||||||
// weekend surfaces begin preparation 48 hours ahead; Race Hub remains an
|
// weekend surfaces begin preparation 48 hours ahead; Race Hub remains an
|
||||||
// analysis destination until the one-hour handoff before the next session.
|
// analysis destination until the one-hour handoff before the next session.
|
||||||
func applyRaceHubDefault(out *WeekendContext, active, analysis, next *contextCandidate, now time.Time) {
|
func applyRaceHubDefault(out *WeekendContext, active, analysis, next, pending *contextCandidate, now time.Time) {
|
||||||
if active != nil {
|
if active != nil {
|
||||||
out.RaceHubDefaultSession = out.ActiveSession
|
out.RaceHubDefaultSession = out.ActiveSession
|
||||||
return
|
return
|
||||||
@@ -214,6 +219,12 @@ func applyRaceHubDefault(out *WeekendContext, active, analysis, next *contextCan
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if pending != nil {
|
||||||
|
out.RaceHubDefaultSession = sessionRef(*pending, LiveEvidence{}, now)
|
||||||
|
out.RaceHubPreSession = true
|
||||||
|
out.RaceHubRefreshAt = now.Add(raceHubPendingPollInterval).Format(time.RFC3339)
|
||||||
|
return
|
||||||
|
}
|
||||||
if analysis != nil {
|
if analysis != nil {
|
||||||
out.RaceHubDefaultSession = out.DefaultAnalysisSession
|
out.RaceHubDefaultSession = out.DefaultAnalysisSession
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -441,6 +441,22 @@ func TestResolveWeekendContextRaceHubDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("keeps the scheduled session pending after its start without live evidence", func(t *testing.T) {
|
||||||
|
now, _ := time.Parse(time.RFC3339, "2026-07-17T09:00:00Z")
|
||||||
|
svc := contextService(t, now)
|
||||||
|
seed(t, svc)
|
||||||
|
got, err := svc.ResolveWeekendContext(LiveEvidence{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.RaceHubDefaultSession == nil || got.RaceHubDefaultSession.Session.SessionKey != 21 || !got.RaceHubPreSession {
|
||||||
|
t.Fatalf("scheduled race hub default = %+v, pre-session = %t", got.RaceHubDefaultSession, got.RaceHubPreSession)
|
||||||
|
}
|
||||||
|
if got.RaceHubRefreshAt != "2026-07-17T09:00:15Z" {
|
||||||
|
t.Fatalf("refresh = %q", got.RaceHubRefreshAt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("active live session wins", func(t *testing.T) {
|
t.Run("active live session wins", func(t *testing.T) {
|
||||||
now, _ := time.Parse(time.RFC3339, "2026-07-17T08:30:00Z")
|
now, _ := time.Parse(time.RFC3339, "2026-07-17T08:30:00Z")
|
||||||
svc := contextService(t, now)
|
svc := contextService(t, now)
|
||||||
|
|||||||
@@ -2,6 +2,42 @@ import { test, expect } from '@playwright/test'
|
|||||||
|
|
||||||
const FULL_SESSION = 9472
|
const FULL_SESSION = 9472
|
||||||
const CORE_ONLY_SESSION = 9000
|
const CORE_ONLY_SESSION = 9000
|
||||||
|
const CONTEXT_MEETING = {
|
||||||
|
meeting_key: 1229,
|
||||||
|
meeting_name: 'Monaco',
|
||||||
|
country_code: 'MON',
|
||||||
|
}
|
||||||
|
const CONTEXT_AVAILABILITY = {
|
||||||
|
source: 'local', schedule: 'available', live_transport: 'unknown', live_session: 'inactive',
|
||||||
|
archive: 'unavailable', local_analysis: 'complete', freshness: 'local', limitations: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
function completedContext(refreshAt?: string) {
|
||||||
|
return {
|
||||||
|
temporal_state: 'between_weekends',
|
||||||
|
race_hub_default_session: {
|
||||||
|
session: { session_key: FULL_SESSION }, meeting: CONTEXT_MEETING, availability: CONTEXT_AVAILABILITY,
|
||||||
|
},
|
||||||
|
race_hub_pre_session: false,
|
||||||
|
race_hub_refresh_at: refreshAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pendingContext(refreshAt: string) {
|
||||||
|
return {
|
||||||
|
temporal_state: 'pre_session',
|
||||||
|
race_hub_default_session: {
|
||||||
|
session: {
|
||||||
|
session_key: 9473, meeting_key: 1229, session_name: 'Practice 1', session_type: 'Practice',
|
||||||
|
date_start: '2030-01-01T00:00:01Z', date_end: '2030-01-01T01:00:01Z', gmt_offset: '00:00:00',
|
||||||
|
},
|
||||||
|
meeting: CONTEXT_MEETING,
|
||||||
|
availability: { ...CONTEXT_AVAILABILITY, local_analysis: 'not_applicable' },
|
||||||
|
},
|
||||||
|
race_hub_pre_session: true,
|
||||||
|
race_hub_refresh_at: refreshAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test.describe('Race Hub Weekend Workspace', () => {
|
test.describe('Race Hub Weekend Workspace', () => {
|
||||||
test('lands on the Overview tab with workspace identity', async ({ page }) => {
|
test('lands on the Overview tab with workspace identity', async ({ page }) => {
|
||||||
@@ -102,9 +138,65 @@ test.describe('Race Hub Weekend Workspace', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('bare /race-hub redirects to the focus session', async ({ page }) => {
|
test('bare /race-hub shows server-selected completed analysis without changing the URL', async ({ page }) => {
|
||||||
|
await page.route('**/api/v1/weekend-context', (route) =>
|
||||||
|
route.fulfill({ contentType: 'application/json', body: JSON.stringify(completedContext()) }),
|
||||||
|
)
|
||||||
await page.goto('/race-hub')
|
await page.goto('/race-hub')
|
||||||
await expect(page).toHaveURL(/session_key=\d+/)
|
await expect(page).toHaveURL(/\/race-hub$/)
|
||||||
await expect(page.getByTestId('race-hub')).toBeVisible()
|
await expect(page.getByTestId('race-hub')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('bare /race-hub hands off to pending pre-session state at the refresh deadline', async ({ page }) => {
|
||||||
|
await page.clock.install({ time: new Date('2030-01-01T00:00:00Z') })
|
||||||
|
let requests = 0
|
||||||
|
const raceHubRequests: number[] = []
|
||||||
|
page.on('request', (request) => {
|
||||||
|
const url = new URL(request.url())
|
||||||
|
if (url.pathname === '/api/v1/race-hub') {
|
||||||
|
raceHubRequests.push(Number(url.searchParams.get('session_key')))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await page.route('**/api/v1/weekend-context', (route) => {
|
||||||
|
requests += 1
|
||||||
|
const body = requests === 1
|
||||||
|
? completedContext('2030-01-01T00:00:01Z')
|
||||||
|
: pendingContext('2030-01-01T00:00:16Z')
|
||||||
|
return route.fulfill({ contentType: 'application/json', body: JSON.stringify(body) })
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto('/race-hub')
|
||||||
|
await expect(page.getByTestId('race-hub')).toBeVisible()
|
||||||
|
await page.clock.fastForward(1_000)
|
||||||
|
|
||||||
|
await expect(page.getByTestId('race-hub-pre-session')).toBeVisible()
|
||||||
|
await expect(page).toHaveURL(/\/race-hub$/)
|
||||||
|
expect(raceHubRequests).toContain(FULL_SESSION)
|
||||||
|
expect(raceHubRequests).not.toContain(9473)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bare /race-hub recovers when no completed local analysis exists', async ({ page }) => {
|
||||||
|
await page.route('**/api/v1/weekend-context', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ temporal_state: 'between_weekends', race_hub_pre_session: false }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.goto('/race-hub')
|
||||||
|
await expect(page.getByTestId('race-hub-empty')).toContainText('No completed local analysis yet')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('an explicit session URL remains stable when canonical context would refresh', async ({ page }) => {
|
||||||
|
let contextRequested = false
|
||||||
|
await page.route('**/api/v1/weekend-context', (route) => {
|
||||||
|
contextRequested = true
|
||||||
|
return route.fulfill({ contentType: 'application/json', body: JSON.stringify(pendingContext('2030-01-01T00:00:01Z')) })
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(`/race-hub?session_key=${FULL_SESSION}`)
|
||||||
|
await expect(page.getByTestId('race-hub')).toBeVisible()
|
||||||
|
await expect(page).toHaveURL(new RegExp(`session_key=${FULL_SESSION}`))
|
||||||
|
expect(contextRequested).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user