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>
79 lines
2.7 KiB
TypeScript
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'])
|
|
})
|
|
})
|