Ctrl K
ComunidadGuías…

e2e/agents-search-narrow.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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);
});