From 10729a906c431ea9028e33b693629a91f19e7fec Mon Sep 17 00:00:00 2001 From: AmanTahiliani Date: Mon, 25 May 2026 10:04:11 -0400 Subject: [PATCH] Add production web smoke coverage --- .../refactor/21-mvp-completion-checklist.md | 38 +++++++++++++++++-- frontend/src/components/live/TimingTower.tsx | 2 +- frontend/src/pages/LiveTimingPage.tsx | 4 +- frontend/src/styles/app.css | 35 ++++++++++++++++- package.json | 1 + playwright.config.ts | 1 + playwright.prod.config.ts | 30 +++++++++++++++ tests/production-smoke.spec.ts | 36 ++++++++++++++++++ 8 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 playwright.prod.config.ts create mode 100644 tests/production-smoke.spec.ts diff --git a/documentations/refactor/21-mvp-completion-checklist.md b/documentations/refactor/21-mvp-completion-checklist.md index b97adb6..4440db4 100644 --- a/documentations/refactor/21-mvp-completion-checklist.md +++ b/documentations/refactor/21-mvp-completion-checklist.md @@ -32,13 +32,16 @@ checkpoint for what has been completed and what remains. Run these before cutting an MVP tag or handing the repo to another agent: ```bash -go test ./internal/live ./internal/store ./internal/ingest ./internal/query ./internal/web -npm test -- --run --prefix frontend -npm run build --prefix frontend +go test ./internal/live ./internal/models ./internal/store ./internal/ingest ./internal/query ./internal/web +npm --prefix frontend test -- --run +npm --prefix frontend run build npm run test:e2e +npm run test:e2e:prod ``` -For a local manual smoke test: +### Dev proxy smoke (Vite + Go API) + +For a local manual smoke test with the Vite dev server proxying API calls: ```bash go run ./scripts/seed-e2e-db/main.go --db /tmp/boxbox-mvp.db @@ -52,6 +55,33 @@ Then open: - `http://127.0.0.1:15173/data-library` - `http://127.0.0.1:15173/live` +### Production web smoke (Go serves built React) + +Verify the same routes when Go serves `frontend/dist` directly (no Vite): + +```bash +npm --prefix frontend run build +go run ./scripts/seed-e2e-db/main.go --db /tmp/boxbox-mvp.db +BOXBOX_DISABLE_LIVE=1 go run ./cmd/main.go --web --db /tmp/boxbox-mvp.db --port 18080 +``` + +Then open: + +- `http://127.0.0.1:18080/race-hub?session_key=9472` +- `http://127.0.0.1:18080/data-library` +- `http://127.0.0.1:18080/live` + +Automated production-serving coverage: + +```bash +npm run test:e2e:prod +``` + +This runs `playwright.prod.config.ts`, which builds the frontend, seeds +`.playwright/boxbox-prod-e2e.db`, starts Go web mode on port 18080, and +exercises Race Hub, Data Library, Live empty state, and nav links against the +built SPA. + ## Remaining Post-MVP Work - Add real visual-regression checks for the React screens. diff --git a/frontend/src/components/live/TimingTower.tsx b/frontend/src/components/live/TimingTower.tsx index 651e06f..274a91c 100644 --- a/frontend/src/components/live/TimingTower.tsx +++ b/frontend/src/components/live/TimingTower.tsx @@ -19,7 +19,7 @@ export function TimingTower({ snapshot }: Props) { return (
- +
diff --git a/frontend/src/pages/LiveTimingPage.tsx b/frontend/src/pages/LiveTimingPage.tsx index dfa475e..f258337 100644 --- a/frontend/src/pages/LiveTimingPage.tsx +++ b/frontend/src/pages/LiveTimingPage.tsx @@ -69,7 +69,7 @@ export function LiveTimingPage() { }, []) return ( -
+
{isError && (
{error instanceof Error ? error.message : 'Failed to load live timing state'} @@ -83,7 +83,7 @@ export function LiveTimingPage() { {isLoading && !snapshot &&
connecting to live timing…
} {!isLoading && !snapshot && ( -
+
No live session active
Check back during an F1 race weekend.
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 214b88f..4ff4a63 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -40,6 +40,7 @@ body { font-size: 13px; line-height: 1.5; -webkit-font-smoothing: antialiased; + overflow-x: hidden; } a { color: inherit; text-decoration: none; } @@ -56,7 +57,11 @@ a { color: inherit; text-decoration: none; } padding: 0 var(--s6); background: var(--surface); border-bottom: 1px solid var(--border); + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; } +.app-nav::-webkit-scrollbar { display: none; } .nav-logo { font-family: var(--f-mono); @@ -68,7 +73,7 @@ a { color: inherit; text-decoration: none; } } .nav-logo em { color: var(--red); font-style: normal; } -.nav-links { display: flex; gap: 2px; } +.nav-links { display: flex; gap: 2px; flex-shrink: 0; } .nav-links a { padding: var(--s2) var(--s4); @@ -226,6 +231,8 @@ a { color: inherit; text-decoration: none; } .session-bar input { width: 110px; + min-width: 80px; + flex: 0 1 auto; padding: 4px var(--s3); background: var(--surface); border: 1px solid var(--border-2); @@ -967,9 +974,33 @@ a { color: inherit; text-decoration: none; } @media (max-width: 640px) { .page { padding: var(--s3); } .app-nav { padding: 0 var(--s4); gap: var(--s3); } + .nav-logo { font-size: 14px; } .nav-links a { padding: var(--s2) var(--s3); font-size: 11px; } .rh-meeting { font-size: 17px; } + .session-bar input { flex: 1; max-width: 140px; } + + .live-banner { + flex-direction: column; + align-items: flex-start; + gap: var(--s3); + } + .live-banner-meta { + justify-content: flex-start; + width: 100%; + } + .live-banner h1 { font-size: 16px; } + + .dataset-strip { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + gap: var(--s3); + padding-bottom: var(--s2); + } + .dataset-strip::-webkit-scrollbar { display: none; } + /* On narrow screens, hide lower-priority table columns */ .hide-mobile { display: none; } .data-table { min-width: 100% !important; } @@ -984,4 +1015,6 @@ a { color: inherit; text-decoration: none; } .dl-content-body { grid-template-columns: 1fr; } .dl-round-scroll { border-right: none; max-height: 40vh; } .dl-detail-wrap { border-top: 1px solid var(--border); } + .dl-content-header { padding: var(--s3) var(--s4); } + .dl-content-meta { font-size: 11px; } } diff --git a/package.json b/package.json index 13853c0..2cbd50d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "Repository-level test tooling for box-box.", "scripts": { "test:e2e": "playwright test", + "test:e2e:prod": "playwright test --config playwright.prod.config.ts", "test:e2e:ui": "playwright test --ui", "test:e2e:report": "playwright show-report" }, diff --git a/playwright.config.ts b/playwright.config.ts index 7fe9c28..88b624b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,6 +6,7 @@ const WEB_PORT = process.env.BOXBOX_WEB_PORT ?? '15173' export default defineConfig({ testDir: './tests', + testIgnore: '**/production-smoke.spec.ts', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, diff --git a/playwright.prod.config.ts b/playwright.prod.config.ts new file mode 100644 index 0000000..0b690e8 --- /dev/null +++ b/playwright.prod.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from '@playwright/test' + +const E2E_DB = '.playwright/boxbox-prod-e2e.db' +const PROD_PORT = process.env.BOXBOX_PROD_PORT ?? '18080' + +export default defineConfig({ + testDir: './tests', + testMatch: 'production-smoke.spec.ts', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: process.env.CI ? 'github' : 'html', + use: { + baseURL: `http://localhost:${PROD_PORT}`, + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: `npm run build --prefix frontend && go run ./scripts/seed-e2e-db/main.go --db ${E2E_DB} && BOXBOX_DISABLE_LIVE=1 go run ./cmd/main.go --web --db ${E2E_DB} --port ${PROD_PORT}`, + url: `http://localhost:${PROD_PORT}/`, + reuseExistingServer: !process.env.CI, + timeout: 180_000, + }, +}) diff --git a/tests/production-smoke.spec.ts b/tests/production-smoke.spec.ts new file mode 100644 index 0000000..05ed147 --- /dev/null +++ b/tests/production-smoke.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test' + +const FULL_SESSION = 9472 + +test.describe('Production serving (Go + built React)', () => { + test('serves race hub with classification from built assets', async ({ page }) => { + await page.goto(`/race-hub?session_key=${FULL_SESSION}`) + + await expect(page.getByText('Final Classification')).toBeVisible() + await expect(page.locator('.drv-code', { hasText: 'VER' })).toBeVisible() + }) + + test('serves data library route', async ({ page }) => { + await page.goto('/data-library') + + await expect(page.getByTestId('data-library')).toBeVisible() + await expect(page.getByTestId('dl-meeting-1229')).toBeVisible() + }) + + test('serves live route with empty state when live is disabled', async ({ page }) => { + await page.goto('/live') + + await expect(page.getByTestId('live-empty')).toBeVisible() + await expect(page.getByText('No live session active')).toBeVisible() + }) + + test('nav links work from built SPA', async ({ page }) => { + await page.goto('/race-hub') + await page.getByRole('link', { name: 'Data Library' }).click() + await expect(page).toHaveURL(/\/data-library/) + + await page.getByRole('link', { name: 'Live' }).click() + await expect(page).toHaveURL(/\/live/) + await expect(page.getByTestId('live-empty')).toBeVisible() + }) +})
Pos