Chat: one phone header button, quick-prompt chips, double-click to reuse a message
The phone header carries a single speech bubble: Control on Home, the assistant elsewhere. Chips above the input offer the user's most sent messages per tab, then starters; a tap or a double-click on a sent or heard message fills the input without sending.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { expect, type Page, test } from "@playwright/test"
|
||||
|
||||
/**
|
||||
* The chat on a phone: its one button in the header, which tab it opens, and
|
||||
* how a message gets back into the input (a double-click or double-tap, a
|
||||
* quick-prompt chip).
|
||||
*
|
||||
* A provider and a wren node are instance state, not something a suite should
|
||||
* set up, so the three endpoints the panel reads are answered here.
|
||||
*/
|
||||
|
||||
test.use({ viewport: { width: 393, height: 851 }, hasTouch: true })
|
||||
|
||||
const SAID = "which flows are running"
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.route("**/api/v1/ai/status", (route) =>
|
||||
route.fulfill({ json: { available: true, model: "test" } }),
|
||||
)
|
||||
await page.route("**/api/v1/ai/control", (route) =>
|
||||
route.fulfill({
|
||||
json: [
|
||||
{
|
||||
id: "home.control",
|
||||
flow: "home",
|
||||
title: "Home · Control",
|
||||
prompt_msg: "home.command",
|
||||
watch: [{ msg: "home.command", kind: "text", node: "" }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
await page.route("**/api/v1/ai/chat", (route) =>
|
||||
route.request().method() === "GET"
|
||||
? route.fulfill({
|
||||
json: [
|
||||
{ role: "user", content: SAID },
|
||||
{ role: "assistant", content: "Two." },
|
||||
],
|
||||
})
|
||||
: route.fallback(),
|
||||
)
|
||||
})
|
||||
|
||||
const pressed = (page: Page, tab: string) =>
|
||||
page.getByTestId("chat-tabs").getByRole("button", { name: tab })
|
||||
|
||||
test("one chat button opens Control on Home", async ({ page }) => {
|
||||
await page.goto("/")
|
||||
await expect(page.getByTestId("chat-trigger")).toHaveCount(1)
|
||||
// Neither the old assistant button nor Home's wider-screen one is on screen.
|
||||
await expect(page.getByRole("button", { name: "Assistant" })).toHaveCount(0)
|
||||
await expect(page.getByRole("button", { name: "Chat" })).toHaveCount(1)
|
||||
|
||||
await page.getByTestId("chat-trigger").click()
|
||||
await expect(pressed(page, "Control")).toHaveAttribute("aria-pressed", "true")
|
||||
})
|
||||
|
||||
test("the same button opens the assistant elsewhere", async ({ page }) => {
|
||||
await page.goto("/flows")
|
||||
await page.getByTestId("chat-trigger").click()
|
||||
await expect(pressed(page, "Build")).toHaveAttribute("aria-pressed", "true")
|
||||
})
|
||||
|
||||
test("a message and a chip go back into the input", async ({ page }) => {
|
||||
await page.goto("/flows")
|
||||
await page.getByTestId("chat-trigger").click()
|
||||
const input = page.getByTestId("chat-input")
|
||||
const said = page.getByText(SAID)
|
||||
|
||||
await said.dblclick()
|
||||
await expect(input).toHaveValue(SAID)
|
||||
await expect(input).toBeFocused()
|
||||
// Something typed leaves no room for the row.
|
||||
await expect(page.getByTestId("chat-prompts")).toHaveCount(0)
|
||||
|
||||
await input.fill("")
|
||||
const box = await said.boundingBox()
|
||||
if (!box) throw new Error("the message is not on screen")
|
||||
const x = box.x + box.width / 2
|
||||
const y = box.y + box.height / 2
|
||||
await page.touchscreen.tap(x, y)
|
||||
await page.touchscreen.tap(x, y)
|
||||
await expect(input).toHaveValue(SAID)
|
||||
|
||||
await input.fill("")
|
||||
const chip = page.getByTestId("chat-prompt").first()
|
||||
const text = (await chip.textContent()) ?? ""
|
||||
expect(text).not.toBe("")
|
||||
await chip.click()
|
||||
await expect(input).toHaveValue(text)
|
||||
// A chip fills the input; it does not send.
|
||||
await expect(page.getByText(text)).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("what was sent comes back as a chip, the most used first", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/ai/chat", (route) =>
|
||||
route.request().method() === "POST"
|
||||
? route.fulfill({
|
||||
contentType: "text/event-stream",
|
||||
body: 'data: {"type":"done"}\n\n',
|
||||
})
|
||||
: route.fallback(),
|
||||
)
|
||||
await page.goto("/flows")
|
||||
await page.getByTestId("chat-trigger").click()
|
||||
const input = page.getByTestId("chat-input")
|
||||
for (const text of ["once", "twice", "twice"]) {
|
||||
await input.fill(text)
|
||||
// Sending is refused while the last turn is still running.
|
||||
await expect(page.getByRole("button", { name: "Send" })).toBeEnabled()
|
||||
await input.press("Enter")
|
||||
await expect(input).toHaveValue("")
|
||||
}
|
||||
|
||||
await page.reload()
|
||||
await page.getByTestId("chat-trigger").click()
|
||||
const chips = page.getByTestId("chat-prompt")
|
||||
await expect(chips.nth(0)).toHaveText("twice")
|
||||
await expect(chips.nth(1)).toHaveText("once")
|
||||
})
|
||||
Reference in New Issue
Block a user