tests/e2e/flows-model-search.spec.ts
Rama main · réplica de lectura del repositorio original.
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);
});