mirror of
https://github.com/block/buzz.git
synced 2026-10-30 14:01:37 +00:00
395 lines
14 KiB
TypeScript
395 lines
14 KiB
TypeScript
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();
|
||
});
|