mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
514 lines
15 KiB
TypeScript
514 lines
15 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await installMockBridge(page);
|
|
});
|
|
|
|
async function gotoApp(page: import("@playwright/test").Page) {
|
|
let lastError: unknown = null;
|
|
|
|
for (const attempt of [0, 1]) {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await waitForInvokeBridge(page);
|
|
|
|
try {
|
|
await expect(page.getByTestId("open-agents-view")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
if (attempt === 1) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
async function openPersonaCatalog(page: import("@playwright/test").Page) {
|
|
await page.getByTestId("new-agent-card").click();
|
|
await page.getByRole("menuitem", { name: "Choose from catalog" }).click();
|
|
}
|
|
|
|
async function getCatalogOrder(page: import("@playwright/test").Page) {
|
|
return page
|
|
.locator('[data-testid^="persona-catalog-list-item-"]')
|
|
.evaluateAll((elements) =>
|
|
elements.map((element) => element.getAttribute("data-testid") ?? ""),
|
|
);
|
|
}
|
|
|
|
async function selectCatalogPersona(
|
|
page: import("@playwright/test").Page,
|
|
personaId: string,
|
|
) {
|
|
await page.getByTestId(`persona-catalog-list-item-${personaId}`).click();
|
|
}
|
|
|
|
async function useCatalogPersona(
|
|
page: import("@playwright/test").Page,
|
|
personaId: string,
|
|
) {
|
|
await page
|
|
.getByTestId(`persona-catalog-use-agent-target-${personaId}`)
|
|
.click();
|
|
}
|
|
|
|
async function waitForInvokeBridge(page: import("@playwright/test").Page) {
|
|
await page.waitForFunction(
|
|
() => {
|
|
const tauriWindow = window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
|
|
__TAURI_INTERNALS__?: {
|
|
invoke?: unknown;
|
|
};
|
|
};
|
|
|
|
return (
|
|
typeof tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ === "function" ||
|
|
typeof tauriWindow.__TAURI_INTERNALS__?.invoke === "function"
|
|
);
|
|
},
|
|
null,
|
|
{ timeout: 5_000 },
|
|
);
|
|
}
|
|
|
|
async function invokeTauri<T>(
|
|
page: import("@playwright/test").Page,
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
): Promise<T> {
|
|
await waitForInvokeBridge(page);
|
|
|
|
return page.evaluate(
|
|
async ({ command: targetCommand, payload: targetPayload }) => {
|
|
const tauriWindow = window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) => Promise<unknown>;
|
|
__TAURI_INTERNALS__?: {
|
|
invoke?: (
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) => Promise<unknown>;
|
|
};
|
|
};
|
|
|
|
const invoke =
|
|
tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ??
|
|
tauriWindow.__TAURI_INTERNALS__?.invoke;
|
|
if (!invoke) {
|
|
throw new Error("Mock invoke bridge is unavailable.");
|
|
}
|
|
|
|
return (await invoke(targetCommand, targetPayload)) as T;
|
|
},
|
|
{ command, payload },
|
|
);
|
|
}
|
|
|
|
async function invokeTauriExpectError(
|
|
page: import("@playwright/test").Page,
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) {
|
|
await waitForInvokeBridge(page);
|
|
|
|
return page.evaluate(
|
|
async ({ command: targetCommand, payload: targetPayload }) => {
|
|
const tauriWindow = window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) => Promise<unknown>;
|
|
__TAURI_INTERNALS__?: {
|
|
invoke?: (
|
|
command: string,
|
|
payload?: Record<string, unknown>,
|
|
) => Promise<unknown>;
|
|
};
|
|
};
|
|
|
|
const invoke =
|
|
tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ??
|
|
tauriWindow.__TAURI_INTERNALS__?.invoke;
|
|
if (!invoke) {
|
|
throw new Error("Mock invoke bridge is unavailable.");
|
|
}
|
|
|
|
try {
|
|
await invoke(targetCommand, targetPayload);
|
|
return null;
|
|
} catch (error) {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
},
|
|
{ command, payload },
|
|
);
|
|
}
|
|
|
|
test("built-in personas are used from the catalog dialog", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1280, height: 420 });
|
|
await gotoApp(page);
|
|
await page.getByTestId("open-agents-view").click();
|
|
|
|
await expect(page.getByTestId("agents-library-personas")).toBeVisible();
|
|
await openPersonaCatalog(page);
|
|
await expect(page.getByTestId("persona-catalog-dialog")).toContainText(
|
|
"Fizz",
|
|
);
|
|
const previewPersonas = [
|
|
["builtin:product-strategist", "Product Strategist"],
|
|
["builtin:implementation-partner", "Implementation Partner"],
|
|
["builtin:qa-reviewer", "QA Reviewer"],
|
|
["builtin:work-coordinator", "Work Coordinator"],
|
|
["builtin:support-guide", "Support Guide"],
|
|
["builtin:experiment-designer", "Experiment Designer"],
|
|
] as const;
|
|
for (const [, personaName] of previewPersonas) {
|
|
await expect(page.getByTestId("persona-catalog-dialog")).toContainText(
|
|
personaName,
|
|
);
|
|
}
|
|
for (const [personaId, personaName] of previewPersonas) {
|
|
await expect(
|
|
page
|
|
.getByTestId(`persona-catalog-list-item-${personaId}`)
|
|
.getByRole("img", { name: `${personaName} avatar` }),
|
|
).toHaveAttribute("src", /.+/);
|
|
}
|
|
await expect(page.getByTestId("persona-catalog-dialog-header")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("persona-catalog-dialog-scroll-area"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("persona-catalog-dialog-scroll-area"),
|
|
).toHaveCSS("overflow-y", "auto");
|
|
const catalogScrollAreaMetrics = await page
|
|
.getByTestId("persona-catalog-dialog-scroll-area")
|
|
.evaluate((element) => ({
|
|
clientHeight: element.clientHeight,
|
|
scrollHeight: element.scrollHeight,
|
|
}));
|
|
expect(catalogScrollAreaMetrics.clientHeight).toBeGreaterThan(0);
|
|
expect(catalogScrollAreaMetrics.scrollHeight).toBeGreaterThanOrEqual(
|
|
catalogScrollAreaMetrics.clientHeight,
|
|
);
|
|
await expect(page.getByTestId("persona-catalog-dialog-body")).toBeVisible();
|
|
await expect(page.getByTestId("persona-catalog-dialog")).not.toContainText(
|
|
"Done",
|
|
);
|
|
await expect(page.getByRole("tooltip")).toHaveCount(0);
|
|
const initialCatalogOrder = await getCatalogOrder(page);
|
|
|
|
await selectCatalogPersona(page, "builtin:fizz");
|
|
await useCatalogPersona(page, "builtin:fizz");
|
|
await expect(
|
|
page
|
|
.locator("[data-sonner-toast]")
|
|
.filter({ hasText: "Selected Fizz for My Agents." }),
|
|
).toBeVisible();
|
|
|
|
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
|
"Fizz",
|
|
);
|
|
await expect(
|
|
page.getByTestId("persona-catalog-use-agent-target-builtin:fizz"),
|
|
).toHaveText("Added to My Agents");
|
|
await expect(
|
|
page.getByTestId("persona-catalog-use-agent-target-builtin:fizz"),
|
|
).toBeDisabled();
|
|
await expect(page.getByTestId("persona-catalog-dialog")).not.toContainText(
|
|
"Remove from My Agents",
|
|
);
|
|
await expect.poll(() => getCatalogOrder(page)).toEqual(initialCatalogOrder);
|
|
});
|
|
|
|
test("agent avatar emoji picker scrolls inside its popover", async ({
|
|
page,
|
|
}) => {
|
|
await gotoApp(page);
|
|
await page.getByTestId("open-agents-view").click();
|
|
await page.getByTestId("new-agent-card").click();
|
|
await page.getByRole("menuitem", { name: "New agent" }).click();
|
|
|
|
await expect(page.getByTestId("persona-dialog")).toBeVisible();
|
|
await page.getByLabel("Add avatar").click();
|
|
await page.getByRole("tab", { name: "Emoji" }).click();
|
|
await expect(page.locator("em-emoji-picker")).toBeVisible();
|
|
|
|
await page.waitForFunction(() => {
|
|
const picker = document.querySelector("em-emoji-picker");
|
|
const scroll = picker?.shadowRoot?.querySelector(".scroll");
|
|
return (
|
|
scroll instanceof HTMLElement && scroll.scrollHeight > scroll.clientHeight
|
|
);
|
|
});
|
|
|
|
const before = await page.locator("em-emoji-picker").evaluate((picker) => {
|
|
const scroll = picker.shadowRoot?.querySelector(".scroll");
|
|
return scroll instanceof HTMLElement ? scroll.scrollTop : -1;
|
|
});
|
|
|
|
const box = await page.locator("em-emoji-picker").boundingBox();
|
|
if (!box) {
|
|
throw new Error("Could not measure emoji picker bounds.");
|
|
}
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await page.mouse.wheel(0, 500);
|
|
|
|
await expect
|
|
.poll(async () =>
|
|
page.locator("em-emoji-picker").evaluate((picker) => {
|
|
const scroll = picker.shadowRoot?.querySelector(".scroll");
|
|
return scroll instanceof HTMLElement ? scroll.scrollTop : -1;
|
|
}),
|
|
)
|
|
.toBeGreaterThan(before);
|
|
});
|
|
|
|
test("agent catalog can reopen from the populated library header", async ({
|
|
page,
|
|
}) => {
|
|
await gotoApp(page);
|
|
await page.getByTestId("open-agents-view").click();
|
|
await openPersonaCatalog(page);
|
|
|
|
await selectCatalogPersona(page, "builtin:fizz");
|
|
await useCatalogPersona(page, "builtin:fizz");
|
|
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
|
"Fizz",
|
|
);
|
|
|
|
await page.keyboard.press("Escape");
|
|
await openPersonaCatalog(page);
|
|
|
|
await expect(page.getByTestId("persona-catalog-dialog")).toBeVisible();
|
|
await selectCatalogPersona(page, "builtin:fizz");
|
|
await expect(
|
|
page.getByTestId("persona-catalog-use-agent-target-builtin:fizz"),
|
|
).toBeDisabled();
|
|
});
|
|
|
|
test("agent catalog chooser order stays stable when selection changes", async ({
|
|
page,
|
|
}) => {
|
|
await gotoApp(page);
|
|
await page.getByTestId("open-agents-view").click();
|
|
await openPersonaCatalog(page);
|
|
|
|
const before = await getCatalogOrder(page);
|
|
|
|
await selectCatalogPersona(page, "builtin:fizz");
|
|
await useCatalogPersona(page, "builtin:fizz");
|
|
await expect(
|
|
page
|
|
.locator("[data-sonner-toast]")
|
|
.filter({ hasText: "Selected Fizz for My Agents." }),
|
|
).toBeVisible();
|
|
|
|
expect(await getCatalogOrder(page)).toEqual(before);
|
|
});
|
|
|
|
test("catalog detail pane shows the full persona details", async ({ page }) => {
|
|
await gotoApp(page);
|
|
await page.getByTestId("open-agents-view").click();
|
|
await openPersonaCatalog(page);
|
|
|
|
await selectCatalogPersona(page, "builtin:fizz");
|
|
const useAgentTarget = page.getByTestId(
|
|
"persona-catalog-use-agent-target-builtin:fizz",
|
|
);
|
|
|
|
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
|
"Fizz",
|
|
);
|
|
await expect(
|
|
page.getByTestId("persona-catalog-detail-pane"),
|
|
).not.toContainText("Added by You");
|
|
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
|
"You are Fizz.",
|
|
);
|
|
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
|
"Built-in agent",
|
|
);
|
|
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
|
"Preferred model",
|
|
);
|
|
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
|
"Preferred runtime",
|
|
);
|
|
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
|
"Agent instruction",
|
|
);
|
|
await expect(useAgentTarget).toHaveAttribute(
|
|
"aria-label",
|
|
"Add Fizz from Agent Catalog",
|
|
);
|
|
await expect(useAgentTarget).toHaveText("Add agent");
|
|
|
|
await useAgentTarget.click();
|
|
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
|
"Fizz",
|
|
);
|
|
});
|
|
|
|
test("custom personas can be shown in the agent catalog", async ({ page }) => {
|
|
const analystPersonaId = "custom:analyst";
|
|
await installMockBridge(page, {
|
|
personas: [
|
|
{
|
|
id: analystPersonaId,
|
|
displayName: "Analyst",
|
|
systemPrompt: "You are Analyst.",
|
|
},
|
|
],
|
|
});
|
|
await gotoApp(page);
|
|
|
|
await page.getByTestId("open-agents-view").click();
|
|
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
|
"Analyst",
|
|
);
|
|
|
|
await page.getByLabel("Open actions for Analyst").click();
|
|
await expect(page.getByRole("menuitem")).toHaveText([
|
|
"Catalog options",
|
|
"Edit",
|
|
"Duplicate",
|
|
"Remove from My Agents",
|
|
]);
|
|
await expect(
|
|
page.getByRole("menuitem", { name: "Catalog options" }),
|
|
).toBeVisible();
|
|
await expect(page.getByRole("menuitem", { name: "Export" })).toHaveCount(0);
|
|
await page.getByRole("menuitem", { name: "Catalog options" }).click();
|
|
|
|
await expect(page.getByTestId("persona-share-dialog")).toBeVisible();
|
|
await expect(page.getByTestId("persona-share-dialog")).toContainText(
|
|
"Catalog options",
|
|
);
|
|
await expect(page.getByTestId("persona-share-dialog")).toContainText(
|
|
"Added by You",
|
|
);
|
|
await expect(page.getByTestId("persona-share-copy-link")).toHaveCount(0);
|
|
await expect(page.getByText("Show in my catalog")).toBeVisible();
|
|
await expect(page.getByTestId("persona-share-export")).toBeVisible();
|
|
await page.getByTestId("persona-share-show-in-catalog").click();
|
|
await page.keyboard.press("Escape");
|
|
|
|
await openPersonaCatalog(page);
|
|
await expect(
|
|
page.getByTestId(`persona-catalog-list-item-${analystPersonaId}`),
|
|
).toContainText("Analyst");
|
|
await selectCatalogPersona(page, analystPersonaId);
|
|
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
|
"Custom agent",
|
|
);
|
|
await expect(
|
|
page.getByTestId(`persona-catalog-use-agent-target-${analystPersonaId}`),
|
|
).toHaveText("Added to My Agents");
|
|
});
|
|
|
|
test("team-managed personas do not expose editable actions", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
personas: [
|
|
{
|
|
id: "team:analyst",
|
|
displayName: "Team Analyst",
|
|
sourceTeam: "team-research-002",
|
|
systemPrompt: "You are Team Analyst.",
|
|
},
|
|
],
|
|
});
|
|
await gotoApp(page);
|
|
|
|
await page.getByTestId("open-agents-view").click();
|
|
await page.getByLabel("Open actions for Team Analyst").click();
|
|
|
|
await expect(page.getByRole("menuitem")).toHaveText([
|
|
"Catalog options",
|
|
"Duplicate",
|
|
"Managed by team",
|
|
]);
|
|
await expect(page.getByRole("menuitem", { name: "Edit" })).toHaveCount(0);
|
|
await expect(
|
|
page.getByRole("menuitem", { name: "Remove from My Agents" }),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("inactive built-ins cannot be used to create teams", async ({ page }) => {
|
|
await gotoApp(page);
|
|
|
|
const error = await invokeTauriExpectError(page, "create_team", {
|
|
input: {
|
|
name: "Fizzes",
|
|
personaIds: ["builtin:fizz"],
|
|
},
|
|
});
|
|
|
|
expect(error).toBe(
|
|
"Fizz is not in My Agents. Choose it from Agent Catalog first.",
|
|
);
|
|
});
|
|
|
|
test("built-in removal failures show up from My Agents", async ({ page }) => {
|
|
await gotoApp(page);
|
|
|
|
await page.getByTestId("open-agents-view").click();
|
|
await openPersonaCatalog(page);
|
|
await selectCatalogPersona(page, "builtin:fizz");
|
|
await useCatalogPersona(page, "builtin:fizz");
|
|
|
|
await invokeTauri(page, "create_team", {
|
|
input: {
|
|
name: "Fizzes",
|
|
personaIds: ["builtin:fizz"],
|
|
},
|
|
});
|
|
|
|
await page.keyboard.press("Escape");
|
|
await page.getByLabel("Open actions for Fizz").click();
|
|
await page.getByRole("menuitem", { name: "Remove from My Agents" }).click();
|
|
|
|
await expect(
|
|
page
|
|
.locator("[data-sonner-toast]")
|
|
.filter({ hasText: "Fizz is still referenced by a team." }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("personas referenced by teams cannot be deleted", async ({ page }) => {
|
|
await gotoApp(page);
|
|
|
|
const persona = await invokeTauri<{ id: string }>(page, "create_persona", {
|
|
input: {
|
|
displayName: "Analyst",
|
|
systemPrompt: "You are Analyst.",
|
|
},
|
|
});
|
|
|
|
await invokeTauri(page, "create_team", {
|
|
input: {
|
|
name: "Analysts",
|
|
personaIds: [persona.id],
|
|
},
|
|
});
|
|
|
|
const error = await invokeTauriExpectError(page, "delete_persona", {
|
|
id: persona.id,
|
|
});
|
|
|
|
expect(error).toBe(
|
|
"Analyst is still referenced by a team. Remove it from those teams first.",
|
|
);
|
|
});
|