Files
buzz/desktop/tests/e2e/bestie.spec.ts

395 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
const BESTIE_PUBKEY = TEST_IDENTITIES.alice.pubkey;
const RELAY_URL = "ws://localhost:3000";
async function enableBestie(page: import("@playwright/test").Page) {
await page.addInitScript(() => {
window.localStorage.setItem(
"buzz-feature-overrides-v1",
JSON.stringify({ bestie: true }),
);
});
}
async function waitForMockLiveSubscription(
page: import("@playwright/test").Page,
channelName: string,
) {
await expect
.poll(() =>
page.evaluate(
(name) =>
window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
channelName: name,
}) ?? false,
channelName,
),
)
.toBe(true);
}
test("assigns from an agent profile, reopens, drags, and offers the message action", async ({
page,
}) => {
await enableBestie(page);
await installMockBridge(
page,
{
managedAgents: [
{
pubkey: BESTIE_PUBKEY,
name: "Mochi",
avatarUrl:
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==",
status: "stopped",
},
],
managedAgentRuntimes: [
{
pubkey: BESTIE_PUBKEY,
relayUrl: RELAY_URL,
lifecycle: "stopped",
},
],
},
{ seedPreviewFeatures: false },
);
await page.goto("/", { waitUntil: "domcontentloaded" });
const floatingAvatar = page.getByTestId("bestie-floating-avatar");
await expect(floatingAvatar).toBeVisible();
expect(
await floatingAvatar.evaluate(
(element) => element.parentElement === document.body,
),
).toBe(true);
await expect(floatingAvatar.getByTestId("bestie-empty-mark")).toBeVisible();
await floatingAvatar.click();
await expect(
page.getByText("Choose a Bestie", { exact: true }),
).toBeVisible();
await expect(page.getByText(/Open one of your local agents/)).toBeVisible();
await waitForAnimations(page);
await page.getByTestId("bestie-bloom-content").screenshot({
path: "test-results/bestie/00-empty-state.png",
});
await page.keyboard.press("Escape");
await page.getByTestId("open-agents-view").click();
await page.getByRole("button", { name: "Mochi agent profile" }).click();
const bestieAction = page.getByTestId("user-profile-bestie-action");
await expect(bestieAction).toContainText("Make Bestie");
await bestieAction.click();
const confirmation = page.getByTestId("bestie-confirm-dialog");
await expect(confirmation).toContainText("Make Mochi your Bestie?");
await waitForAnimations(page);
await confirmation.screenshot({
path: "test-results/bestie/01-confirm-bestie.png",
});
await confirmation.getByRole("button", { name: "Make Bestie" }).click();
await expect(bestieAction).toContainText("Remove Bestie");
await expect(
floatingAvatar.getByTestId("bestie-trigger-avatar"),
).toBeVisible();
await expect(
floatingAvatar.getByTestId("bestie-trigger-avatar-image"),
).toHaveAttribute("draggable", "false");
const sidebarBestie = page.getByTestId("bestie-sidebar-entry");
await expect(sidebarBestie).toContainText("Mochi");
await expect(
page.getByTestId("dm-list").getByTestId("channel-alice-tyler"),
).toHaveCount(0);
expect(
await sidebarBestie.evaluate(
(element) =>
element.previousElementSibling?.querySelector(
'[data-testid="open-agents-view"]',
) !== null,
),
).toBe(true);
await waitForAnimations(page);
await page.getByTestId("sidebar-primary-menu").screenshot({
path: "test-results/bestie/02-sidebar-entry.png",
});
await page.getByTestId("user-profile-agent-management-section").screenshot({
path: "test-results/bestie/03-profile-actions.png",
});
await page.keyboard.press("Escape");
const bestieCard = page.getByTestId(`managed-agent-${BESTIE_PUBKEY}`);
await expect(
bestieCard.getByTestId(`bestie-card-badge-${BESTIE_PUBKEY}`),
).toBeVisible();
await waitForAnimations(page);
await bestieCard.screenshot({
path: "test-results/bestie/04-bestie-card.png",
});
await floatingAvatar.click();
await expect(page.getByTestId("bestie-activity-dot").last()).toBeVisible();
await expect(page.getByText("Waking", { exact: true })).toHaveCount(0);
await expect(page.getByText("Choose a different agent")).toHaveCount(0);
await waitForAnimations(page);
await page.getByTestId("bestie-bloom-content").screenshot({
path: "test-results/bestie/05-floating-popover.png",
});
const bloomContent = page.getByTestId("bestie-bloom-content");
const bloomAvatar = bloomContent.getByTestId("bestie-agent-avatar");
const openPanelBeforeDrag = await bloomContent.boundingBox();
const openAvatarBeforeDrag = await bloomAvatar.boundingBox();
for (const edge of ["top", "right", "bottom", "left"]) {
await expect(
floatingAvatar.locator(`[data-bestie-drag-edge="${edge}"]`),
).toBeVisible();
}
const dragHandle = floatingAvatar.locator('[data-bestie-drag-edge="bottom"]');
const dragHandleBox = await dragHandle.boundingBox();
expect(dragHandleBox).not.toBeNull();
await page.mouse.move(
(dragHandleBox?.x ?? 0) + (dragHandleBox?.width ?? 0) / 2,
(dragHandleBox?.y ?? 0) + (dragHandleBox?.height ?? 0) / 2,
);
await page.mouse.down();
await page.mouse.move(
(dragHandleBox?.x ?? 0) + (dragHandleBox?.width ?? 0) / 2 - 80,
(dragHandleBox?.y ?? 0) + (dragHandleBox?.height ?? 0) / 2 + 80,
{ steps: 8 },
);
await page.mouse.up();
const openPanelAfterDrag = await bloomContent.boundingBox();
const openAvatarAfterDrag = await bloomAvatar.boundingBox();
const openContainerAfterDrag = await page
.getByTestId("bestie-bloom-container")
.boundingBox();
const panelDelta = {
x: (openPanelAfterDrag?.x ?? 0) - (openPanelBeforeDrag?.x ?? 0),
y: (openPanelAfterDrag?.y ?? 0) - (openPanelBeforeDrag?.y ?? 0),
};
const avatarDelta = {
x: (openAvatarAfterDrag?.x ?? 0) - (openAvatarBeforeDrag?.x ?? 0),
y: (openAvatarAfterDrag?.y ?? 0) - (openAvatarBeforeDrag?.y ?? 0),
};
expect(Math.abs(panelDelta.x - avatarDelta.x)).toBeLessThan(1);
expect(Math.abs(panelDelta.y - avatarDelta.y)).toBeLessThan(1);
await bloomContent.getByRole("button", { name: "Close Bestie" }).click();
await expect(bloomContent).toHaveCount(0);
await waitForAnimations(page);
const closeAnchor = await floatingAvatar.boundingBox();
expect(
Math.abs(
(closeAnchor?.x ?? 0) +
(closeAnchor?.width ?? 0) -
((openContainerAfterDrag?.x ?? 0) +
(openContainerAfterDrag?.width ?? 0)),
),
).toBeLessThan(1);
const floatingTrigger = floatingAvatar.getByRole("button", {
name: "Open Bestie",
});
await expect(floatingTrigger).toHaveAttribute("aria-haspopup", "dialog");
await floatingTrigger.focus();
await page.keyboard.press("Enter");
const keyboardOpenedPopover = page.getByTestId("bestie-bloom-content");
await expect(keyboardOpenedPopover).toHaveAttribute("role", "dialog");
await expect(page.getByTestId("bestie-composer")).toBeFocused();
await page.keyboard.press("Escape");
await expect(keyboardOpenedPopover).toHaveCount(0);
await expect(floatingTrigger).toBeFocused();
await floatingTrigger.click();
await expect(page.getByTestId("bestie-composer")).toBeFocused();
const outsideDestination = page.getByTestId("open-agents-view");
await outsideDestination.click();
await expect(page.getByTestId("bestie-bloom-content")).toHaveCount(0);
await expect(outsideDestination).toBeFocused();
await sidebarBestie.click();
await expect(page).toHaveURL(/\/channels\//);
await expect(
page.getByRole("heading", { name: "alice-tyler" }),
).toBeVisible();
const firstConversationUrl = page.url();
await expect(page).toHaveURL(firstConversationUrl);
await expect(page.getByTestId("bestie-bloom-content")).toHaveCount(0);
await floatingAvatar.click();
await expect(page.getByTestId("bestie-activity-dot").last()).toBeVisible();
await page.getByRole("button", { name: "Close Bestie" }).click();
await waitForAnimations(page);
// The Motion close morph can outlive the CSS/Web animations above. Wait for
// the closed, untranslated shell before measuring the drag's starting point.
const closedBloom = floatingAvatar.getByTestId("bestie-bloom-container");
await expect(closedBloom).toHaveCSS("width", "48px");
await expect(closedBloom).toHaveCSS("height", "48px");
await expect(closedBloom).toHaveCSS("transform", "matrix(1, 0, 0, 1, 0, 0)");
const beforeDrag = await floatingAvatar.boundingBox();
const collapsedAvatar = floatingAvatar.getByTestId("bestie-trigger-avatar");
const collapsedAvatarBeforeDrag = await collapsedAvatar.boundingBox();
expect(beforeDrag).not.toBeNull();
await page.mouse.move(
(beforeDrag?.x ?? 0) + (beforeDrag?.width ?? 0) / 2,
(beforeDrag?.y ?? 0) + (beforeDrag?.height ?? 0) / 2,
);
await page.mouse.down();
await page.mouse.move(
(beforeDrag?.x ?? 0) - 120,
(beforeDrag?.y ?? 0) + 120,
{ steps: 8 },
);
await page.mouse.up();
const afterDrag = await floatingAvatar.boundingBox();
const collapsedAvatarAfterDrag = await collapsedAvatar.boundingBox();
expect(afterDrag?.x).toBeLessThan(beforeDrag?.x ?? 0);
expect(afterDrag?.y).toBeGreaterThan(beforeDrag?.y ?? 0);
expect(
Math.abs(
(afterDrag?.x ?? 0) -
(beforeDrag?.x ?? 0) -
((collapsedAvatarAfterDrag?.x ?? 0) -
(collapsedAvatarBeforeDrag?.x ?? 0)),
),
).toBeLessThan(1);
expect(
Math.abs(
(afterDrag?.y ?? 0) -
(beforeDrag?.y ?? 0) -
((collapsedAvatarAfterDrag?.y ?? 0) -
(collapsedAvatarBeforeDrag?.y ?? 0)),
),
).toBeLessThan(1);
await page.getByTestId("channel-general").click();
const messageRow = page.getByTestId("message-row").first();
await messageRow.hover();
const messageAction = messageRow.locator('[data-testid^="bestie-message-"]');
await expect(messageAction).toBeVisible();
await messageAction.click();
const messagePopover = page
.locator("[data-radix-popper-content-wrapper]")
.last();
await expect(messagePopover.getByLabel("Message Mochi")).toHaveValue("");
await expect(
messagePopover.getByText("How can I help?", { exact: true }),
).toBeVisible();
const snapshot = messagePopover.getByTestId("bestie-message-snapshot");
await expect(snapshot).toBeVisible();
const snapshotBox = await snapshot.boundingBox();
const popoverBox = await messagePopover.boundingBox();
expect(snapshotBox?.height).toBeLessThanOrEqual(96);
expect(snapshotBox?.width).toBeLessThanOrEqual(
(popoverBox?.width ?? 0) * 0.75 + 1,
);
await waitForAnimations(page);
await messagePopover.screenshot({
path: "test-results/bestie/06-message-handoff.png",
});
const channelUrl = page.url();
const miniComposer = messagePopover.getByLabel("Message Mochi");
await miniComposer.fill("Give me a concise reply");
await miniComposer.press("Shift+Enter");
await miniComposer.pressSequentially("with one example");
await expect(miniComposer).toHaveValue(
"Give me a concise reply\nwith one example",
);
await expect(
messagePopover.getByTestId("bestie-mini-transcript"),
).toHaveCount(0);
await miniComposer.press("Enter");
await expect(page).toHaveURL(channelUrl);
await expect(messagePopover).toBeVisible();
await expect(miniComposer).toHaveValue("");
await expect(snapshot).toHaveCount(0);
await expect(
messagePopover.getByTestId("bestie-mini-transcript"),
).toContainText("Give me a concise reply\nwith one example");
const miniTranscript = messagePopover.getByTestId("bestie-mini-transcript");
const sentMessageRow = miniTranscript
.getByTestId("message-row")
.filter({ hasText: "Give me a concise reply" });
await expect(sentMessageRow).toBeVisible();
const sentMessageId = await sentMessageRow.getAttribute("data-message-id");
expect(sentMessageId).toBeTruthy();
const bestieChannelName = await miniTranscript.getAttribute(
"data-bestie-channel-name",
);
expect(bestieChannelName).toBeTruthy();
await waitForMockLiveSubscription(page, bestieChannelName as string);
await page.evaluate(
({ channelName, pubkey, targetId }) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName,
content: "Here’s the concise reply.",
pubkey,
});
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName,
content: "👀",
extraTags: [["e", targetId]],
kind: 7,
pubkey,
});
},
{
channelName: bestieChannelName as string,
pubkey: BESTIE_PUBKEY,
targetId: sentMessageId as string,
},
);
await expect(miniTranscript).toContainText("Here’s the concise reply.");
await expect(sentMessageRow.getByLabel("Toggle 👀 reaction")).toBeVisible();
const miniDayDivider = miniTranscript
.getByTestId("message-timeline-day-divider")
.first();
if ((await miniDayDivider.count()) > 0) {
await expect(miniDayDivider).toHaveCSS("position", "static");
}
await sentMessageRow.hover();
await expect(
sentMessageRow.locator('[data-testid^="bestie-message-"]'),
).toHaveCount(0);
await miniComposer.fill("One more thought");
await miniComposer.press("Enter");
await expect(page).toHaveURL(channelUrl);
await expect(miniTranscript).toContainText("One more thought");
await waitForAnimations(page);
await messagePopover.screenshot({
path: "test-results/bestie/07-message-conversation.png",
});
await messagePopover.getByRole("button", { name: "Close Bestie" }).click();
await expect(page.getByLabel("Message Mochi")).toHaveCount(0);
await floatingAvatar.click();
const freshFloatingPopover = page.getByTestId("bestie-bloom-content");
await expect(freshFloatingPopover.getByLabel("Message Mochi")).toBeVisible();
await expect(
freshFloatingPopover.getByTestId("bestie-mini-transcript"),
).toHaveCount(0);
await expect(freshFloatingPopover).not.toContainText("One more thought");
await freshFloatingPopover
.getByRole("button", { name: "Close Bestie" })
.click();
await messageRow.hover();
await messageAction.focus();
await messageAction.press("Enter");
const freshMessagePopover = page
.locator("[data-radix-popper-content-wrapper]")
.last();
await expect(freshMessagePopover.getByLabel("Message Mochi")).toBeVisible();
await expect(
freshMessagePopover.getByTestId("bestie-mini-transcript"),
).toHaveCount(0);
await expect(freshMessagePopover).not.toContainText("One more thought");
await expect(
freshMessagePopover.getByTestId("bestie-message-snapshot"),
).toBeVisible();
});