diff --git a/apps/web/e2e/auth-lifecycle.spec.ts b/apps/web/e2e/auth-lifecycle.spec.ts index 7221059..3ef6599 100644 --- a/apps/web/e2e/auth-lifecycle.spec.ts +++ b/apps/web/e2e/auth-lifecycle.spec.ts @@ -48,7 +48,7 @@ test("logout protects bot deep links and sign-in restores the session", async ({ await page.waitForURL(/\/app\/[^/]+$/); const protectedBotPath = new URL(page.url()).pathname; - await expect(page.getByPlaceholder("Message Chief")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await page.getByRole("button", { name: new RegExp(userName, "i") }).click(); await expect(page.getByRole("button", { name: "Settings", exact: true })).toBeVisible(); diff --git a/apps/web/e2e/bot-crud.spec.ts b/apps/web/e2e/bot-crud.spec.ts index c7d612c..0780649 100644 --- a/apps/web/e2e/bot-crud.spec.ts +++ b/apps/web/e2e/bot-crud.spec.ts @@ -33,7 +33,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = await form.getByRole("button", { name: "Create", exact: true }).click(); await createAborted; // Failed create keeps the form open on the current bot chat. - await expect(page.getByPlaceholder("Message Chief")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create"); await expect(page.getByTestId("create-bot-error")).toBeVisible(); expect(createFailed).toBe(true); @@ -51,7 +51,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = await route.abort("failed"); }); await createBotFromPicker(page); - await expect(page.getByPlaceholder("Message New Bot")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect.poll(() => failedPostCreateRefresh).toBe(true); await page.unroute("**/rpc/spaces/list"); await expect(botList.getByRole("button", { name: /^New Bot/ })).toBeVisible(); @@ -82,7 +82,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = await expect(studio).toBeHidden(); await page.getByRole("button", { name: "Save", exact: true }).click(); await expect(botList.getByRole("button", { name: /^Researcher/ })).toBeVisible(); - await expect(page.getByPlaceholder("Message Researcher")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await page.locator("main").getByRole("button", { name: "Researcher", exact: true }).click(); await expect(nameInput).toHaveValue("Researcher"); @@ -136,12 +136,12 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = await descriptionInput.fill("Builds durable, source-backed research briefs."); await page.getByRole("button", { name: "Save", exact: true }).click(); await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible(); - await expect(page.getByPlaceholder("Message Atlas")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await captureScreenshot(page, testInfo, "28-edited-bot-profile"); await page.reload(); await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible(); - await expect(page.getByPlaceholder("Message Atlas")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await page.locator("main").getByRole("button", { name: "Atlas", exact: true }).click(); await expect(nameInput).toHaveValue("Atlas"); await expect(titleInput).toHaveValue("Research lead"); @@ -157,7 +157,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = name: new RegExp(`^${longUnbrokenName}`), }); await expect(longNameBot).toBeVisible(); - await expect(page.getByPlaceholder(`Message ${longUnbrokenName}`)).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await longNameBot.click({ button: "right" }); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -176,6 +176,6 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = await page.waitForURL((url) => url.pathname !== deletedBotPath); await expect(botList.getByText(longUnbrokenName, { exact: true })).toHaveCount(0); await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible(); - await expect(page.getByPlaceholder("Message Chief")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await captureScreenshot(page, testInfo, "31-deleted-bot-fallback"); }); diff --git a/apps/web/e2e/golden.spec.ts b/apps/web/e2e/golden.spec.ts index 91fe500..01215fb 100644 --- a/apps/web/e2e/golden.spec.ts +++ b/apps/web/e2e/golden.spec.ts @@ -368,7 +368,7 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) => await expect(page.getByTestId("composer-steering-status")).toHaveCount(0); await expect(page.getByText("Messages sent now guide the next turn.")).toHaveCount(0); await expect(page.getByText(/^Steer /)).toHaveCount(0); - await expect(composer).toHaveAttribute("placeholder", "Message Chief"); + await expect(composer).toHaveAttribute("placeholder", "Message…"); await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible(); await composer.fill("Use the newer report and keep the answer short."); await page.keyboard.press("Tab"); @@ -386,7 +386,7 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) => await page.setViewportSize({ width: 390, height: 844 }); await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible(); - await expect(composer).toHaveAttribute("placeholder", "Message Chief"); + await expect(composer).toHaveAttribute("placeholder", "Message…"); await captureScreenshot(page, testInfo, "14-active-bot-work-mobile"); await page.setViewportSize({ width: 1280, height: 720 }); expect(browserErrors).toEqual([]); diff --git a/apps/web/e2e/helpers.ts b/apps/web/e2e/helpers.ts index 5e57f49..1142e0b 100644 --- a/apps/web/e2e/helpers.ts +++ b/apps/web/e2e/helpers.ts @@ -178,6 +178,6 @@ export async function createNamedBot( computerMode: options.computerMode ?? "team", }); await page.goto(`/app/${bot.id}`); - await expect(page.getByPlaceholder(`Message ${name}`)).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); return bot.id; } diff --git a/apps/web/e2e/new-bot-ux.spec.ts b/apps/web/e2e/new-bot-ux.spec.ts index 9aec67f..046a7d6 100644 --- a/apps/web/e2e/new-bot-ux.spec.ts +++ b/apps/web/e2e/new-bot-ux.spec.ts @@ -40,7 +40,7 @@ test("create opens form, then empty chat; picker lists bots; sidebar collapses", await form.locator("label:has-text('Name') input").fill("New Bot"); await form.getByRole("button", { name: "Create", exact: true }).click(); await page.waitForURL(/\/app\/[^/]+$/); - await expect(page.getByPlaceholder("Message New Bot")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed"); await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0); await captureScreenshot(page, testInfo, "create-chat-sidepanel-closed"); @@ -118,7 +118,7 @@ test("later bot waits before showing the focus card; sending cancels it", async await page.clock.install(); await createBotFromPicker(page); - await expect(page.getByPlaceholder("Message New Bot")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0); await page.clock.fastForward(9_000); @@ -150,7 +150,7 @@ test("plus picker can create a Private computer bot", async ({ page }, testInfo) await page.waitForURL(/\/app\/[^/]+$/); await createBotFromPicker(page, { computerMode: "dedicated" }); - await expect(page.getByPlaceholder("Message New Bot")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await captureScreenshot(page, testInfo, "create-private-computer-bot"); const botId = page.url().split("/").pop()!; @@ -179,7 +179,7 @@ test("second bot from plus opens create form before persist", async ({ page }, t await form.getByRole("button", { name: "Create", exact: true }).click(); await create; await page.waitForURL(/\/app\/[^/]+$/); - await expect(page.getByPlaceholder("Message Researcher")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed"); await captureScreenshot(page, testInfo, "second-bot-created"); }); diff --git a/apps/web/e2e/onboarding-conversation.spec.ts b/apps/web/e2e/onboarding-conversation.spec.ts index 60f52dd..3639d9a 100644 --- a/apps/web/e2e/onboarding-conversation.spec.ts +++ b/apps/web/e2e/onboarding-conversation.spec.ts @@ -27,7 +27,7 @@ test("focus choice suggests apps and preserves a completed connection", async ({ // The focus step suggests apps but must not rename the bot: the name the // user chose during creation ("Chief") is preserved. await expect(page.locator("main").getByText("Chief", { exact: true })).toBeVisible(); - await expect(page.getByPlaceholder("Message Chief")).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); await expect(page.getByText("Slack", { exact: true })).toBeVisible(); await expect(page.getByText("Gmail", { exact: true })).toBeVisible(); const connectionCards = page.getByRole("group", { name: / connection$/ }); diff --git a/apps/web/e2e/spaces.spec.ts b/apps/web/e2e/spaces.spec.ts index 06b560a..9af9000 100644 --- a/apps/web/e2e/spaces.spec.ts +++ b/apps/web/e2e/spaces.spec.ts @@ -21,9 +21,10 @@ test("Projekte + opens new project dialog instead of group form", async ({ page await dialog.getByRole("button", { name: "Create project", exact: true }).click(); await expect(page).not.toHaveURL(/\/onboarding/); await page.waitForURL(/\/app/); - await expect(sidebar.getByText("Customer project", { exact: true })).toBeVisible({ + await expect(projekte.getByText("Customer project", { exact: true })).toBeVisible({ timeout: 15_000, }); + await expect(page.locator(".sidebar-unterhaltungen").getByText("Customer project")).toHaveCount(0); await expect( page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }), ).toBeVisible(); @@ -37,6 +38,8 @@ test("spaces stay invisible by default and chat creation requires approval", asy await completeOnboarding(page); const sidebar = page.locator("aside").first(); + const personalSpaceId = await page.evaluate(() => window.localStorage.getItem("rakazo:space-id")); + expect(personalSpaceId).toBeTruthy(); await expect(sidebar.getByText("Personal", { exact: true })).toHaveCount(0); await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1); await captureScreenshot(page, testInfo, "single-space-sidebar"); @@ -48,7 +51,7 @@ test("spaces stay invisible by default and chat creation requires approval", asy await captureScreenshot(page, testInfo, "new-space-dialog"); await dialog.getByRole("button", { name: "Cancel" }).click(); - const composer = page.getByRole("combobox", { name: "Message Chief" }); + const composer = page.getByRole("combobox", { name: "Message" }); await composer.fill("Create a space named Customer support"); await composer.press("Enter"); await expect(page.getByRole("button", { name: "Create space", exact: true })).toBeVisible({ @@ -61,16 +64,19 @@ test("spaces stay invisible by default and chat creation requires approval", asy await page.getByRole("button", { name: "Create space", exact: true }).click(); await expect(page.getByText("Created", { exact: true })).toBeVisible(); - await expect(sidebar.getByText("Personal", { exact: true })).toBeVisible(); - await expect(sidebar.getByText("Customer support", { exact: true })).toBeVisible({ + const projekte = page.locator(".sidebar-projekte"); + const unterhaltungen = page.locator(".sidebar-unterhaltungen"); + await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible({ timeout: 15_000, }); + await expect(unterhaltungen.getByText("Customer support", { exact: true })).toHaveCount(0); + await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toHaveCount(1); - const supportSpace = sidebar - .locator('[data-sidebar-group^="space:"]') - .filter({ hasText: "Customer support" }); + const supportSpace = projekte.locator('[data-sidebar-group^="project-space:"]').filter({ + hasText: "Customer support", + }); const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group"); - const supportSpaceId = supportSpaceGroup?.split(":")[1]; + const supportSpaceId = supportSpaceGroup?.split(":")[2]; expect(supportSpaceId).toBeTruthy(); await supportSpace.getByRole("button", { name: "Open Customer support" }).click(); await page.waitForURL(/\/(onboarding|app)/); @@ -79,23 +85,20 @@ test("spaces stay invisible by default and chat creation requires approval", asy .toBe(supportSpaceId); await completeOnboarding(page); - await expect(sidebar.getByText("Personal", { exact: true })).toBeVisible(); - await expect(sidebar.getByText("Customer support", { exact: true })).toBeVisible(); - await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(2); + await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible(); + await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toHaveCount(1); + await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); await captureScreenshot(page, testInfo, "spaces-sidebar"); - const personalSpace = sidebar - .locator('[data-sidebar-group^="space:"]') - .filter({ hasText: "Personal" }); - const personalSpaceGroup = await personalSpace.getAttribute("data-sidebar-group"); - const personalSpaceId = personalSpaceGroup?.split(":")[1]; - expect(personalSpaceId).toBeTruthy(); - await personalSpace.getByRole("button", { name: /^Chief/ }).click(); + await page.evaluate((id) => { + window.localStorage.setItem("rakazo:space-id", id!); + }, personalSpaceId); + await page.reload(); await page.waitForURL(/\/app\/[^/]+$/); await expect .poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id"))) .toBe(personalSpaceId); - await expect(sidebar.getByText("Customer support", { exact: true })).toBeVisible(); + await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible(); }); test("a new space auto-completes onboarding and can be deleted from its menu", async ({ @@ -114,27 +117,24 @@ test("a new space auto-completes onboarding and can be deleted from its menu", a // Model is already connected, so onboarding skips the form and lands in Chief. await completeOnboarding(page); await expect(page).toHaveURL(/\/app\//); - await expect(sidebar.getByText("Temporary", { exact: true })).toBeVisible(); - const temporarySpace = sidebar - .locator('[data-sidebar-group^="space:"]') - .filter({ hasText: "Temporary" }); - await expect(temporarySpace.getByRole("button", { name: /^Chief/ })).toBeVisible(); - await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(2); + const projekte = page.locator(".sidebar-projekte"); + await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible(); + await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1); await captureScreenshot(page, testInfo, "new-space-after-onboarding"); // Spaces can only be deleted when empty — remove auto-created Chief first. - await temporarySpace.getByRole("button", { name: /^Chief/ }).click({ button: "right" }); + await sidebar.getByRole("button", { name: /^Chief/ }).click({ button: "right" }); await page.getByRole("menuitem", { name: "Delete" }).click(); const deleteBotDialog = page.getByRole("alertdialog", { name: /Delete Chief/ }); await expect(deleteBotDialog).toBeVisible(); await deleteBotDialog.getByRole("button", { name: "Delete", exact: true }).click(); - await expect(temporarySpace.getByRole("button", { name: /^Chief/ })).toHaveCount(0); + await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(0); // The emptied space stays selectable instead of trapping the user. - await expect(sidebar.getByRole("button", { name: "Open Temporary" })).toBeVisible(); + await expect(projekte.getByRole("button", { name: "Open Temporary" })).toBeVisible(); // Deletion is discoverable through the visible Space actions menu. - await sidebar.getByRole("button", { name: "Actions for Temporary" }).click(); + await projekte.getByRole("button", { name: "Actions for Temporary" }).click(); await captureScreenshot(page, testInfo, "space-actions-menu"); await page.getByRole("menuitem", { name: "Delete space" }).click(); const deleteSpaceDialog = page.getByRole("alertdialog", { name: "Delete Temporary?" }); @@ -169,14 +169,12 @@ test("deleting the last bot in a space stays in the app after first use", async await dialog.getByRole("button", { name: "Create space", exact: true }).click(); await page.waitForURL(/\/(onboarding|app)/); await completeOnboarding(page); - await expect(sidebar.getByText("Side", { exact: true })).toBeVisible(); + const projekte = page.locator(".sidebar-projekte"); + await expect(projekte.getByText("Side", { exact: true })).toBeVisible(); // Delete the only bot in the new space: the app must stay put instead of // forcing onboarding again. - const sideBot = sidebar - .locator('[data-sidebar-group^="space:"]') - .filter({ hasText: "Side" }) - .getByRole("button", { name: /^Chief/ }); + const sideBot = sidebar.getByRole("button", { name: /^Chief/ }); await sideBot.click({ button: "right" }); await page.getByRole("menuitem", { name: "Delete" }).click(); const deleteDialog = page.getByRole("alertdialog", { name: /Delete Chief/ }); @@ -186,12 +184,12 @@ test("deleting the last bot in a space stays in the app after first use", async await expect(page).not.toHaveURL(/\/onboarding/); await page.reload(); await expect(page).not.toHaveURL(/\/onboarding/); - await expect(sidebar.getByRole("button", { name: "Open Side" })).toBeVisible(); + await expect(projekte.getByRole("button", { name: "Open Side" })).toBeVisible(); await expect( page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }), ).toBeVisible(); - await sidebar.getByRole("button", { name: "Actions for Side" }).click(); + await projekte.getByRole("button", { name: "Actions for Side" }).click(); await page.getByRole("menuitem", { name: "Delete space" }).click(); await page .getByRole("alertdialog", { name: "Delete Side?" }) diff --git a/apps/web/e2e/team-computer.spec.ts b/apps/web/e2e/team-computer.spec.ts index 9df2d9d..b6c09af 100644 --- a/apps/web/e2e/team-computer.spec.ts +++ b/apps/web/e2e/team-computer.spec.ts @@ -264,7 +264,7 @@ async function openBot(page: Page, name: string) { .getByRole("complementary") .getByRole("button", { name: new RegExp(`^${name}`) }) .click(); - await expect(page.getByPlaceholder(`Message ${name}`)).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); } async function openComputerPanel(page: Page) { diff --git a/apps/web/e2e/unterhaltungen-sidebar.spec.ts b/apps/web/e2e/unterhaltungen-sidebar.spec.ts new file mode 100644 index 0000000..7cb9f26 --- /dev/null +++ b/apps/web/e2e/unterhaltungen-sidebar.spec.ts @@ -0,0 +1,59 @@ +import { expect, test } from "@playwright/test"; +import { completeOnboarding, openNewBot, rpc, signup } from "./helpers"; + +test("Unterhaltungen and Projekte stay separate; + opens bot picker", async ({ page }) => { + const stamp = Date.now(); + await signup(page, `unterhaltungen-${stamp}@rakazo.test`, "password12", "Unterhaltungen UX"); + await completeOnboarding(page); + await page.goto("/app"); + await page.waitForURL(/\/app\/[^/]+$/); + + const sidebar = page.locator("aside").first(); + const unterhaltungen = page.locator(".sidebar-unterhaltungen"); + const projekte = page.locator(".sidebar-projekte"); + + await expect(unterhaltungen.getByText("Unterhaltungen", { exact: true })).toBeVisible(); + await expect(projekte.getByText("Projekte", { exact: true })).toBeVisible(); + await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible(); + + await unterhaltungen.getByTestId("conversation-picker-trigger").click(); + const picker = page.getByTestId("conversation-bot-picker"); + await expect(picker).toBeVisible(); + await expect(picker.getByTestId("conversation-create-bot")).toBeVisible(); + await expect(picker.getByText("Chief", { exact: true })).toBeVisible(); + + await picker.getByTestId("conversation-create-bot").click(); + await expect(page.getByTestId("create-bot-form")).toBeVisible(); + + await page.keyboard.press("Escape"); + await openNewBot(page); + const form = page.getByTestId("create-bot-form"); + await form.locator("label:has-text('Name') input").fill("Researcher"); + await form.getByRole("button", { name: "Create", exact: true }).click(); + await page.waitForURL(/\/app\/[^/]+$/); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); + + await projekte.getByRole("button", { name: "Create a project" }).click(); + const dialog = page.getByRole("dialog", { name: "New project" }); + await dialog.getByLabel("Name").fill("Side project"); + await dialog.getByRole("button", { name: "Create project", exact: true }).click(); + await page.waitForURL(/\/app/); + await expect(projekte.getByText("Side project", { exact: true })).toBeVisible(); + await expect(unterhaltungen.getByText("Side project", { exact: true })).toHaveCount(0); +}); + +test("chat header shows avatar once and composer uses generic placeholder", async ({ page }) => { + const stamp = Date.now(); + await signup(page, `chat-header-${stamp}@rakazo.test`, "password12", "Chat Header"); + await completeOnboarding(page); + await page.goto("/app"); + await page.waitForURL(/\/app\/[^/]+$/); + + const header = page.getByTestId("bot-settings-trigger"); + await expect(header.locator(".rakazo-bot-avatar, [data-testid='bot-avatar']").first()).toBeVisible(); + await expect(header.getByText("Chief", { exact: true })).toBeVisible(); + await expect(header.getByText("1:1 conversation", { exact: true })).toHaveCount(0); + + const composer = page.getByRole("combobox", { name: "Message" }); + await expect(composer).toHaveAttribute("placeholder", "Message…"); +}); diff --git a/apps/web/src/components/stage/OoStage.tsx b/apps/web/src/components/stage/OoStage.tsx index 02b8c47..7821ad0 100644 --- a/apps/web/src/components/stage/OoStage.tsx +++ b/apps/web/src/components/stage/OoStage.tsx @@ -170,8 +170,8 @@ export function OoStage({ />
- {t`1:1 conversation`} - {persona.roleLine ? ` · ${persona.roleLine}` : ""} -
+ {persona.roleLine ? ( ++ {persona.roleLine} +
+ ) : null}- {t`Threads by topic · folders`} -
-{t`No projects yet.`}
- -- {t`Your Oos — open a 1:1 conversation`} + {t`1:1 mit deinen Oos`}
{t`No Oos in your team yet.`}
+{t`Noch keine Unterhaltung.`}
+ {t`Threads by topic · folders`} +
+{t`No projects yet.`}
+ +