Ctrl K
ComunidadGuías…

tests/e2e/flows-model-search.spec.ts

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

Archivo original ↗Preguntar a KiBot
import { expect, test } from "@playwright/test";
import type { FlowNode } from "../../lib/types";
import {
  flowPayload,
  fulfillJson,
  LONG_MODEL_ID,
  setupWorkflowsPage,
} from "./helpers/flows-inspector-layout-fixtures";
import { capturePageErrors } from "./helpers/page-errors";

test.beforeEach(async ({ page }) => {
  await page.setViewportSize({ width: 1600, height: 900 });
  await setupWorkflowsPage(page);
  const models = [
    { id: LONG_MODEL_ID, provider: "minimax", name: "MiniMax M3" },
    { id: "openai/gpt-5.4", provider: "openai", name: "GPT-5.4" },
    {
      id: "meta/long-model-id",
      provider: "meta",
      name: "Muse Spark 1.3 Contributor Extended Context",
    },
  ].map((model) => ({
    ...model,
    context_length: 1000000,
    capabilities: ["text"],
    is_available: true,
    is_enabled: true,
  }));
  await page.route("**/api/models", (route) =>
    fulfillJson(route, { models, catalog: models })
  );
  let currentFlow = structuredClone(flowPayload);
  await page.route("**/api/flows/flow-1", async (route) => {
    if (route.request().method() === "PUT") {
      const payload = route.request().postDataJSON() as Partial<
        typeof currentFlow
      >;
      currentFlow = { ...currentFlow, ...payload };
    }
    await fulfillJson(route, currentFlow);
  });
  await page.goto("/alex/automations");
  await page.getByTestId("rf__node-agent-1").click();
  await expect(page.getByLabel("Model", { exact: true })).toBeVisible();
});

test("agent model settings lead the sidebar and return to the top on selection", async ({
  page,
}, testInfo) => {
  const model = page.getByLabel("Model", { exact: true });
  const description = page.getByPlaceholder(
    "Describe what this flow should accomplish."
  );
  await page.screenshot({
    path: testInfo.outputPath("inspector.png"),
    animations: "disabled",
  });
  const modelBox = (await model.boundingBox())!;
  const descriptionBox = (await description.boundingBox())!;
  const scrollBox = (await page
    .getByTestId("flows-inspector-scroll")
    .boundingBox())!;
  expect(modelBox.y).toBeLessThan(descriptionBox.y);
  expect(modelBox.y - scrollBox.y).toBeLessThan(120);
  await expect(model).toBeInViewport();
  await expect(
    page.getByLabel("Fallback model", { exact: true })
  ).toBeInViewport();

  await description.scrollIntoViewIfNeeded();
  await page.getByTestId("rf__node-start").click();
  await page.getByTestId("rf__node-agent-1").click();
  await expect(model).toBeInViewport();
  expect(
    await page
      .getByTestId("flows-inspector-scroll")
      .evaluate((el) => el.scrollTop)
  ).toBe(0);
});

test("model search filters by name, provider and ID and saves both choices", async ({
  page,
}, testInfo) => {
  const errors = capturePageErrors(page);
  const model = page.getByLabel("Model", { exact: true });
  await expect(model).toHaveAccessibleDescription(/MiniMax M3/);
  await model.click();
  const search = page.getByPlaceholder("Search models...");
  await expect(search).toBeFocused();
  await search.fill("NO-SUCH-MODEL");
  await expect(page.getByText("No models found.")).toBeVisible();
  await expect(page.getByRole("option")).toHaveCount(0);
  await search.fill("  GPT-5.4  ");
  await expect(page.getByRole("option")).toHaveCount(1);
  await search.press("ArrowDown");
  await search.press("Enter");
  await expect(model).toHaveAttribute("data-value", "openai/gpt-5.4");
  await expect(model).toHaveAccessibleDescription(/GPT-5.4/);
  await expect(model).toBeFocused();

  const fallback = page.getByLabel("Fallback model", { exact: true });
  await fallback.click();
  await expect(search).toHaveValue("");
  await search.fill("META");
  await expect(page.getByRole("option")).toHaveCount(1);
  await search.fill("meta/long-model-id");
  await expect(page.getByRole("option")).toHaveCount(1);
  await page.screenshot({
    path: testInfo.outputPath("model-search.png"),
    animations: "disabled",
  });
  await page.getByRole("option").click();
  await expect(fallback).toHaveAttribute("data-value", "meta/long-model-id");
  await expect(fallback).toBeFocused();
  await fallback.click();
  await search.fill("minimax");
  await search.press("Escape");
  await expect(fallback).toBeFocused();
  await expect(fallback).toHaveAttribute("data-value", "meta/long-model-id");

  await expect(page.getByTestId("flow-save-status")).toHaveText("Saved");
  await page.reload();
  await page.getByTestId("rf__node-agent-1").click();
  await expect(model).toHaveAttribute("data-value", "openai/gpt-5.4");
  await expect(fallback).toHaveAttribute("data-value", "meta/long-model-id");

  await fallback.click();
  await search.fill("Default fallback pool");
  await page.getByRole("option", { name: "Default fallback pool" }).click();
  await expect(fallback).toHaveAttribute("data-value", "");
  await expect(page.getByTestId("flow-save-status")).toHaveText("Saved");
  await page.reload();
  await page.getByTestId("rf__node-agent-1").click();
  await expect(fallback).toHaveAttribute("data-value", "");
  expect(errors).toEqual([]);
});

test("model search fits a narrow inspector and keeps long names readable", async ({
  page,
}, testInfo) => {
  await page.setViewportSize({ width: 390, height: 844 });
  const model = page.getByLabel("Model", { exact: true });
  await model.click();
  const search = page.getByPlaceholder("Search models...");
  await search.fill("Muse Spark");
  const option = page.getByRole("option");
  await expect(option).toBeVisible();
  const optionBox = (await option.boundingBox())!;
  expect(optionBox.x).toBeGreaterThanOrEqual(0);
  expect(optionBox.x + optionBox.width).toBeLessThanOrEqual(390);
  await page.screenshot({
    path: testInfo.outputPath("mobile-model-search.png"),
    animations: "disabled",
  });
  await option.click();
  await expect(model).toContainText(
    "Muse Spark 1.3 Contributor Extended Context"
  );
  const label = model.locator("span").first();
  expect(await label.evaluate((el) => el.scrollWidth <= el.clientWidth)).toBe(
    true
  );
  await page.screenshot({
    path: testInfo.outputPath("mobile-model-selected.png"),
    animations: "disabled",
  });
});

test("changing nodes preserves the flow description and notes elements", async ({
  page,
}) => {
  const description = page.getByLabel("Description", { exact: true });
  const notes = page.getByPlaceholder(
    "Capture intent, guardrails, and context for this flow."
  );
  await description.fill("Review changes carefully");
  await notes.fill("Keep the draft notes");
  const descriptionElement = await description.elementHandle();
  const notesElement = await notes.elementHandle();
  await page.getByTestId("rf__node-start").click();
  // Keeping these DOM elements preserves their browser-owned editing state.
  expect(await descriptionElement!.evaluate((el) => el.isConnected)).toBe(true);
  expect(await notesElement!.evaluate((el) => el.isConnected)).toBe(true);
  await expect(description).toHaveValue("Review changes carefully");
  await expect(notes).toHaveValue("Keep the draft notes");
  await page.getByTestId("rf__node-agent-1").click();
  expect(await descriptionElement!.evaluate((el) => el.isConnected)).toBe(true);
  expect(await notesElement!.evaluate((el) => el.isConnected)).toBe(true);
  await expect(page.getByLabel("Model", { exact: true })).toBeInViewport();
});

test("retired fallback is labeled, searchable and removed after replacement", async ({
  page,
}) => {
  const legacyId = "retired/old-fallback";
  let currentFlow = {
    ...structuredClone(flowPayload),
    draft_graph: {
      ...flowPayload.draft_graph,
      nodes: (flowPayload.draft_graph.nodes as FlowNode[]).map((node) =>
        node.type === "agent"
          ? { ...node, data: { ...node.data, fallbackModelOverride: legacyId } }
          : node
      ),
    },
  };
  await page.route("**/api/flows/flow-1", async (route) => {
    if (route.request().method() === "PUT") {
      currentFlow = {
        ...currentFlow,
        ...(route.request().postDataJSON() as Partial<typeof currentFlow>),
      };
    }
    await fulfillJson(route, currentFlow);
  });
  await page.reload();
  await page.getByTestId("rf__node-agent-1").click();
  const fallback = page.getByLabel("Fallback model", { exact: true });
  await expect(fallback).toHaveAccessibleDescription(`Legacy · ${legacyId}`);
  await fallback.click();
  const search = page.getByPlaceholder("Search models...");
  await search.fill("Legacy");
  await expect(page.getByRole("option")).toHaveCount(1);
  await expect(page.getByRole("option")).toHaveText(`Legacy · ${legacyId}`);
  await search.fill("GPT-5.4");
  await page.getByRole("option").click();
  await expect(fallback).toBeFocused();
  await expect(fallback).toHaveAccessibleDescription(/OpenAI · GPT-5.4/);
  await fallback.click();
  await search.fill(legacyId);
  await expect(page.getByText("No models found.")).toBeVisible();
  await search.press("Escape");
  await expect(page.getByTestId("flow-save-status")).toHaveText("Saved");
  await page.reload();
  await page.getByTestId("rf__node-agent-1").click();
  await expect(fallback).toHaveAttribute("data-value", "openai/gpt-5.4");
});

test("reopening during the exit animation resets search and highlights the saved model", async ({
  page,
}) => {
  // Stretch the existing exit animation so reopening deterministically happens
  // while Radix still keeps the previous content mounted.
  await page.addStyleTag({
    content:
      '[data-slot="popover-content"][data-state="closed"] { animation-duration: 10s !important; }',
  });
  const model = page.getByLabel("Model", { exact: true });
  await model.click();
  const search = page.getByPlaceholder("Search models...");
  await search.fill("GPT-5.4");
  await expect(page.getByRole("option")).toHaveCount(1);
  await search.press("Escape");
  await expect(page.locator('[data-slot="popover-content"]')).toHaveAttribute(
    "data-state",
    "closed"
  );
  await model.click();
  await expect(search).toHaveValue("");
  await expect(search).toBeFocused();
  await expect(page.getByRole("option")).toHaveCount(4);
  await expect(
    page.getByRole("option", { name: /MiniMax M3/ })
  ).toHaveAttribute("aria-selected", "true");
  await search.press("Enter");
  await expect(model).toHaveAttribute("data-value", LONG_MODEL_ID);
});