Files
apress/src/pages/EnvSetup.test.tsx
T
Omar SobhandClaude Opus 4.8 cefddb245c feat(phase2): real say-hi chat + Connected node state on Meet-your-node
- Open your node: shows a 'Connected' badge (+ board name) once the board is claimed.
- Say hi: replaces the liveness-poll self-test with an actual round-trip to the
  agent on the team's own board — sends a greeting, shows the agent's reply, and
  only unlocks once it answers. New blocking /nodes/:teamId/say-hi route (greeting
  never flashes, so no hang risk) + bridge.sayHi + client sayHi(). The /prompt route
  stays fire-and-forget for the flash path.
- Pick your domain already captures to the store and syncs to the server DB via
  useCollectiveSync -> pushTeam, available to later steps/judging.
- Proceed gates on connected + agent-replied + domain. Tests updated + say-hi
  coverage (route + page).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21 09:26:50 -07:00

115 lines
4.3 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { EnvSetup } from './EnvSetup'
import { useSession } from '@/store/session'
import { sayHi } from '@/lib/api'
vi.mock('@/lib/api', async (orig) => ({
...(await orig<typeof import('@/lib/api')>()),
sayHi: vi.fn(),
}))
const mockSayHi = vi.mocked(sayHi)
function renderPage() {
return render(
<MemoryRouter>
<EnvSetup />
</MemoryRouter>,
)
}
/** Connect a board with a live node URL — the common precondition. */
function connect(nodeUrl: string | null = 'http://192.168.1.7:8080') {
useSession.getState().setDevice({ connected: true, port: 'board · crimson-otter', uptimeS: 0, nodeUrl })
}
describe('EnvSetup — Meet your node', () => {
beforeEach(() => {
useSession.getState().reset()
sessionStorage.clear()
mockSayHi.mockReset()
})
it('renders the phase strip set to setup and the heading', () => {
renderPage()
expect(screen.getByTestId('phase-strip')).toBeInTheDocument()
expect(screen.getByRole('heading', { name: /meet your node/i })).toBeInTheDocument()
expect(
screen.getByTestId('phase-strip').querySelector('[data-phase="setup"]'),
).toHaveAttribute('data-state', 'active')
})
it('prompts to claim a board first when not connected', () => {
renderPage()
expect(screen.getByText(/claim your board first/i)).toBeInTheDocument()
expect(screen.getByRole('button', { name: /say hi to your agent/i })).toBeDisabled()
expect(screen.getByRole('button', { name: /proceed/i })).toBeDisabled()
})
it('shows the node as Connected with the board url when claimed', () => {
connect('http://192.168.1.7:8080')
renderPage()
const link = screen.getByRole('link', { name: /open your node/i })
expect(link).toHaveAttribute('href', 'http://192.168.1.7:8080')
expect(link).toHaveAttribute('target', '_blank')
expect(screen.getByText(/^connected$/i)).toBeInTheDocument()
})
it('falls back to the claim prompt when connected but there is no nodeUrl', () => {
connect(null)
renderPage()
expect(screen.queryByRole('link', { name: /open your node/i })).not.toBeInTheDocument()
expect(screen.getByText(/claim your board first/i)).toBeInTheDocument()
})
it('says hi to the agent and shows its reply', async () => {
mockSayHi.mockResolvedValue("Hi! I'm your node — I can read your sensor and drive the matrix.")
connect()
renderPage()
fireEvent.click(screen.getByRole('button', { name: /say hi to your agent/i }))
expect(await screen.findByTestId('node-reply')).toHaveTextContent(/read your sensor/i)
expect(screen.getByRole('button', { name: /your node replied/i })).toBeInTheDocument()
})
it('requires a domain even after the agent replies', async () => {
mockSayHi.mockResolvedValue('hello there')
connect()
renderPage()
fireEvent.click(screen.getByRole('button', { name: /say hi to your agent/i }))
await screen.findByTestId('node-reply')
expect(screen.getByRole('button', { name: /proceed/i })).toBeDisabled()
})
it('keeps Proceed gated when a domain is named but the agent has not replied', () => {
connect()
useSession.getState().setDomain('air quality')
renderPage()
expect(screen.getByRole('button', { name: /proceed/i })).toBeDisabled()
})
it('enables Proceed once the agent has replied AND a domain is named', async () => {
mockSayHi.mockResolvedValue('hello there')
connect()
useSession.getState().setDomain('structural stress')
renderPage()
const proceed = screen.getByRole('button', { name: /proceed/i })
expect(proceed).toBeDisabled()
fireEvent.click(screen.getByRole('button', { name: /say hi to your agent/i }))
await screen.findByTestId('node-reply')
expect(proceed).toBeEnabled()
expect(useSession.getState().stats.calls).toBe(0)
})
it('surfaces an error when the node does not answer', async () => {
mockSayHi.mockRejectedValue(new Error('your node did not answer — is it online?'))
connect()
renderPage()
fireEvent.click(screen.getByRole('button', { name: /say hi to your agent/i }))
expect(await screen.findByRole('alert')).toHaveTextContent(/did not answer/i)
})
})