7365db7064
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
201 lines
7.1 KiB
TypeScript
201 lines
7.1 KiB
TypeScript
import { type Page } from "@playwright/test";
|
|
import path from "node:path";
|
|
import fs from "node:fs";
|
|
import { test, expect } from "../../fixtures/test-base";
|
|
import type { ApiClient } from "../../helpers/api-client";
|
|
import {
|
|
GitHelper,
|
|
makeGitEnv,
|
|
openTaskSession,
|
|
createStandardProfile,
|
|
} from "../../helpers/git-helper";
|
|
import { dwell } from "../../helpers/causal-waits";
|
|
|
|
// Inline rename lives in file-context-menu.tsx (useFileRename + TreeNodeName).
|
|
// Entry points (today, in product code):
|
|
// - Right-click -> "Rename" menu item
|
|
// - The input is focused immediately after isRenaming=true, while blur-commit is
|
|
// gated by a 400ms ref so the initial focus handoff does not fire onBlur.
|
|
// Commit on Enter, cancel on Escape, commit on blur.
|
|
// We test the user-visible flow only (no direct DOM hacks), so the 400ms
|
|
// blur gate is exercised implicitly.
|
|
|
|
async function setupTask(
|
|
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;
|
|
}
|
|
|
|
async function startRenameViaContextMenu(testPage: Page, node: ReturnType<Page["locator"]>) {
|
|
await node.click({ button: "right" });
|
|
const renameItem = testPage.getByRole("menuitem", { name: "Rename" });
|
|
await expect(renameItem).toBeVisible({ timeout: 5_000 });
|
|
await renameItem.click();
|
|
// The input is auto-focused ~150ms after isRenaming flips. The Input inside
|
|
// the row is the only one with this className combo; locate via role.
|
|
const input = node.getByRole("textbox");
|
|
await expect(input).toBeVisible({ timeout: 2_000 });
|
|
await expect(input).toBeFocused();
|
|
return input;
|
|
}
|
|
|
|
test.describe("File tree inline rename", () => {
|
|
test("rename commits on Enter and the new file appears on disk", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
|
const git = new GitHelper(repoDir, makeGitEnv(backend.tmpDir));
|
|
git.createFile("rename-me.ts", "hello");
|
|
git.stageAll();
|
|
git.commit("seed rename file");
|
|
|
|
const session = await setupTask(testPage, apiClient, seedData, "ft-rename", "FT Rename Enter");
|
|
|
|
const node = session.fileTreeNode("rename-me.ts");
|
|
await expect(node).toBeVisible({ timeout: 15_000 });
|
|
|
|
const input = await startRenameViaContextMenu(testPage, node);
|
|
// Select-all then type the new name (the hook also calls .select() but
|
|
// doing it explicitly is robust against focus-timing edge cases).
|
|
await input.press("ControlOrMeta+A");
|
|
await input.fill("renamed.ts");
|
|
await input.press("Enter");
|
|
|
|
// Old node disappears from the tree, new node appears.
|
|
await expect(session.fileTreeNode("rename-me.ts")).toHaveCount(0, { timeout: 10_000 });
|
|
await expect(session.fileTreeNode("renamed.ts")).toBeVisible({ timeout: 10_000 });
|
|
|
|
// And the rename hit the file system.
|
|
await expect
|
|
.poll(() => fs.existsSync(path.join(repoDir, "renamed.ts")), { timeout: 10_000 })
|
|
.toBe(true);
|
|
expect(fs.existsSync(path.join(repoDir, "rename-me.ts"))).toBe(false);
|
|
});
|
|
|
|
test("rename cancels on Escape and leaves the tree and disk untouched", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
|
const git = new GitHelper(repoDir, makeGitEnv(backend.tmpDir));
|
|
git.createFile("keep-name.ts", "stay");
|
|
git.stageAll();
|
|
git.commit("seed keep file");
|
|
|
|
const session = await setupTask(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-rename-esc",
|
|
"FT Rename Escape",
|
|
);
|
|
|
|
const node = session.fileTreeNode("keep-name.ts");
|
|
await expect(node).toBeVisible({ timeout: 15_000 });
|
|
|
|
const input = await startRenameViaContextMenu(testPage, node);
|
|
await input.press("ControlOrMeta+A");
|
|
await input.fill("nope.ts");
|
|
await input.press("Escape");
|
|
|
|
// No mutation - original node still present, no renamed node, disk unchanged.
|
|
await expect(session.fileTreeNode("keep-name.ts")).toBeVisible({ timeout: 5_000 });
|
|
await expect(session.fileTreeNode("nope.ts")).toHaveCount(0);
|
|
expect(fs.existsSync(path.join(repoDir, "keep-name.ts"))).toBe(true);
|
|
expect(fs.existsSync(path.join(repoDir, "nope.ts"))).toBe(false);
|
|
});
|
|
|
|
test("rename commits on blur after typing a new name", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
|
const git = new GitHelper(repoDir, makeGitEnv(backend.tmpDir));
|
|
git.createFile("blur-original.ts", "blur");
|
|
git.createFile("other.ts", "other");
|
|
git.stageAll();
|
|
git.commit("seed blur file");
|
|
|
|
const session = await setupTask(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-rename-blur",
|
|
"FT Rename Blur",
|
|
);
|
|
|
|
const node = session.fileTreeNode("blur-original.ts");
|
|
await expect(node).toBeVisible({ timeout: 15_000 });
|
|
|
|
const input = await startRenameViaContextMenu(testPage, node);
|
|
await input.press("ControlOrMeta+A");
|
|
await input.fill("blur-final.ts");
|
|
await dwell(
|
|
testPage,
|
|
500,
|
|
"product-timer",
|
|
"the product gates blur-commit on a ~400ms timer after isRenaming flips; that timer publishes nothing to observe, so the wait has to outlast it",
|
|
);
|
|
// Click another file to blur the input. The other node also belongs to
|
|
// the tree, so we don't lose tree-container focus state.
|
|
await session.fileTreeNode("other.ts").click();
|
|
|
|
await expect(session.fileTreeNode("blur-final.ts")).toBeVisible({ timeout: 10_000 });
|
|
await expect(session.fileTreeNode("blur-original.ts")).toHaveCount(0);
|
|
await expect
|
|
.poll(() => fs.existsSync(path.join(repoDir, "blur-final.ts")), { timeout: 10_000 })
|
|
.toBe(true);
|
|
});
|
|
|
|
test("rename is a no-op when the name is unchanged", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
|
const git = new GitHelper(repoDir, makeGitEnv(backend.tmpDir));
|
|
git.createFile("noop.ts", "noop");
|
|
git.stageAll();
|
|
git.commit("seed noop");
|
|
|
|
const session = await setupTask(
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
"ft-rename-noop",
|
|
"FT Rename NoOp",
|
|
);
|
|
|
|
const node = session.fileTreeNode("noop.ts");
|
|
await expect(node).toBeVisible({ timeout: 15_000 });
|
|
|
|
const input = await startRenameViaContextMenu(testPage, node);
|
|
// Don't change anything, just press Enter.
|
|
await input.press("Enter");
|
|
|
|
await expect(session.fileTreeNode("noop.ts")).toBeVisible({ timeout: 5_000 });
|
|
expect(fs.existsSync(path.join(repoDir, "noop.ts"))).toBe(true);
|
|
});
|
|
});
|