mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 11:54:59 -04:00
Add production web smoke coverage
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
30
playwright.prod.config.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
36
tests/production-smoke.spec.ts
Normal file
36
tests/production-smoke.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user