479 lines
14 KiB
TypeScript
479 lines
14 KiB
TypeScript
import { expect, resetSeedRepositoryCheckout, test } from "../fixtures/test-base";
|
|
import path from "node:path";
|
|
import {
|
|
GitHelper,
|
|
makeGitEnv,
|
|
openTaskSession,
|
|
createStandardProfile,
|
|
} from "../helpers/git-helper";
|
|
|
|
const MOD = process.platform === "darwin" ? ("Meta" as const) : ("Control" as const);
|
|
|
|
async function setupFileTreeTest(
|
|
testPage: Page,
|
|
apiClient: ApiClient,
|
|
seedData: { workspaceId: string; workflowId: string; startStepId: string; repositoryId: string },
|
|
profileName: string,
|
|
taskTitle: string,
|
|
) {
|
|
const profile = await createStandardProfile(apiClient, profileName);
|
|
await apiClient.createTaskWithAgent(seedData.workspaceId, taskTitle, profile.id, {
|
|
description: "/e2e:simple-message",
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
repository_ids: [seedData.repositoryId],
|
|
});
|
|
const session = await openTaskSession(testPage, taskTitle);
|
|
await session.clickTab("Files");
|
|
return session;
|
|
}
|
|
|
|
test.describe("File Tree Multi-Select", () => {
|
|
test.beforeEach(({ backend, seedData }) => {
|
|
// Each test commits directly to the shared seed checkout. Restore the
|
|
// immutable fixture baseline before the next test so prior commits cannot
|
|
// change which rows the file tree exposes.
|
|
resetSeedRepositoryCheckout(seedData, backend.tmpDir);
|
|
});
|
|
|
|
// --- Selection Basics ---
|
|
|
|
test("ctrl-click selects a file without opening it", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("alpha.ts", "a");
|
|
git.createFile("beta.ts", "b");
|
|
git.stageAll();
|
|
git.commit("add files");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-select-1",
|
|
"FT Select File",
|
|
);
|
|
const alpha = session.fileTreeNode("alpha.ts");
|
|
await expect(alpha).toBeVisible({ timeout: 15_000 });
|
|
|
|
await alpha.click({ modifiers: [MOD] });
|
|
await expect(alpha).toHaveAttribute("data-selected", "true");
|
|
});
|
|
|
|
test("ctrl-click toggles file out of selection", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("toggle-a.ts", "a");
|
|
git.stageAll();
|
|
git.commit("add toggle file");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-toggle",
|
|
"FT Toggle File",
|
|
);
|
|
const file = session.fileTreeNode("toggle-a.ts");
|
|
await expect(file).toBeVisible({ timeout: 15_000 });
|
|
|
|
await file.click({ modifiers: [MOD] });
|
|
await expect(file).toHaveAttribute("data-selected", "true");
|
|
|
|
await file.click({ modifiers: [MOD] });
|
|
await expect(file).toHaveAttribute("data-selected", "false");
|
|
});
|
|
|
|
test("ctrl-click selects multiple files", async ({ testPage, apiClient, seedData, backend }) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("multi-a.ts", "a");
|
|
git.createFile("multi-b.ts", "b");
|
|
git.createFile("multi-c.ts", "c");
|
|
git.stageAll();
|
|
git.commit("add multi files");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-multi",
|
|
"FT Multi Select",
|
|
);
|
|
const a = session.fileTreeNode("multi-a.ts");
|
|
const b = session.fileTreeNode("multi-b.ts");
|
|
await expect(a).toBeVisible({ timeout: 15_000 });
|
|
|
|
await a.click({ modifiers: [MOD] });
|
|
await b.click({ modifiers: [MOD] });
|
|
await expect(a).toHaveAttribute("data-selected", "true");
|
|
await expect(b).toHaveAttribute("data-selected", "true");
|
|
});
|
|
|
|
test("plain click clears selection", async ({ testPage, apiClient, seedData, backend }) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("clear-a.ts", "a");
|
|
git.createFile("clear-b.ts", "b");
|
|
git.stageAll();
|
|
git.commit("add clear files");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-clear",
|
|
"FT Plain Click Clear",
|
|
);
|
|
const a = session.fileTreeNode("clear-a.ts");
|
|
const b = session.fileTreeNode("clear-b.ts");
|
|
await expect(a).toBeVisible({ timeout: 15_000 });
|
|
|
|
await a.click({ modifiers: [MOD] });
|
|
await b.click({ modifiers: [MOD] });
|
|
await expect(session.fileTreeSelectedNodes()).toHaveCount(2);
|
|
|
|
await a.click();
|
|
await expect(session.fileTreeSelectedNodes()).toHaveCount(0);
|
|
});
|
|
|
|
// --- Shift-Click Range ---
|
|
|
|
test("shift-click selects range after plain click anchor", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("r-a.ts", "a");
|
|
git.createFile("r-b.ts", "b");
|
|
git.createFile("r-c.ts", "c");
|
|
git.createFile("r-d.ts", "d");
|
|
git.stageAll();
|
|
git.commit("add range files");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-range",
|
|
"FT Shift Range",
|
|
);
|
|
const a = session.fileTreeNode("r-a.ts");
|
|
const d = session.fileTreeNode("r-d.ts");
|
|
await expect(a).toBeVisible({ timeout: 15_000 });
|
|
await expect(d).toBeVisible({ timeout: 15_000 });
|
|
|
|
await a.click();
|
|
await d.click({ modifiers: ["Shift"] });
|
|
|
|
await expect(session.fileTreeNode("r-a.ts")).toHaveAttribute("data-selected", "true");
|
|
await expect(session.fileTreeNode("r-b.ts")).toHaveAttribute("data-selected", "true");
|
|
await expect(session.fileTreeNode("r-c.ts")).toHaveAttribute("data-selected", "true");
|
|
await expect(session.fileTreeNode("r-d.ts")).toHaveAttribute("data-selected", "true");
|
|
});
|
|
|
|
test("shift-click selects range after ctrl-click anchor", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("s-a.ts", "a");
|
|
git.createFile("s-b.ts", "b");
|
|
git.createFile("s-c.ts", "c");
|
|
git.stageAll();
|
|
git.commit("add shift range files");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-shift-anchor",
|
|
"FT Shift Anchor",
|
|
);
|
|
const b = session.fileTreeNode("s-b.ts");
|
|
const c = session.fileTreeNode("s-c.ts");
|
|
await expect(b).toBeVisible({ timeout: 15_000 });
|
|
|
|
await b.click({ modifiers: [MOD] });
|
|
await expect(b).toHaveAttribute("data-selected", "true");
|
|
|
|
await c.click({ modifiers: ["Shift"] });
|
|
await expect(session.fileTreeNode("s-b.ts")).toHaveAttribute("data-selected", "true");
|
|
await expect(session.fileTreeNode("s-c.ts")).toHaveAttribute("data-selected", "true");
|
|
});
|
|
|
|
// --- Directory Selection ---
|
|
|
|
test("ctrl-click selects a directory without expanding it", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("mydir/nested.ts", "n");
|
|
git.stageAll();
|
|
git.commit("add dir");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-dir-select",
|
|
"FT Dir Select",
|
|
);
|
|
const dir = session.fileTreeNode("mydir");
|
|
await expect(dir).toBeVisible({ timeout: 15_000 });
|
|
|
|
await dir.click({ modifiers: [MOD] });
|
|
await expect(dir).toHaveAttribute("data-selected", "true");
|
|
});
|
|
|
|
test("ctrl-click selects file inside expanded directory", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("srcdir/inner.ts", "inner");
|
|
git.stageAll();
|
|
git.commit("add inner file");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-nested-select",
|
|
"FT Nested Select",
|
|
);
|
|
const dir = session.fileTreeNode("srcdir");
|
|
await expect(dir).toBeVisible({ timeout: 15_000 });
|
|
await dir.click();
|
|
|
|
const inner = session.fileTreeNode("srcdir/inner.ts");
|
|
await expect(inner).toBeVisible({ timeout: 10_000 });
|
|
await inner.click({ modifiers: [MOD] });
|
|
await expect(inner).toHaveAttribute("data-selected", "true");
|
|
await expect(dir).toHaveAttribute("data-selected", "false");
|
|
});
|
|
|
|
// --- Right-Click Context Menu ---
|
|
|
|
test("right-click single file uses local delete confirmation", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
prCapture,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("ctx-file.ts", "ctx");
|
|
git.stageAll();
|
|
git.commit("add ctx file");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-ctx-menu",
|
|
"FT Context Menu",
|
|
);
|
|
const file = session.fileTreeNode("ctx-file.ts");
|
|
await expect(file).toBeVisible({ timeout: 15_000 });
|
|
|
|
await file.click({ button: "right" });
|
|
const deleteItem = testPage.getByRole("menuitem", { name: "Delete" });
|
|
await expect(deleteItem).toBeVisible({
|
|
timeout: 5_000,
|
|
});
|
|
|
|
await deleteItem.click();
|
|
await expect(testPage.getByTestId("file-delete-confirm-popover")).toBeVisible();
|
|
await expect(testPage.getByRole("alertdialog")).toHaveCount(0);
|
|
await prCapture.screenshot("desktop-file-delete-confirmation", {
|
|
caption: "Desktop file context menu with one-file delete confirmation",
|
|
});
|
|
await testPage.getByTestId("file-delete-confirm").click();
|
|
await expect(file).not.toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("right-click on multi-selection shows bulk delete", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("bulk-a.ts", "a");
|
|
git.createFile("bulk-b.ts", "b");
|
|
git.stageAll();
|
|
git.commit("add bulk files");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-bulk-ctx",
|
|
"FT Bulk Context",
|
|
);
|
|
const a = session.fileTreeNode("bulk-a.ts");
|
|
const b = session.fileTreeNode("bulk-b.ts");
|
|
await expect(a).toBeVisible({ timeout: 15_000 });
|
|
|
|
await a.click({ modifiers: [MOD] });
|
|
await b.click({ modifiers: [MOD] });
|
|
|
|
await a.click({ button: "right" });
|
|
const deleteItem = testPage.getByRole("menuitem", { name: "Delete 2 items" });
|
|
await expect(deleteItem).toBeVisible({ timeout: 5_000 });
|
|
|
|
// Click the delete menu item and confirm dialog
|
|
await deleteItem.click();
|
|
const confirmBtn = testPage.getByRole("button", { name: "Delete" });
|
|
await expect(confirmBtn).toBeVisible({ timeout: 5_000 });
|
|
await confirmBtn.click();
|
|
|
|
// Both files should be removed from the tree
|
|
await expect(a).not.toBeVisible({ timeout: 10_000 });
|
|
await expect(b).not.toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
// --- Click Outside ---
|
|
|
|
test("clicking outside tree clears selection", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("outside-a.ts", "a");
|
|
git.stageAll();
|
|
git.commit("add outside file");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-click-outside",
|
|
"FT Click Outside",
|
|
);
|
|
const file = session.fileTreeNode("outside-a.ts");
|
|
await expect(file).toBeVisible({ timeout: 15_000 });
|
|
|
|
await file.click({ modifiers: [MOD] });
|
|
await expect(file).toHaveAttribute("data-selected", "true");
|
|
|
|
// Click empty area in the files panel header area
|
|
await session.files.click({ position: { x: 5, y: 5 } });
|
|
await expect(session.fileTreeSelectedNodes()).toHaveCount(0, { timeout: 5_000 });
|
|
});
|
|
|
|
// --- Keyboard ---
|
|
|
|
test("select-all selects every visible row from non-editable tree focus", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
// @covers AC-UI-FILE-TREE-KEYBOARD-SCOPE-001.3
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("tree-select-all-a.ts", "a");
|
|
git.createFile("tree-select-all-b.ts", "b");
|
|
git.stageAll();
|
|
git.commit("add select-all files");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-tree-select-all",
|
|
"FT Tree Select All",
|
|
);
|
|
const firstFile = session.fileTreeNode("tree-select-all-a.ts");
|
|
await expect(firstFile).toBeVisible({ timeout: 15_000 });
|
|
const visibleRows = session.files.locator("[data-testid='file-tree-node']:visible");
|
|
const visibleRowCount = await visibleRows.count();
|
|
|
|
const fileBrowser = firstFile.locator("xpath=ancestor::div[@tabindex='-1'][1]");
|
|
await fileBrowser.focus();
|
|
await fileBrowser.press("ControlOrMeta+a");
|
|
|
|
await expect(session.fileTreeSelectedNodes()).toHaveCount(visibleRowCount);
|
|
});
|
|
|
|
test("escape clears selection", async ({ testPage, apiClient, seedData, backend }) => {
|
|
const git = new GitHelper(
|
|
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
|
makeGitEnv(backend.tmpDir),
|
|
);
|
|
git.createFile("esc-file.ts", "e");
|
|
git.stageAll();
|
|
git.commit("add esc file");
|
|
|
|
const session = await setupFileTreeTest(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-escape",
|
|
"FT Escape",
|
|
);
|
|
const file = session.fileTreeNode("esc-file.ts");
|
|
await expect(file).toBeVisible({ timeout: 15_000 });
|
|
|
|
await file.click({ modifiers: [MOD] });
|
|
await expect(file).toHaveAttribute("data-selected", "true");
|
|
|
|
await file.press("Escape");
|
|
await expect(session.fileTreeSelectedNodes()).toHaveCount(0, { timeout: 5_000 });
|
|
});
|
|
});
|