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
101 lines
4.9 KiB
TypeScript
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(
|
|
[],
|
|
);
|
|
});
|