Add production web smoke coverage

This commit is contained in:
2026-05-25 10:04:11 -04:00
parent 17c94d83ad
commit 10729a906c
8 changed files with 139 additions and 8 deletions

View File

@@ -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.

View File

@@ -19,7 +19,7 @@ export function TimingTower({ snapshot }: Props) {
return (
<div className="scroll-x">
<table className="data-table live-tower">
<table className="data-table live-tower" style={{ minWidth: 520 }}>
<thead>
<tr>
<th>Pos</th>

View File

@@ -69,7 +69,7 @@ export function LiveTimingPage() {
}, [])
return (
<div className="page live-page">
<div className="page live-page" data-testid="live-page">
{isError && (
<div className="error-box">
{error instanceof Error ? error.message : 'Failed to load live timing state'}
@@ -83,7 +83,7 @@ export function LiveTimingPage() {
{isLoading && !snapshot && <div className="loading-state">connecting to live timing</div>}
{!isLoading && !snapshot && (
<div className="empty-state">
<div className="empty-state" data-testid="live-empty">
<div className="empty-state-title">No live session active</div>
<div className="empty-state-desc">Check back during an F1 race weekend.</div>
</div>

View File

@@ -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; }
}

View File

@@ -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"
},

View File

@@ -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,

30
playwright.prod.config.ts Normal file
View File

@@ -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,
},
})

View File

@@ -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()
})
})