Files
kandev/apps/web/e2e/tests/session/dockview-repository-scripts.spec.ts
T

319 lines
11 KiB
TypeScript

import { type Page } from "@playwright/test";
import { test, expect } from "../../fixtures/test-base";
import type { SeedData } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
import { SessionPage } from "../../pages/session-page";
async function seedTaskWithSession(
testPage: Page,
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<SessionPage> {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
title,
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
if (!task.session_id) throw new Error("createTaskWithAgent did not return a session_id");
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
// The auto-started mock agent can finish its turn before the client's WS
// subscription registers, so a raw idleInput() visibility wait loses that
// race. waitForChatIdle reloads once to re-derive state from SSR.
await session.waitForChatIdle();
return session;
}
/** Read the visible top-level menu items in the dockview "+" dropdown. */
async function readMenuItemTexts(testPage: Page): Promise<string[]> {
return testPage.locator('[role="menuitem"]:visible').allInnerTexts();
}
/**
* Read every xterm buffer on the page and return their concatenated text. Used
* to assert that *some* terminal contains given output regardless of which
* dockview group / tab order it ended up in.
*/
async function readAllTerminalBuffers(testPage: Page): Promise<string> {
return testPage.evaluate(() => {
type XC = HTMLElement & { __xtermReadBuffer?: () => string };
const panels = Array.from(document.querySelectorAll('[data-testid="terminal-panel"]'));
return panels
.map((panel) => {
const xtermEl = panel.querySelector(".xterm");
const container = xtermEl?.parentElement as XC | null | undefined;
return container?.__xtermReadBuffer?.() ?? "";
})
.join("\n----\n");
});
}
test.describe("Dockview repository scripts in + menu", () => {
test.describe.configure({ retries: 1 });
test("does not render Scripts section when repo has no scripts and no dev_script", async ({
testPage,
apiClient,
seedData,
}) => {
// Ensure repo has no dev_script (seed doesn't set one, but be explicit).
await apiClient.updateRepository(seedData.repositoryId, { dev_script: "" });
const session = await seedTaskWithSession(testPage, apiClient, seedData, "No Scripts");
await session.addPanelButton().click();
await expect(testPage.getByTestId("new-terminal-button")).toBeVisible();
await expect(testPage.getByText("Scripts", { exact: true })).toHaveCount(0);
await expect(testPage.getByTestId("run-dev-script")).toHaveCount(0);
});
test("renders custom scripts at the end of the + menu", async ({
testPage,
apiClient,
seedData,
}) => {
const buildScript = await apiClient.createRepositoryScript(
seedData.repositoryId,
"Run Build",
"echo build",
0,
);
const lintScript = await apiClient.createRepositoryScript(
seedData.repositoryId,
"Run Lint",
"echo lint",
1,
);
const session = await seedTaskWithSession(testPage, apiClient, seedData, "Custom Scripts");
await session.addPanelButton().click();
await expect(testPage.getByTestId(`run-script-${buildScript.id}`)).toBeVisible();
await expect(testPage.getByTestId(`run-script-${lintScript.id}`)).toBeVisible();
// Scripts must be the LAST items in the menu. Verify by ordering: the
// "Browser" item appears before the script items in the visible menu.
const items = await readMenuItemTexts(testPage);
const browserIdx = items.findIndex((t) => t.includes("Browser"));
const buildIdx = items.findIndex((t) => t.includes("Run Build"));
const lintIdx = items.findIndex((t) => t.includes("Run Lint"));
expect(browserIdx).toBeGreaterThanOrEqual(0);
expect(buildIdx).toBeGreaterThan(browserIdx);
expect(lintIdx).toBeGreaterThan(browserIdx);
});
test("clicking a custom script opens a terminal that runs the script command", async ({
testPage,
apiClient,
seedData,
}) => {
const marker = "CUSTOM_SCRIPT_RAN_OK_3131";
const script = await apiClient.createRepositoryScript(
seedData.repositoryId,
"Echo Hello",
`echo ${marker}`,
0,
);
const session = await seedTaskWithSession(testPage, apiClient, seedData, "Click Script");
await session.addPanelButton().click();
await testPage.getByTestId(`run-script-${script.id}`).click();
// The backend assigns script terminals an id prefixed with "script-".
await expect
.poll(
async () => {
return testPage.evaluate(() => {
type Panel = { id: string };
type Api = { panels: Panel[] };
const api = (window as unknown as { __dockviewApi__?: Api }).__dockviewApi__;
return api?.panels.some((p) => p.id.startsWith("script-")) ?? false;
});
},
{ timeout: 10_000, message: "Expected a script terminal panel to be added" },
)
.toBe(true);
// The script command must actually execute — *some* terminal buffer
// (the one belonging to the new script panel) must contain the marker.
await expect
.poll(async () => (await readAllTerminalBuffers(testPage)).includes(marker), {
timeout: 60_000,
message: `Expected a terminal to contain "${marker}"`,
})
.toBe(true);
});
test("renders Dev Server entry when repository has dev_script", async ({
testPage,
apiClient,
seedData,
}) => {
await apiClient.updateRepository(seedData.repositoryId, {
dev_script: "echo dev-server-running",
});
const session = await seedTaskWithSession(testPage, apiClient, seedData, "Dev Script");
await session.addPanelButton().click();
await expect(testPage.getByTestId("run-dev-script")).toBeVisible();
await expect(testPage.getByText("Dev Server", { exact: true })).toBeVisible();
// Also at the end of the menu — after Browser.
const items = await readMenuItemTexts(testPage);
const browserIdx = items.findIndex((t) => t.includes("Browser"));
const devIdx = items.findIndex((t) => t.includes("Dev Server"));
expect(devIdx).toBeGreaterThan(browserIdx);
});
test("dev_script and custom scripts coexist in the Scripts section", async ({
testPage,
apiClient,
seedData,
}) => {
await apiClient.updateRepository(seedData.repositoryId, { dev_script: "echo dev" });
const customScript = await apiClient.createRepositoryScript(
seedData.repositoryId,
"Test Suite",
"echo test",
0,
);
const session = await seedTaskWithSession(testPage, apiClient, seedData, "Mixed Scripts");
await session.addPanelButton().click();
await expect(testPage.getByTestId("run-dev-script")).toBeVisible();
await expect(testPage.getByTestId(`run-script-${customScript.id}`)).toBeVisible();
});
test("header run script dropdown shows reasonable script names and commands", async ({
testPage,
apiClient,
seedData,
}) => {
await apiClient.createRepositoryScript(
seedData.repositoryId,
"Windows",
"flutter run -d windows",
0,
);
await seedTaskWithSession(testPage, apiClient, seedData, "Readable Script Menu");
await testPage.getByRole("button", { name: "Run script" }).click();
const menu = testPage.locator('[data-slot="dropdown-menu-content"]:visible');
await expect(menu).toHaveCount(1);
await expect(menu).toBeVisible();
const menuBox = await menu.boundingBox();
expect(menuBox?.width).toBeGreaterThanOrEqual(260);
expect(menuBox?.width).toBeLessThanOrEqual(320);
const item = testPage
.getByRole("menuitem")
.filter({ hasText: "Windows" })
.filter({ hasText: "flutter run -d windows" });
await expect(item).toHaveCount(1);
await expect(item).toBeVisible();
const clippedText = await item.evaluate((element) =>
Array.from(element.querySelectorAll("span"))
.filter((span) =>
["Windows", "flutter run -d windows"].includes(span.textContent?.trim() ?? ""),
)
.filter((span) => span.scrollWidth > span.clientWidth + 1)
.map((span) => span.textContent?.trim()),
);
expect(clippedText).toEqual([]);
});
test("header run script dropdown caps long script previews", async ({
testPage,
apiClient,
seedData,
}) => {
await apiClient.createRepositoryScript(
seedData.repositoryId,
"Start desktop target",
"set -e\nflutter clean\nflutter pub get\nflutter run -d windows --dart-define=ENV=local",
0,
);
await seedTaskWithSession(testPage, apiClient, seedData, "Compact Script Preview");
await testPage.getByRole("button", { name: "Run script" }).click();
const item = testPage.getByRole("menuitem").filter({ hasText: "Start desktop target" });
await expect(item).toHaveCount(1);
await expect(item).toBeVisible();
const metrics = await item.evaluate((element) => {
const commandSpan = Array.from(element.querySelectorAll("span")).find(
(span) => span.children.length === 0 && span.textContent?.includes("flutter clean"),
);
if (!commandSpan) {
throw new Error("Expected command preview span");
}
const style = window.getComputedStyle(commandSpan);
return {
commandClientHeight: commandSpan.clientHeight,
commandScrollHeight: commandSpan.scrollHeight,
commandWhiteSpace: style.whiteSpace,
itemHeight: element.getBoundingClientRect().height,
};
});
expect(metrics.commandWhiteSpace).toBe("nowrap");
expect(metrics.commandScrollHeight).toBeLessThanOrEqual(metrics.commandClientHeight + 1);
expect(metrics.itemHeight).toBeLessThanOrEqual(44);
});
test("clicking Dev Server opens a terminal that runs the dev_script command", async ({
testPage,
apiClient,
seedData,
}) => {
const marker = "DEV_SCRIPT_RAN_OK_4242";
await apiClient.updateRepository(seedData.repositoryId, {
dev_script: `echo ${marker}`,
});
const session = await seedTaskWithSession(testPage, apiClient, seedData, "Run Dev Script");
await session.addPanelButton().click();
await testPage.getByTestId("run-dev-script").click();
// Backend assigns "script-"-prefixed ids to script terminals.
await expect
.poll(
async () => {
return testPage.evaluate(() => {
type Panel = { id: string };
type Api = { panels: Panel[] };
const api = (window as unknown as { __dockviewApi__?: Api }).__dockviewApi__;
return api?.panels.some((p) => p.id.startsWith("script-")) ?? false;
});
},
{ timeout: 10_000, message: "Expected a dev-script terminal panel to be added" },
)
.toBe(true);
// The dev_script must actually execute — *some* terminal buffer should
// contain the marker emitted by the script command.
await expect
.poll(async () => (await readAllTerminalBuffers(testPage)).includes(marker), {
timeout: 60_000,
message: `Expected a terminal to contain "${marker}"`,
})
.toBe(true);
});
});