fix(#76): correct frontend availability, retry, and live recovery semantics

Make response freshness React Query-safe, retry the failed Weekend/Preview/Briefing resources with busy gating, preserve distinct embedded Preview notices, and clear fatal Live errors once SSE supplies usable timing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 20:43:51 -04:00
parent eabf87a2b8
commit 01d291507d
17 changed files with 659 additions and 63 deletions

View File

@@ -184,33 +184,66 @@ async function stubPreSession(page: Page, opts?: { failSessions?: boolean; recov
}
test.describe('Issue #76 availability / Preview resilience evidence', () => {
test('Weekend pre-session keeps shell usable on Preview supplement failure and recovers', async ({
test('Weekend pre-session recovers Preview sessions via Retry and keeps distinct notices', async ({
page,
}, testInfo) => {
await stubPreSession(page, { failSessions: true, recover: true })
let sessionCalls = 0
let allowSessions = false
await stubPreSession(page)
await page.unroute('**/api/v1/sessions**')
await page.route('**/api/v1/sessions**', async (route) => {
const url = new URL(route.request().url())
if (url.searchParams.get('meeting_key') === '2') {
sessionCalls += 1
}
if (!allowSessions) {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'API 500: forced sessions failure' }),
})
return
}
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify([
{
session_key: 21,
session_name: 'Practice 1',
session_type: 'Practice',
meeting_key: 2,
date_start: '2026-07-24T09:00:00Z',
date_end: '2026-07-24T10:00:00Z',
gmt_offset: '',
},
]),
})
})
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/')
await expect(page.getByTestId('weekend-pre-session')).toBeVisible()
await expect(page.getByTestId('wk-pre-head')).toContainText('Hungarian Grand Prix')
await expect(page.getByTestId('weekend-data-notice')).toContainText(/Partial/i)
// Weekend shell owns disclosure — embedded Preview must not stack a second notice.
await expect(page.getByTestId('preview-data-notice')).toHaveCount(0)
// Shell Partial + Preview championship Stale are distinct — both may show.
await expect(page.getByTestId('preview-data-notice')).toContainText(/Stale/i)
await expect(page.getByTestId('preview-page')).toBeVisible()
await expect(page.getByTestId('preview-page')).toHaveAttribute('data-meeting-key', '2')
// No raw HTTP jargon in the primary Weekend child.
await expect(page.getByTestId('preview-sessions-error')).toBeVisible()
await expect(page.locator('body')).not.toContainText(/API 500|forced sessions failure/i)
// Title fight remains usable from the shared championship supplement.
await expect(page.getByTestId('preview-title-fight-card')).toContainText('VER')
// Recovery: guarded Retry on the shell notice re-runs context; sessions auto-recover
// on React Query retry (recover stub). Shell stays usable either way.
const retry = page.getByTestId('weekend-data-notice').getByRole('button', { name: 'Retry' })
const failedCalls = sessionCalls
expect(failedCalls).toBeGreaterThanOrEqual(1)
allowSessions = true
const retry = page.getByTestId('preview-sessions-retry')
await expect(retry).toBeEnabled()
await retry.click()
await expect(page.getByTestId('weekend-pre-session')).toBeVisible()
await expect(page.getByTestId('preview-page')).toBeVisible()
await expect(page.getByTestId('preview-title-fight-card')).toContainText('VER')
await expect(page.getByTestId('preview-schedule')).toContainText('Practice 1')
await expect(page.getByTestId('preview-sessions-error')).toHaveCount(0)
expect(sessionCalls).toBe(failedCalls + 1)
await page.screenshot({
path: path.join(evidenceDir, `weekend-presession-failure-mobile-${testInfo.project.name}.png`),
@@ -222,9 +255,8 @@ test.describe('Issue #76 availability / Preview resilience evidence', () => {
path: path.join(evidenceDir, `weekend-presession-partial-desktop-${testInfo.project.name}.png`),
fullPage: true,
})
// Desktop evidence must also keep a single Partial notice vocabulary.
await expect(page.getByTestId('weekend-data-notice')).toContainText(/Partial/i)
await expect(page.getByTestId('preview-data-notice')).toHaveCount(0)
await expect(page.getByTestId('preview-data-notice')).toContainText(/Stale/i)
})
test('Championship discloses stale metadata above usable standings', async ({ page }, testInfo) => {
@@ -343,6 +375,8 @@ test.describe('Issue #76 availability / Preview resilience evidence', () => {
await expect(page.getByTestId('briefing-data-notice')).toContainText(/Limited/i)
await expect(page.getByText('Paddock briefing sample')).toBeVisible()
await expect(page.locator('body')).not.toContainText(/API 503/i)
const retry = page.getByTestId('briefing-data-notice').getByRole('button', { name: 'Retry' })
await expect(retry).toBeEnabled()
await page.screenshot({
path: path.join(evidenceDir, `briefing-limited-mobile-${testInfo.project.name}.png`),
fullPage: true,

View File

@@ -452,4 +452,28 @@ test.describe('Live Timing (no session)', () => {
await expect(page.getByTestId('live-feed-health')).toContainText(/reconnecting/i)
await expect(page.getByTestId('live-feed-health')).not.toContainText(/feed healthy/i)
})
test('clears fatal initial error once SSE supplies a live snapshot', async ({ page }) => {
await page.route('**/api/v1/live/state', (route) =>
route.fulfill({
status: 503,
contentType: 'application/json',
body: JSON.stringify({ error: 'API 503: live state unavailable' }),
}),
)
await page.route('**/api/v1/live/stream', (route) =>
route.fulfill({
contentType: 'text/event-stream',
headers: { 'Cache-Control': 'no-cache' },
body: `event: snapshot\ndata: ${JSON.stringify(raceSnapshot)}\n\n`,
}),
)
await page.goto('/live')
await expect(page.getByText('Timing Tower')).toBeVisible({ timeout: 15_000 })
await expect(page.getByTestId('live-initial-error')).toHaveCount(0)
await expect(page.getByText(/Live timing unavailable/i)).toHaveCount(0)
await expect(page.getByTestId('live-page')).toHaveAttribute('data-phase', /^(live|disconnected)$/)
await expect(page.locator('.live-tower')).toContainText('VER')
})
})