Compare commits
No commits in common. "490e2fefe856ffc2eec466cd9dcf5fcfabb40e7f" and "a9a251b80ad01fa5ffcb4844cb412828ad5a5ab9" have entirely different histories.
490e2fefe8
...
a9a251b80a
@ -307,10 +307,6 @@ test("group chats share every context-menu action", async ({ page }, testInfo) =
|
||||
await group.click({ button: "right" });
|
||||
await captureScreenshot(page, testInfo, "group-context-menu-mobile");
|
||||
await page.keyboard.press("Escape");
|
||||
if ((await page.getByRole("menu").count()) > 0) {
|
||||
await page.keyboard.press("Escape");
|
||||
}
|
||||
await expect(page.getByRole("menu")).toHaveCount(0);
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
|
||||
await group.click({ button: "right" });
|
||||
|
||||
@ -5,15 +5,12 @@ test("onboarding names the first Oo and shows it in chat", async ({ page }) => {
|
||||
const stamp = Date.now();
|
||||
await signup(page, `name-oo-${stamp}@rakazo.test`, "password12", "Namer");
|
||||
await page.waitForURL(/\/onboarding/, { timeout: 20_000 });
|
||||
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
|
||||
await page.getByTestId("onboarding-oo-name-input").fill("Luna");
|
||||
await page
|
||||
.getByTestId("onboarding-name-step")
|
||||
.getByRole("button", { name: /^(Continue|Weiter)$/i })
|
||||
.click();
|
||||
await page.getByRole("button", { name: /^(Skip|Überspringen ·)/i }).click();
|
||||
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
|
||||
await page.getByTestId("onboarding-name-step").getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: /Skip/i }).click();
|
||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/app\/g\/[^/]+/, { timeout: 30_000 });
|
||||
await expect(
|
||||
page.getByTestId("bot-settings-trigger").getByText("Luna", { exact: true }),
|
||||
@ -28,8 +25,8 @@ test("German UI keeps Deutsch on language skip label", async ({ page }) => {
|
||||
});
|
||||
const stamp = Date.now();
|
||||
await signup(page, `de-skip-${stamp}@rakazo.test`, "password12", "Deutsch");
|
||||
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /Wähle deine Sprache|Choose your language/i }),
|
||||
).toBeVisible();
|
||||
@ -43,16 +40,15 @@ test("German voice step lists German voices after choosing Deutsch", async ({ pa
|
||||
});
|
||||
const stamp = Date.now();
|
||||
await signup(page, `de-voice-${stamp}@rakazo.test`, "password12", "Stimme");
|
||||
await page.getByRole("button", { name: /^(Continue|Weiter)$/i }).click();
|
||||
await page.getByRole("button", { name: /^(Skip|Überspringen)$/i }).click();
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Deutsch", exact: true }).click();
|
||||
await expect(page.getByText(/Loading voices|Stimmen werden geladen/i)).toHaveCount(0, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.getByTestId("onboarding-voice-df_eva")).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId("onboarding-voice-dm_bernd")).toBeVisible();
|
||||
await expect(page.getByTestId("onboarding-voice-af_alloy")).toHaveCount(0);
|
||||
await expect(page.getByTestId("onboarding-voice-df_eva")).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(page.getByRole("button", { name: "Eva", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Alloy", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Eva", exact: true })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
test("main conversation does not show a group subtitle for one Oo", async ({ page }) => {
|
||||
|
||||
@ -87,14 +87,10 @@ async function visible(locator: Locator, timeout: number) {
|
||||
return locator.isVisible({ timeout }).catch(() => false);
|
||||
}
|
||||
|
||||
const onboardingContinueButton = /^(Continue|Weiter)$/i;
|
||||
const onboardingSkipButton = /^(Skip|Überspringen)$/i;
|
||||
const onboardingLanguageSkipButton = /^(Skip|Überspringen) ·/i;
|
||||
|
||||
/** After the look picker, skip the optional Oo naming step (keeps the default name). */
|
||||
export async function skipOnboardingNameStep(page: Page) {
|
||||
await expect(page.getByTestId("onboarding-name-step")).toBeVisible();
|
||||
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,15 +113,11 @@ export async function completeOnboarding(
|
||||
);
|
||||
}
|
||||
|
||||
const design = page.getByRole("heading", { name: /Design your Oo|Gestalte dein Oo/i });
|
||||
const design = page.getByRole("heading", { name: /Design your Oo/i });
|
||||
const naming = page.getByTestId("onboarding-name-step");
|
||||
const language = page.getByRole("heading", {
|
||||
name: /Choose your language|Wähle deine Sprache/i,
|
||||
});
|
||||
const voice = page.getByRole("heading", { name: /Pick a voice|Wähle eine Stimme/i });
|
||||
const integrations = page.getByRole("heading", {
|
||||
name: /Server integrations|Server-Integrationen/i,
|
||||
});
|
||||
const language = page.getByRole("heading", { name: /Choose your language/i });
|
||||
const voice = page.getByRole("heading", { name: /Pick a voice/i });
|
||||
const integrations = page.getByRole("heading", { name: "Server integrations", exact: true });
|
||||
const expectedOoName = options.ooName ?? "Chief";
|
||||
const chief = page.getByText(expectedOoName).first();
|
||||
|
||||
@ -135,30 +127,30 @@ export async function completeOnboarding(
|
||||
await expect(entry.first()).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
if (await design.isVisible()) {
|
||||
await page.getByRole("button", { name: onboardingContinueButton }).click();
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(naming).toBeVisible();
|
||||
if (options.ooName) {
|
||||
await page.getByTestId("onboarding-oo-name-input").fill(options.ooName);
|
||||
await naming.getByRole("button", { name: onboardingContinueButton }).click();
|
||||
await naming.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
} else {
|
||||
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||
}
|
||||
await expect(language).toBeVisible();
|
||||
await page.getByRole("button", { name: onboardingLanguageSkipButton }).click();
|
||||
await page.getByRole("button", { name: /Skip/i }).click();
|
||||
await expect(voice).toBeVisible();
|
||||
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||
}
|
||||
|
||||
if (await visible(integrations, 8_000)) {
|
||||
if (testInfo) await captureScreenshot(page, testInfo, "02-connect-apps");
|
||||
await page.getByRole("button", { name: onboardingSkipButton }).click();
|
||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||
}
|
||||
|
||||
// The scan step, when shown, either asks first (skip it) or advances by itself. Onboarding
|
||||
// then lands in the main conversation.
|
||||
const skipScan = page
|
||||
.locator('[data-onboarding-step="scan"]')
|
||||
.getByRole("button", { name: onboardingSkipButton });
|
||||
.getByRole("button", { name: "Skip", exact: true });
|
||||
await expect(async () => {
|
||||
if (await skipScan.isVisible()) await skipScan.click();
|
||||
expect(page.url()).toMatch(/\/app\/g\/[^/]+$/);
|
||||
@ -194,17 +186,15 @@ export async function signup(
|
||||
) {
|
||||
await expect(async () => {
|
||||
await page.goto("/sign-up", { waitUntil: "domcontentloaded" });
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /Create your evOo|Dein evOo erstellen/i }),
|
||||
).toBeVisible({
|
||||
await expect(page.getByRole("heading", { name: "Create your evOo" })).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
}).toPass({ timeout: 45_000 });
|
||||
if (testInfo) await captureScreenshot(page, testInfo, "01-sign-up");
|
||||
await page.getByPlaceholder(/Your name|Dein Name/i).fill(name);
|
||||
await page.getByPlaceholder(/Your email address|Deine E-Mail-Adresse/i).fill(email);
|
||||
await page.getByPlaceholder(/^Password$|^Passwort$/i).fill(password);
|
||||
await page.getByRole("button", { name: /Create account|Konto erstellen/i }).click();
|
||||
await page.getByPlaceholder("Your name").fill(name);
|
||||
await page.getByPlaceholder("Your email address").fill(email);
|
||||
await page.getByPlaceholder("Password").fill(password);
|
||||
await page.getByRole("button", { name: "Create account" }).click();
|
||||
}
|
||||
|
||||
export async function captureScreenshot(page: Page, testInfo: TestInfo, name: string) {
|
||||
@ -299,7 +289,7 @@ export async function waitForChatComposer(page: Page) {
|
||||
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.getByRole("combobox", { name: /^Message$|^Nachricht$/i })).toBeVisible();
|
||||
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||
}
|
||||
|
||||
/** Create a named bot via RPC for test setup (skips the + picker). */
|
||||
|
||||
@ -84,21 +84,13 @@ test("Heute shows the coach's brief, habits and goals, checks habits off and set
|
||||
await expect(page.getByTestId(`heute-habit-streak-${habit.id}`)).toHaveText("3 days");
|
||||
await captureScreenshot(page, testInfo, "heute-coach");
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await page.reload();
|
||||
await expect(page).toHaveURL(/\/app\/heute$/);
|
||||
await expect(page.getByTestId("heute-brief")).toBeVisible();
|
||||
await expect(page.getByTestId(`heute-habit-${habit.id}`).getByRole("checkbox")).toBeChecked();
|
||||
|
||||
// Check-in times live behind one compact control.
|
||||
const rhythm = page.getByTestId("heute-rhythm");
|
||||
await expect(page.getByTestId("heute-coach-brief")).toBeVisible();
|
||||
await rhythm.scrollIntoViewIfNeeded();
|
||||
await expect(rhythm).toBeVisible({ timeout: 30_000 });
|
||||
const rhythmLoaded = page.waitForResponse(
|
||||
(response) => response.url().includes("/rpc/coach/rhythm/get") && response.ok(),
|
||||
);
|
||||
await rhythm.click();
|
||||
await rhythmLoaded;
|
||||
await page.getByTestId("heute-rhythm").click();
|
||||
const panel = page.getByTestId("heute-rhythm-panel");
|
||||
await expect(panel).toBeVisible();
|
||||
const morning = panel.getByTestId("heute-rhythm-morning");
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import type { VoiceInfo } from "@rakazo/contracts";
|
||||
import { voicesForLocale } from "@rakazo/core";
|
||||
import { Button } from "@rakazo/ui-web";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
defaultOnboardingVoiceId,
|
||||
offlineOnboardingVoices,
|
||||
onboardingVoicesFromList,
|
||||
} from "../../lib/onboarding-voices";
|
||||
import { defaultOnboardingVoiceId, offlineOnboardingVoices } from "../../lib/onboarding-voices";
|
||||
import { rpc } from "../../lib/rpc";
|
||||
import type { UiLocale } from "../../lib/ui-locale";
|
||||
|
||||
@ -42,7 +39,7 @@ export function OnboardingVoiceStep({
|
||||
await rpc.voice.connect({ provider: "kokoro", apiKey: KOKORO_LOCAL_TOKEN });
|
||||
const listed = await rpc.voice.voices({ provider: "kokoro" });
|
||||
if (cancelled) return;
|
||||
const ordered = onboardingVoicesFromList(listed, uiLocale);
|
||||
const ordered = voicesForLocale(listed, uiLocale).slice(0, 6);
|
||||
setVoiceOptions(ordered);
|
||||
const nextId = defaultOnboardingVoiceId(uiLocale, ordered);
|
||||
if (nextId) onSelectedVoiceIdChange(nextId);
|
||||
@ -92,7 +89,6 @@ export function OnboardingVoiceStep({
|
||||
<button
|
||||
key={voice.id}
|
||||
type="button"
|
||||
data-testid={`onboarding-voice-${voice.id}`}
|
||||
onClick={() => onSelectedVoiceIdChange(voice.id)}
|
||||
aria-pressed={selectedVoiceId === voice.id}
|
||||
className={`rounded-xl border-2 px-3 py-3 text-left text-sm font-medium transition-all ${
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
defaultOnboardingVoiceId,
|
||||
offlineOnboardingVoices,
|
||||
onboardingVoicesFromList,
|
||||
} from "./onboarding-voices.js";
|
||||
import { defaultOnboardingVoiceId, offlineOnboardingVoices } from "./onboarding-voices.js";
|
||||
|
||||
describe("offlineOnboardingVoices", () => {
|
||||
it("offers German voices for de", () => {
|
||||
@ -22,18 +18,6 @@ describe("offlineOnboardingVoices", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("onboardingVoicesFromList", () => {
|
||||
it("uses offline German presets when the server only lists English voices", () => {
|
||||
const englishOnly = [{ id: "af_alloy", label: "Alloy", description: "neutral" }];
|
||||
expect(onboardingVoicesFromList(englishOnly, "de").map((voice) => voice.id)).toEqual([
|
||||
"df_eva",
|
||||
"dm_bernd",
|
||||
"df_victoria",
|
||||
"dm_martin",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultOnboardingVoiceId", () => {
|
||||
it("prefers df_eva for German", () => {
|
||||
expect(defaultOnboardingVoiceId("de", offlineOnboardingVoices("de"))).toBe("df_eva");
|
||||
|
||||
@ -13,41 +13,11 @@ const ENGLISH_ONBOARDING_VOICES: VoiceInfo[] = [
|
||||
|
||||
const OFFLINE_CATALOG = [...GERMAN_VOICE_PRESETS, ...ENGLISH_ONBOARDING_VOICES];
|
||||
|
||||
const KOKORO_LANGUAGE_PREFIXES: Record<string, string> = {
|
||||
de: "d",
|
||||
en: "ab",
|
||||
es: "e",
|
||||
fr: "f",
|
||||
hi: "h",
|
||||
it: "i",
|
||||
ja: "j",
|
||||
pt: "p",
|
||||
zh: "z",
|
||||
};
|
||||
|
||||
function listedVoicesSpeakLocale(listed: VoiceInfo[], locale: string): boolean {
|
||||
const language = locale.toLowerCase().split("-")[0] ?? "";
|
||||
const prefixes = KOKORO_LANGUAGE_PREFIXES[language];
|
||||
if (!prefixes) return listed.length > 0;
|
||||
return listed.some((voice) => {
|
||||
const prefix = /^([a-z])[fm]_/.exec(voice.id)?.[1];
|
||||
return Boolean(prefix && prefixes.includes(prefix));
|
||||
});
|
||||
}
|
||||
|
||||
/** Locale-aware voice choices for onboarding when listing from the server fails. */
|
||||
export function offlineOnboardingVoices(locale: string): VoiceInfo[] {
|
||||
return voicesForLocale(OFFLINE_CATALOG, locale).slice(0, 6);
|
||||
}
|
||||
|
||||
/** Prefer server voices that speak the UI language; otherwise use offline presets. */
|
||||
export function onboardingVoicesFromList(listed: VoiceInfo[], locale: string): VoiceInfo[] {
|
||||
if (listedVoicesSpeakLocale(listed, locale)) {
|
||||
return voicesForLocale(listed, locale).slice(0, 6);
|
||||
}
|
||||
return offlineOnboardingVoices(locale);
|
||||
}
|
||||
|
||||
export function defaultOnboardingVoiceId(locale: string, voices: VoiceInfo[]): string {
|
||||
return defaultVoiceForLocale(voices, locale)?.id ?? "";
|
||||
}
|
||||
|
||||
@ -273,11 +273,6 @@ export function ShellPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const onHeuteRoute = isHeuteAppPath(location.pathname);
|
||||
useEffect(() => {
|
||||
if (botId === "heute") {
|
||||
navigate(HEUTE_APP_PATH, { replace: true });
|
||||
}
|
||||
}, [botId, navigate]);
|
||||
const [activeNavPillar, setActiveNavPillar] = useState<NavPillar>(() => {
|
||||
if (isHeuteAppPath(location.pathname)) return "heute";
|
||||
if (groupId) return "projekte";
|
||||
@ -625,11 +620,8 @@ export function ShellPage() {
|
||||
const recordingSkill = activeTaughtSkills.find((skill) => skill.status === "recording") ?? null;
|
||||
const routeBotId = useRef<string | undefined>(botId);
|
||||
routeBotId.current = botId;
|
||||
const routePathname = useRef(location.pathname);
|
||||
routePathname.current = location.pathname;
|
||||
const routeOnHeute = useRef(onHeuteRoute);
|
||||
routeOnHeute.current = onHeuteRoute;
|
||||
const holdingHeuteRoute = () => routeOnHeute.current || isHeuteAppPath(routePathname.current);
|
||||
const routeGroupId = useRef<string | undefined>(groupId);
|
||||
routeGroupId.current = groupId;
|
||||
const activeBotId = useRef<string | undefined>(inGroup ? undefined : active?.id);
|
||||
@ -764,7 +756,7 @@ export function ShellPage() {
|
||||
return;
|
||||
}
|
||||
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
||||
if (holdingHeuteRoute()) return;
|
||||
if (routeOnHeute.current) return;
|
||||
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
|
||||
}
|
||||
} finally {
|
||||
@ -1012,11 +1004,9 @@ export function ShellPage() {
|
||||
return;
|
||||
}
|
||||
if (inProject && !botId) {
|
||||
if (holdingHeuteRoute()) return;
|
||||
navigate(firstThreadRoute(bootstrap.bots, groupList, true), { replace: true });
|
||||
return;
|
||||
}
|
||||
if (holdingHeuteRoute()) return;
|
||||
const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id;
|
||||
if (selectedBotId && selectedBotId !== botId) {
|
||||
navigate(`/app/${selectedBotId}`, { replace: true });
|
||||
|
||||
@ -22,13 +22,14 @@ export function useMobileSidebar() {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key !== "Escape" || window.matchMedia("(min-width: 768px)").matches) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setMobileSidebarOpen(false);
|
||||
queueMicrotask(() => {
|
||||
document.querySelector<HTMLElement>('[data-testid="open-navigation"]')?.focus();
|
||||
});
|
||||
}
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("keydown", onKeyDown, true);
|
||||
return () => window.removeEventListener("keydown", onKeyDown, true);
|
||||
}, [mobileSidebarOpen]);
|
||||
|
||||
function onMobileSidebarTouchStart(event: React.TouchEvent<HTMLDivElement>) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user