Files
Jarvis/webview/packages/hud/tests/CameraButton.test.tsx
james.maesandClaude Opus 4.7 4159d44f4e feat(track-c 3): HUD camera button emits frameAttachRequested
Closes vision-audit §4 "HUD button emitter has zero matches". The bus
protocol declared `BusInbound.frameAttachRequested` and the Swift
inbound handler at App/AppDelegate.swift:1696-1698 routed it into
`FrameAttachController.requestAttach(reason: .hudButton)`, but no React
component dispatched the message. Now the user has a clickable camera
affordance next to the chat input.

Changes:
- webview/packages/hud/src/chat/CameraButton.tsx (new): stateless button
  that posts `{ type: 'frameAttachRequested' }` via window.jarvisBus.send.
- ChatInput.tsx: imports CameraButton and renders it in the toolbar row
  between the input field and the Send button.
- chat-panel.css: `.camera-button` styling, matches `.chat-input__send`
  chrome so the toolbar reads as one row.

Tests: +3 (CameraButton.test.tsx)
- CB1: renders accessible button (aria-label "Attach camera frame").
- CB2: click posts the BusInbound.frameAttachRequested message.
- CB3: payload shape is `{ type: 'frameAttachRequested' }` only —
  matches the no-associated-value Swift case in BusInbound.swift.

JS-side path: webview/packages/hud/src/chat/CameraButton.tsx:14
  → window.jarvisBus.send({ type: 'frameAttachRequested' })
Swift-side path: App/AppDelegate.swift:1696-1698
  → frameAttachController.requestAttach(reason: .hudButton)

HUD vitest 94 → 97. Pre-existing SegmentedRing typecheck warnings are
unrelated (verified via git stash on a clean tree).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 13:06:47 -05:00

79 lines
2.7 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { render, fireEvent } from '@testing-library/react'
import { CameraButton } from '../src/chat/CameraButton'
import { installJarvisBus } from '@jarvis/bus'
// Track-C 3 — closes vision-audit §4 "HUD button emitter has zero matches".
// The Swift inbound handler in App/AppDelegate.swift:1696-1698 routes
// `BusInbound.frameAttachRequested` into `FrameAttachController.requestAttach
// (reason: .hudButton)`. These tests assert the JS producer side.
type Postable = (body: unknown) => Promise<unknown>
function installWebkitMock() {
const postMessage = vi.fn<Postable>(async () => undefined)
;(window as unknown as {
webkit: { messageHandlers: { jarvisBus: { postMessage: Postable } } }
}).webkit = {
messageHandlers: { jarvisBus: { postMessage } },
}
return { postMessage }
}
function clearBus() {
delete (window as unknown as { jarvisBus?: unknown }).jarvisBus
}
function findInbound(
mock: ReturnType<typeof installWebkitMock>,
type: string,
): Record<string, unknown> | null {
for (const args of mock.postMessage.mock.calls) {
const body = args[0]
if (typeof body !== 'string') continue
try {
const parsed = JSON.parse(body) as Record<string, unknown>
if (parsed?.type === type) return parsed
} catch {
/* skip */
}
}
return null
}
describe('CameraButton (CB1-CB3) — closes vision-audit §4 HUD emitter gap', () => {
beforeEach(() => {
clearBus()
})
it('CB1: renders an accessible button', () => {
installWebkitMock()
installJarvisBus()
const { getByRole } = render(<CameraButton />)
const btn = getByRole('button', { name: /attach camera frame/i })
expect(btn).toBeDefined()
})
it('CB2: click posts BusInbound.frameAttachRequested', () => {
const mock = installWebkitMock()
installJarvisBus()
const { getByRole } = render(<CameraButton />)
fireEvent.click(getByRole('button', { name: /attach camera frame/i }))
const sent = findInbound(mock, 'frameAttachRequested')
expect(sent).not.toBeNull()
expect(sent?.type).toBe('frameAttachRequested')
})
it('CB3: the message body has no payload (cardinal frameAttachRequested shape)', () => {
const mock = installWebkitMock()
installJarvisBus()
const { getByRole } = render(<CameraButton />)
fireEvent.click(getByRole('button', { name: /attach camera frame/i }))
const sent = findInbound(mock, 'frameAttachRequested')
expect(sent).not.toBeNull()
// Only `type` — matches `case frameAttachRequested` (no associated value)
// in packages/Bus/Sources/Bus/BusInbound.swift.
expect(Object.keys(sent ?? {})).toEqual(['type'])
})
})