e2e/agents-search-narrow.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test, type Locator, type Page } from "@playwright/test";
import { loginAsDefault } from "./helpers";
/**
* MUL-8490: on a phone, the open agents search used to overflow its slot and
* cover Reset (390px) and Display settings too (375px), so a tap on Reset
* landed on the field. Every header control must keep its own box.
*
* The agent list is mocked at the network boundary: the header layout does
* not depend on real agents, and the spec leaves nothing behind.
*/
// Scope "all" lists every agent, so the owner only has to be well-formed.
const OWNER_ID = "44444444-4444-4444-8444-444444444444";
const AGENTS = [
{ id: "a1a1a1a1-1111-4111-8111-111111111111", name: "PR Reviewer", description: "Reviews pull requests" },
{ id: "b2b2b2b2-2222-4222-8222-222222222222", name: "Docs Bot", description: "Keeps docs in sync" },
];
async function mockAgents(page: Page) {
await page.route("**/api/agents**", (route) => {
const req = route.request();
const url = new URL(req.url());
if (req.method() !== "GET" || !url.pathname.endsWith("/api/agents")) {
return route.fallback();
}
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(
AGENTS.map((agent) => ({
...agent,
workspace_id: url.searchParams.get("workspace_id") ?? "ws-mock",
runtime_id: "33333333-3333-4333-8333-333333333333",
instructions: "",
avatar_url: null,
runtime_mode: "local",
runtime_config: {},
custom_args: [],
visibility: "workspace",
permission_mode: "private",
invocation_targets: [],
status: "idle",
max_concurrent_tasks: 1,
model: "claude-opus-4",
owner_id: OWNER_ID,
skills: [],
created_at: "2026-06-30T00:00:00Z",
updated_at: "2026-06-30T00:00:00Z",
archived_at: null,
archived_by: null,
})),
),
});
});
}
/** Open the list with one filter already on, the case that widens the header most. */
async function openFilteredList(page: Page, width: number) {
await page.setViewportSize({ width, height: 760 });
const slug = await loginAsDefault(page);
await mockAgents(page);
await page.evaluate((key) => {
localStorage.setItem(
key,
JSON.stringify({
state: {
scope: "all",
filters: { availability: [], runtimes: [], owners: [], models: ["claude-opus-4"], access: [] },
},
version: 2,
}),
);
}, `multica_agents_view:${slug}`);
await page.goto(`/${slug}/agents`, { waitUntil: "domcontentloaded" });
await expect(page.getByRole("button", { name: "1 filter" })).toBeVisible({ timeout: 20000 });
}
async function box(locator: Locator) {
const b = await locator.boundingBox();
if (!b) throw new Error("Could not measure a header control");
return b;
}
function overlaps(a: { x: number; width: number }, b: { x: number; width: number }) {
return a.x < b.x + b.width && b.x < a.x + a.width;
}
for (const width of [375, 390]) {
test(`keeps search, Reset and Display settings apart at ${width}px`, async ({ page }) => {
await openFilteredList(page, width);
await page.getByRole("button", { name: "Search agents" }).click();
const field = page.getByRole("textbox", { name: "Search agents" });
await field.fill("review");
const reset = page.getByRole("button", { name: "Reset" });
const display = page.getByRole("button", { name: "Display settings" });
const create = page.getByRole("button", { name: "New agent" });
const filter = page.getByRole("button", { name: "1 filter" });
const fieldBox = await box(page.getByRole("group").filter({ has: field }));
// The field keeps room to read a query.
expect(fieldBox.width).toBeGreaterThanOrEqual(96);
for (const other of [filter, reset, display, create]) {
expect(overlaps(fieldBox, await box(other))).toBe(false);
}
expect(overlaps(await box(reset), await box(display))).toBe(false);
const createBox = await box(create);
expect(createBox.x + createBox.width).toBeLessThanOrEqual(width);
// Display settings takes its own click.
await display.click({ timeout: 3000 });
await expect(page.getByRole("dialog")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toHaveCount(0);
// Reset takes its own click and clears both the search and the filter.
await reset.click({ timeout: 3000 });
await expect(field).toHaveCount(0);
await expect(page.getByRole("button", { name: "Filter", exact: true })).toBeVisible();
await expect(page.getByRole("heading", { level: 1, name: /All agents/ })).toBeVisible();
});
}
test("keeps the title beside an open search on a wide window", async ({ page }) => {
await openFilteredList(page, 1280);
await page.getByRole("button", { name: "Search agents" }).click();
await page.getByRole("textbox", { name: "Search agents" }).fill("review");
await expect(page.getByRole("heading", { level: 1, name: /All agents/ })).toBeVisible();
const fieldBox = await box(
page.getByRole("group").filter({ has: page.getByRole("textbox", { name: "Search agents" }) }),
);
expect(fieldBox.width).toBe(240);
});