Restructure the workshop flow into the designer's cockpit: a persistent shell (header + 5-step stepper + sticky instrument rail) wrapping the phase routes via a React-Router layout route, so the rail stays mounted across navigation. - Design system: IBM Plex Mono + Newsreader; the full cockpit token set (light + dark) in index.css; a working light/dark theme toggle (store `theme` + useApplyTheme); a `switch` ui primitive. - Shell: CockpitLayout, Stepper (forward-gated), PanelChrome helpers. Every phase page restyled to the editorial panels + the WORKSHOP-FLOW fixes (channels-after-bind, domain framing + L1 prefill, L2/L3 prefill at 3/3, in-place submission finale). Store gains `tried` + `channels.saidHi` (v4). - Live rail (CockpitRail): real node heartbeat + agent activity log + ADD progress; sim telemetry (useTelemetry) for the waveform/accel/I2C behind a seam, marked SIM. - LED-matrix PIXEL MIRROR (real): the rail shows exactly what the physical matrix displays — API GET /nodes/:team/matrix reads the board's framebuffer off the :9999 relay (readMatrixFrame + the `matrixget` relay command); useMatrixMirror polls it and unpacks the 104 bits. Co-Authored-By: Claude Opus 4.8 <[email protected]>
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { render, screen, act, waitFor } 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'
|
|
import type { WsEvent } from '@/types'
|
|
|
|
let emit: (e: WsEvent) => void = () => {}
|
|
vi.mock('@/lib/api', async (orig) => ({
|
|
...(await orig<typeof import('@/lib/api')>()),
|
|
openTeamActivity: (_teamId: string, on: (e: WsEvent) => void) => {
|
|
emit = on
|
|
return () => {}
|
|
},
|
|
sendPrompt: vi.fn().mockResolvedValue(undefined),
|
|
}))
|
|
|
|
function renderPage() {
|
|
return render(
|
|
<MemoryRouter>
|
|
<Module2 />
|
|
</MemoryRouter>,
|
|
)
|
|
}
|
|
|
|
/** Click a canned prompt and let the agent reach a terminal (success) step. */
|
|
async function completePrompt(user: ReturnType<typeof userEvent.setup>, id: string) {
|
|
await user.click(screen.getByTestId(`prompt-${id}`))
|
|
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
|
|
await waitFor(() => expect(screen.getByTestId(`prompt-${id}`)).toHaveAttribute('data-state', 'done'))
|
|
}
|
|
|
|
describe('Module2', () => {
|
|
beforeEach(() => {
|
|
useSession.getState().reset()
|
|
sessionStorage.clear()
|
|
})
|
|
|
|
it('renders the heading', () => {
|
|
renderPage()
|
|
expect(screen.getByRole('heading', { name: /skills.*policies/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it('is a chat with the three canned prompts — no live feed / build & flash', () => {
|
|
renderPage()
|
|
expect(screen.getByTestId('agent-chat')).toBeInTheDocument()
|
|
expect(screen.getByTestId('prompt-i2c')).toBeInTheDocument()
|
|
expect(screen.getByTestId('prompt-count')).toBeInTheDocument()
|
|
expect(screen.getByTestId('prompt-scroll')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('live-board-feed')).toBeNull()
|
|
expect(screen.queryByTestId('activity-log')).toBeNull()
|
|
})
|
|
|
|
it('reveals "what\'s next" (the ADD layers) only after all three prompts succeed', async () => {
|
|
const user = userEvent.setup()
|
|
renderPage()
|
|
expect(screen.queryByTestId('whats-next')).toBeNull()
|
|
expect(screen.queryByLabelText(/layer 2/i)).toBeNull()
|
|
|
|
await completePrompt(user, 'i2c')
|
|
await completePrompt(user, 'count')
|
|
expect(screen.queryByTestId('whats-next')).toBeNull() // still one to go
|
|
await completePrompt(user, 'scroll')
|
|
|
|
expect(screen.getByTestId('whats-next')).toBeInTheDocument()
|
|
expect(screen.getByLabelText(/layer 2/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('gates Proceed until all prompts ran AND L2 + L3 are filled', async () => {
|
|
const user = userEvent.setup()
|
|
useSession.getState().setAddLayer('L2', 'escalate on critical')
|
|
useSession.getState().setAddLayer('L3', 'drive damper on critical')
|
|
renderPage()
|
|
|
|
await completePrompt(user, 'i2c')
|
|
await completePrompt(user, 'count')
|
|
await completePrompt(user, 'scroll')
|
|
|
|
const proceed = screen.getByRole('button', { name: /proceed/i })
|
|
expect(proceed).toBeEnabled()
|
|
})
|
|
|
|
it('marks m2 complete on Proceed', async () => {
|
|
const user = userEvent.setup()
|
|
renderPage()
|
|
await completePrompt(user, 'i2c')
|
|
await completePrompt(user, 'count')
|
|
await completePrompt(user, 'scroll')
|
|
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: /proceed/i }))
|
|
expect(useSession.getState().phases.m2).toBe(true)
|
|
})
|
|
})
|