Files
hermes-ui/tests/e2e/voice-dialogue.spec.ts
T
toor 1e9ea89d86
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
2026-10-16 10:34:35 +08:00

194 lines
5.5 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { authenticate, mockChatSocket, mockHermesApi, TEST_ACCESS_KEY } from './fixtures'
declare global {
interface Window {
__PW_FAKE_VOICE_CAPTURE__?: {
getUserMediaCalls: number
recorderState: string
}
}
}
async function waitForRun(page: Page, index = 0) {
const handle = await page.waitForFunction((runIndex) => {
const state = (window as any).__PW_CHAT_SOCKET__
const runs = state?.emitted?.filter((item: any) => item.event === 'run') || []
const run = runs[runIndex]
return run ? run.payload : null
}, index)
return handle.jsonValue() as Promise<any>
}
async function installMockVoiceCapture(page: Page) {
await page.addInitScript(() => {
const state = {
getUserMediaCalls: 0,
recorderState: 'inactive',
}
Object.defineProperty(window, '__PW_FAKE_VOICE_CAPTURE__', {
configurable: true,
writable: true,
value: state,
})
class FakeMediaRecorder {
static isTypeSupported() {
return true
}
stream: unknown
mimeType: string
state = 'inactive'
ondataavailable: ((event: { data: Blob }) => void) | null = null
onerror: ((event: unknown) => void) | null = null
onstop: (() => void) | null = null
constructor(stream: unknown, options: { mimeType?: string } = {}) {
this.stream = stream
this.mimeType = options.mimeType || 'audio/webm'
}
start() {
this.state = 'recording'
state.recorderState = 'recording'
}
stop() {
if (this.state === 'inactive') {
return
}
this.state = 'inactive'
state.recorderState = 'inactive'
const blob = new Blob(['fake-voice-audio'], { type: this.mimeType })
setTimeout(() => {
if (this.ondataavailable) {
this.ondataavailable({ data: blob })
}
if (this.onstop) {
this.onstop()
}
}, 0)
}
}
const fakeTrack = {
kind: 'audio',
enabled: true,
readyState: 'live',
stop() {},
}
const fakeStream = {
active: true,
getTracks: () => [fakeTrack],
getAudioTracks: () => [fakeTrack],
getVideoTracks: () => [],
}
Object.defineProperty(window, 'MediaRecorder', {
configurable: true,
writable: true,
value: FakeMediaRecorder,
})
Object.defineProperty(globalThis, 'MediaRecorder', {
configurable: true,
writable: true,
value: FakeMediaRecorder,
})
const mediaDevices = navigator.mediaDevices || {}
Object.defineProperty(mediaDevices, 'getUserMedia', {
configurable: true,
writable: true,
value: async () => {
state.getUserMediaCalls += 1
return fakeStream
},
})
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: mediaDevices,
})
})
}
test('records, transcribes, stages editable text, then sends through the real chat UI', async ({ page }) => {
await installMockVoiceCapture(page)
await page.addInitScript(() => {
window.localStorage.setItem('hermes-stt-settings-v1', JSON.stringify({
provider: 'openai',
openaiModel: 'gpt-4o-transcribe',
openaiLanguage: '',
openaiPrompt: '',
customBaseUrl: '',
customModel: 'gpt-4o-transcribe',
customLanguage: '',
customPrompt: '',
}))
})
await authenticate(page, TEST_ACCESS_KEY, 'research')
const api = await mockHermesApi(page)
await mockChatSocket(page)
let transcriptionRequests = 0
await page.route('**/api/hermes/stt/transcribe', async (route) => {
transcriptionRequests += 1
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ text: 'hello by voice', provider: 'openai' }),
})
})
await page.goto('/#/hermes/chat')
await page.evaluate(() => {
const state = window.__PW_FAKE_VOICE_CAPTURE__
if (!state) {
return
}
const FakeMediaRecorder = window.MediaRecorder
globalThis.MediaRecorder = FakeMediaRecorder
if (navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia = async () => {
state.getUserMediaCalls += 1
const fakeMediaStream: any = {
active: true,
getTracks: () => [{ kind: 'audio', enabled: true, readyState: 'live', stop() {} }],
getAudioTracks: () => [{ kind: 'audio', enabled: true, readyState: 'live', stop() {} }],
getVideoTracks: () => [],
}
return fakeMediaStream
}
}
})
expect(await page.evaluate(() => window.__PW_FAKE_VOICE_CAPTURE__)).toEqual({
getUserMediaCalls: 0,
recorderState: 'inactive',
})
const toggle = page.getByTestId('voice-record-toggle')
await expect(toggle).toBeVisible()
await expect(toggle).toHaveAttribute('aria-pressed', 'false')
await toggle.click()
await expect(toggle).toHaveAttribute('aria-pressed', 'true')
await page.waitForFunction(() => window.__PW_FAKE_VOICE_CAPTURE__?.recorderState === 'recording')
await toggle.click()
await expect(page.locator('textarea')).toHaveValue('hello by voice')
expect(transcriptionRequests).toBe(1)
await expect(toggle).toHaveAttribute('aria-pressed', 'false')
await page.getByRole('button', { name: /send/i }).click()
const run = await waitForRun(page)
expect(run.input).toBe('hello by voice')
await expect(page.locator('p').filter({ hasText: /^hello by voice$/ })).toBeVisible()
expect(api.unexpectedRequests).toEqual([])
})