evOo-local/apps/web/e2e/voice.spec.ts
Claude 0703d79b93
feat(web): aurora voice stage with live word-by-word caption
Replace the full-bleed cyan/violet/emerald bloom field with one dark glass
panel: the Oo, a live caption whose new words fade in, and a compact control
row (phase label, transcript, interrupt, hang up). An iridescent aurora rises
from the panel's bottom edge, follows mic energy while listening, drifts while
thinking and blooms while speaking. Its spectrum is the shared bot palette
rotated so the active Oo's color sits in the middle, so no new hex values.

Voice entry buttons drop the hardcoded cyan/amber styling for neutral tokens
plus a thin aurora rim when voice is ready. Dead voice CSS is removed and the
voice e2e now captures the stage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012G3dUMVDCDL3PwQE4WTQxL
2026-10-02 18:58:15 +00:00

101 lines
4.9 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, openUserSettings, rpc, signup } from "./helpers";
test("voice settings connect a key, speak a reply, and open a call", async ({ page }, testInfo) => {
const stamp = Date.now();
const userName = `Voice ${stamp}`;
await signup(page, `voice-${stamp}@rakazo.test`, "password12", userName);
await completeOnboarding(page);
await expect(page.getByRole("button", { name: "Call", exact: true })).toHaveCount(0);
await expect(
page.getByTestId("composer-bar").getByRole("button", { name: "Voice", exact: true }),
).toHaveCount(1);
await captureScreenshot(page, testInfo, "voice-composer");
await page
.getByTestId("composer-bar")
.getByRole("button", { name: "Voice", exact: true })
.click();
await expect(page.getByTestId("voice-settings")).toBeVisible();
await page.getByRole("button", { name: "Other voice providers" }).click();
await expect(page.getByLabel("API key", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Dictate", exact: true })).toHaveCount(0);
await captureScreenshot(page, testInfo, "voice-settings");
await page.getByRole("button", { name: "Close voice settings" }).click();
await expect(page.getByTestId("voice-settings")).toHaveCount(0);
const preparedOff = await rpc<{ ready: boolean }>(page, "voice/prepare", {
text: "Hello there.",
});
expect(preparedOff.ready).toBe(false);
await openUserSettings(page, "voice");
await expect(page.getByTestId("voice-settings")).toBeVisible();
await page.getByRole("button", { name: "Other voice providers" }).click();
await page.getByLabel("Provider", { exact: true }).selectOption("scripted");
const apiKeyInput = page.getByPlaceholder(/Paste your API key/);
await expect(apiKeyInput).toHaveAttribute("autocomplete", "new-password");
await apiKeyInput.fill("fake-scripted-voice-key");
await page.getByRole("button", { name: "Connect", exact: true }).click();
await expect(page.getByText("Connected Scripted.")).toBeVisible();
await expect(page.getByRole("button", { name: "Replace key" })).toBeVisible();
await expect(page.getByRole("button", { name: "Disconnect", exact: true })).toBeVisible();
await captureScreenshot(page, testInfo, "voice-settings-connected");
const credentials = await rpc<Array<{ hasKey: boolean; provider: string }>>(
page,
"voice/credentials",
{},
);
expect(credentials).toEqual([expect.objectContaining({ hasKey: true, provider: "scripted" })]);
expect(JSON.stringify(credentials)).not.toContain("fake-scripted-voice-key");
await page.getByRole("button", { name: "Close voice settings" }).click();
const composer = page.getByPlaceholder(/Message/);
await composer.fill("say hello");
await page.keyboard.press("Enter");
// A reply can render more than one text bubble; speak the latest one.
const speakReply = page.getByRole("button", { name: "Speak this reply" }).last();
await expect(speakReply).toBeVisible({
timeout: 30_000,
});
const replySpoken = page.waitForResponse(
(response) => response.url().includes("/api/voice/speak") && response.ok(),
);
await speakReply.click();
await replySpoken;
await openUserSettings(page, "voice");
await page.getByRole("button", { name: "Other voice providers" }).click();
await page.getByLabel("Provider", { exact: true }).selectOption("scripted");
await expect(page.getByRole("button", { name: "Replace key" })).toBeVisible();
await expect(page.getByRole("button", { name: "Disconnect", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Close voice settings" }).click();
await page
.getByTestId("composer-bar")
.getByRole("button", { name: "Voice", exact: true })
.click();
await expect(page.getByTestId("call-view")).toBeVisible();
await expect(page.getByTestId("voice-panel")).toBeVisible();
await expect(page.getByRole("button", { name: "Interrupt" })).toBeVisible();
await expect(page.getByRole("button", { name: "Hang up" })).toBeVisible();
await captureScreenshot(page, testInfo, "voice-stage");
await page.getByRole("button", { name: "Hang up" }).click();
await expect(page.getByTestId("call-view")).toHaveCount(0);
await openUserSettings(page, "voice");
const settings = page.getByTestId("voice-settings");
await settings.getByRole("button", { name: "Other voice providers" }).click();
await settings.getByPlaceholder(/Paste a replacement key/).fill("leftover-voice-key");
await settings.getByRole("button", { name: "Disconnect", exact: true }).click();
await expect(settings.getByRole("button", { name: "Disconnect", exact: true })).toHaveCount(0);
await expect(settings.getByRole("button", { name: "Connect", exact: true })).toBeVisible();
await expect(settings.getByLabel("API key", { exact: true })).toHaveValue("");
await expect(rpc<Array<{ provider: string }>>(page, "voice/credentials", {})).resolves.toEqual(
[],
);
});