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); await voice.getByRole("switch", { name: "Speaking on or off" }).click(); 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 preparedOff = await rpc<{ ready: boolean }>(page, "voice/prepare", { text: "Hello there.", }); expect(preparedOff.ready).toBe(false); 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("df_eva"); 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: "Bernd" })).toHaveCount(1); await expect(cards.filter({ hasText: "Heart" })).toHaveCount(0); await expect(voice.getByRole("button", { name: "Eva", exact: true })).toHaveAttribute( "aria-pressed", "true", ); await expect(voice.getByRole("button", { name: "Bernd 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"); });