evOo-local/apps/web/e2e/voice.spec.ts
cynarAI 8a98bfe79f
fix(adapters,sandboxes): tell agents where git works and make the refusal explicit (#223)
* fix(adapters,sandboxes): tell agents where git works and make the refusal explicit

Agents searched for the project repository in mounted host folders and read
the broker refusal as a missing broker. The prompt now names the folder where
git and gh work, and the refusal says to cd there instead of looking elsewhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

* test(web): wait for the automatic voice start before switching speaking on

Clicking the switch while the app was still turning the voice on by itself
could turn it off again, which failed the voice settings e2e twice in a row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 23:51:31 +02:00

152 lines
6.5 KiB
TypeScript

import { expect, type Page, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, openUserSettings, rpc, signup } from "./helpers";
async function turnOnSpeaking(page: Page) {
const settings = await openUserSettings(page, "voice");
const voice = settings.getByTestId("voice-settings");
await expect(voice).toBeVisible();
await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0);
await expect(voice.getByLabel("API key", { exact: true })).toHaveCount(0);
// The app turns the local voice on by itself the first time it starts.
const speaking = voice.getByRole("switch", { name: "Speaking on or off" });
// Clicking before the automatic start has settled would turn the voice off again.
const startedByItself = await expect(speaking)
.toHaveAttribute("aria-checked", "true", { timeout: 5_000 })
.then(
() => true,
() => false,
);
if (!startedByItself) await speaking.click();
await expect(speaking).toHaveAttribute("aria-checked", "true");
await expect(voice.getByTestId("voice-carousel")).toBeVisible();
return voice;
}
test("voice settings pick a voice from the carousel, speak a reply, and open a call", async ({
page,
}, testInfo) => {
const stamp = Date.now();
await signup(page, `voice-${stamp}@rakazo.test`, "password12", `Voice ${stamp}`);
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");
const voice = await turnOnSpeaking(page);
const cards = voice.getByTestId("voice-card");
// English UI lists only the English voices of the local server.
await expect(cards.filter({ hasText: "Heart" })).toHaveCount(1);
await expect(cards.filter({ hasText: "George" })).toHaveCount(1);
await expect(cards.filter({ hasText: "Eva" })).toHaveCount(0);
await expect(voice.getByRole("button", { name: "Heart", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
await voice.getByRole("button", { name: "Bella", exact: true }).click();
await expect(voice.getByRole("button", { name: "Bella", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
await expect(voice.getByRole("button", { name: "Heart", exact: true })).toHaveAttribute(
"aria-pressed",
"false",
);
await expect
.poll(async () => (await rpc<{ voiceId: string }>(page, "voice/status", {})).voiceId)
.toBe("af_bella");
const previewSpoken = page.waitForResponse(
(response) => response.url().includes("/api/voice/speak") && response.ok(),
);
await voice.getByRole("button", { name: "Play Sky", exact: true }).click();
await previewSpoken;
// A preview only plays the voice; the chosen one stays.
await expect(voice.getByRole("button", { name: "Bella", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
for (const colorScheme of ["light", "dark"] as const) {
await page.emulateMedia({ colorScheme });
await captureScreenshot(page, testInfo, `voice-settings-carousel-${colorScheme}`);
}
await page.emulateMedia({ colorScheme: "light" });
await page.getByRole("button", { name: "Close voice settings" }).click();
await expect(page.getByTestId("user-settings")).toHaveCount(0);
const composer = page.getByPlaceholder(/Message/);
await composer.fill("say hello");
await expect(
page.getByTestId("composer-bar").getByRole("button", { name: "Voice", exact: true }),
).toHaveCount(0);
await captureScreenshot(page, testInfo, "voice-composer-typing");
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 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: "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);
});
test("German lists only German voices and switches the saved voice to one", async ({
page,
}, testInfo) => {
const stamp = Date.now();
await signup(page, `voice-de-${stamp}@rakazo.test`, "password12", `Stimme ${stamp}`);
await completeOnboarding(page);
let voice = await turnOnSpeaking(page);
await expect(voice.getByRole("button", { name: "Heart", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
await page.getByRole("button", { name: "Close voice settings" }).click();
// Switching the app language moves the saved voice to that language too.
const settings = await openUserSettings(page);
await settings.getByTestId("ui-locale-select").click();
await settings.getByRole("option", { name: "Deutsch", exact: true }).click();
await expect(settings.getByRole("heading", { name: "Sprache", exact: true })).toBeVisible();
await expect
.poll(async () => (await rpc<{ voiceId: string }>(page, "voice/status", {})).voiceId)
.toBe("dm_thorsten");
await settings.getByTestId("settings-nav-voice").click();
voice = settings.getByTestId("voice-settings");
const cards = voice.getByTestId("voice-card");
await expect(cards.filter({ hasText: "Eva" })).toHaveCount(1);
await expect(cards.filter({ hasText: "Thorsten" })).toHaveCount(1);
await expect(cards.filter({ hasText: "Heart" })).toHaveCount(0);
await expect(voice.getByRole("button", { name: "Thorsten", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
await expect(voice.getByRole("button", { name: "Eva anhören", exact: true })).toBeVisible();
for (const colorScheme of ["light", "dark"] as const) {
await page.emulateMedia({ colorScheme });
await captureScreenshot(page, testInfo, `voice-settings-carousel-de-${colorScheme}`);
}
await page.emulateMedia({ colorScheme: "light" });
await page.setViewportSize({ width: 390, height: 844 });
await expect(voice.getByTestId("voice-carousel")).toBeVisible();
await captureScreenshot(page, testInfo, "voice-settings-carousel-de-phone");
});