Files
openidea/e2e/gitIgnore.spec.ts
T
GuoYuandguoyucode d9d1caa476 feat(vcs): add Add to .gitignore to the commit changes menu (#371)
Unversioned files and folders in the Commit tool window's changes menu offer Add to .gitignore / Add to .git/info/exclude, the same action as the Project view's Git submenu. vcs_add_to_ignore returns the ignore files it changed and the IDE opens the nearest one after writing. A selected symlink or Windows junction is ignored by name without a trailing slash; symlinked ancestors are still refused, and already-ignored paths are left unchanged.

Co-authored-by: guoyucode <29471644+guoyucode@users.noreply.github.com>
2026-10-30 15:09:55 +08:00

117 lines
5.2 KiB
TypeScript

/** Linux browser acceptance: synthetic MemoryFs + mocked native IPC, real menu/action/refresh UI. */
import { expect, test } from "./fixtures";
for (const destination of ["gitignore", "localExclude"] as const) {
test(`ignore ${destination}: menu dispatch and immediate tree/status refresh`, async ({ workbench: page }) => {
await page.evaluate(async (destination) => {
const fsModule = "/src/features/project-tree/fs.ts";
const storeModule = "/src/features/project-tree/store.ts";
const ipcModule = "/src/platform/ipc.ts";
const { projectFs } = await import(fsModule);
const { reloadAll } = await import(storeModule);
const { commands } = await import(ipcModule);
const root = "/dev/acme-api";
const path = `${root}/scratch.log`;
const fs = projectFs(root);
await fs.createFile(path);
let written = false;
commands.vcsAddToIgnore = async (paths: string[], actual: string) => {
if (actual !== destination || paths.length !== 1 || paths[0] !== path)
throw new Error("Wrong ignore destination or paths");
written = true;
fs.ignored.add(path);
return {
status: "ok",
data: [destination === "gitignore" ? `${root}/.gitignore` : `${root}/.git/info/exclude`],
};
};
commands.vcsStatus = async () => {
document.body.dataset.ignoreStatusRefresh = written ? "after-write" : "before-write";
return {
status: "ok",
data: written
? []
: [{ path, relPath: "scratch.log", staged: null, unstaged: "untracked", renamedFrom: null }],
};
};
commands.vcsRoots = async () => ({
status: "ok",
data: [{ root, operation: null, head: "main", headCommit: null }],
});
commands.vcsBranchInfo = async () => ({
status: "ok",
data: { head: "main", headCommit: null, upstream: null, ahead: 0, behind: 0, operation: null },
});
const vcsModule = "/src/features/vcs/store.ts";
const { useVcs } = await import(vcsModule);
useVcs.setState({ available: true, root, repo: root, branchesLoaded: false, stashesLoaded: false });
await reloadAll();
const banner = document.createElement("div");
banner.textContent = "LINUX BROWSER E2E · SYNTHETIC FIXTURE · NATIVE IPC MOCKED";
Object.assign(banner.style, {
position: "fixed",
bottom: "28px",
right: "18px",
zIndex: "999999",
padding: "10px 14px",
background: "#fff4ce",
color: "#362a00",
font: "bold 12px sans-serif",
border: "1px solid #9e8500",
borderRadius: "5px",
});
document.body.append(banner);
}, destination);
await page.getByRole("treeitem", { name: "scratch.log", exact: true }).click({ button: "right" });
await page.getByRole("menuitem", { name: "Git", exact: true }).hover();
await page.getByRole("menuitem", { name: "Add to .gitignore", exact: true }).hover();
await expect(page.getByRole("menuitem", { name: ".gitignore", exact: true })).toBeVisible();
await expect(page.getByRole("menuitem", { name: ".git/info/exclude", exact: true })).toBeVisible();
const captureDir = process.env.OPENIDEA_ACCEPTANCE_DIR;
if (captureDir && destination === "gitignore")
await page.screenshot({ path: `${captureDir}/01-ignore-destinations.png` });
await page
.getByRole("menuitem", { name: destination === "gitignore" ? ".gitignore" : ".git/info/exclude", exact: true })
.click();
await expect
.poll(async () =>
page.evaluate(async () => {
const module = "/src/features/project-tree/store.ts";
const { useProjectTree } = await import(module);
return useProjectTree
.getState()
.listings.get("/dev/acme-api")
?.find((entry: { name: string }) => entry.name === "scratch.log")?.ignored;
}),
)
.toBe(true);
await expect
.poll(async () =>
page.evaluate(async () => {
const module = "/src/features/vcs/store.ts";
const { useVcs } = await import(module);
return useVcs.getState().status.length;
}),
)
.toBe(0);
await expect(page.locator("body")).toHaveAttribute("data-ignore-status-refresh", "after-write");
// Display fixture rule text in the actual editor. This is browser memory, not a native disk read.
await page.evaluate(async (destination) => {
const module = "/src/workbench/contributions.ts";
const { editorService } = await import(module);
await editorService().openFile(
`/dev/acme-api/${destination === "gitignore" ? ".gitignore" : ".git/info/exclude"}`,
{ focus: true },
);
}, destination);
const content = page.locator(".cm-editor .cm-content").first();
await content.fill(
`# Synthetic acceptance fixture: ${destination === "gitignore" ? "shared repository rules" : ".git/info/exclude: local rules (common to linked worktrees)"}\n/scratch.log\n`,
);
if (captureDir)
await page.screenshot({
path: `${captureDir}/${destination === "gitignore" ? "02-shared-ignore-effect" : "03-local-exclude-effect"}.png`,
});
});
}