feat: participant flow screens setup/module1/module2/add — TDD
All four workshop phases are now real screens, replacing WorkshopStub: - EnvSetup (/workshop/setup): device check + serial self-test (no stats pollution) + provider/model selection; gated Proceed - Module1 (/workshop/module1): live IMU feed via useSerial, actor map, ADD Layer 1 capture; gated on observed frames + L1 goal - Module2 (/workshop/module2): harness tuner + live TOML preview, test triggers that classify against the tuned threshold, ADD L2/L3 - AddBuilder (/workshop/add): ADD L4/L5, assembled AddDocument, print-to-PDF export, deterministic submission code; completes the run - New components: ActorMap, HarnessTuner, TriggerButtons, AddDocument 23 new tests; suite 86/86 green, typecheck + lint clean, build OK. Only /admin and /judge remain stubbed (need the backend). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
965af7e5c0
commit
7c141dcd89
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { Module2 } from './Module2'
|
||||
import { useSession } from '@/store/session'
|
||||
|
||||
function renderPage() {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<Module2 />
|
||||
</MemoryRouter>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('Module2', () => {
|
||||
beforeEach(() => {
|
||||
useSession.getState().reset()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
it('renders the phase strip set to m2 and the heading', () => {
|
||||
renderPage()
|
||||
expect(screen.getByRole('heading', { name: /harness engineering/i })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByTestId('phase-strip').querySelector('[data-phase="m2"]'),
|
||||
).toHaveAttribute('data-state', 'active')
|
||||
})
|
||||
|
||||
it('tunes a threshold and reflects it in the TOML preview', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
const input = screen.getByLabelText(/threshold · g/i)
|
||||
await user.clear(input)
|
||||
await user.type(input, '1.2')
|
||||
expect(useSession.getState().harness.thresholdG).toBe(1.2)
|
||||
expect(screen.getByTestId('harness-toml')).toHaveTextContent('threshold_g = 1.2')
|
||||
})
|
||||
|
||||
it('fires a trigger that classifies against the tuned threshold', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
// tune low so the impact frame is unambiguously critical
|
||||
const g = screen.getByLabelText(/threshold · g/i)
|
||||
await user.clear(g)
|
||||
await user.type(g, '0.5')
|
||||
await user.click(screen.getByRole('button', { name: /impact/i }))
|
||||
expect(useSession.getState().stats.critical).toBe(1)
|
||||
})
|
||||
|
||||
it('gates Proceed until a trigger fired and L2 + L3 are filled', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
const proceed = screen.getByRole('button', { name: /proceed/i })
|
||||
expect(proceed).toBeDisabled()
|
||||
|
||||
await user.type(screen.getByLabelText(/layer 2/i), 'escalate on critical')
|
||||
await user.type(screen.getByLabelText(/layer 3/i), 'drive damper on critical')
|
||||
expect(proceed).toBeDisabled() // no trigger fired yet
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /shake/i }))
|
||||
expect(proceed).toBeEnabled()
|
||||
})
|
||||
|
||||
it('marks m2 complete on Proceed', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
await user.type(screen.getByLabelText(/layer 2/i), 'L2 text')
|
||||
await user.type(screen.getByLabelText(/layer 3/i), 'L3 text')
|
||||
await user.click(screen.getByRole('button', { name: /impact/i }))
|
||||
await user.click(screen.getByRole('button', { name: /proceed/i }))
|
||||
expect(useSession.getState().phases.m2).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user