дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
Attach adds a trusted folder to the current project without reopening it or moving its root. Attachments live in the primary project's private .openidea/workspace.json, and detaching the last one restores a plain project. ProjectInfo.name stays the primary project's name; displayName supplies window titles, project tabs and Recent Projects. Attach is no longer a remembered opening policy, and File > Attach Project is available when the policy is not Ask. Attached folders must be trusted, and Restricted Mode names any attached folder that blocks trust. Explicit workspaces keep their behaviour and still cannot be created in or inside a linked project.
160 рядки
8.7 KiB
TypeScript
160 рядки
8.7 KiB
TypeScript
import { expect, ipcCalls, openFromTree, stubDesktop, test } from "./fixtures";
|
|
|
|
test.use({ video: "on" });
|
|
|
|
test("Attach preserves unsaved tabs and detaches back to the plain project", async ({ workbench: page }) => {
|
|
const editor = await openFromTree(page, "README.md");
|
|
await editor.locator(".cm-content").click();
|
|
await page.keyboard.press("Meta+End");
|
|
await page.keyboard.type("unsaved attach text");
|
|
await page.screenshot({ path: test.info().outputPath("plain-project.png"), animations: "disabled" });
|
|
await stubDesktop(page);
|
|
await page.evaluate(async () => {
|
|
const module = "/src/platform/project.ts";
|
|
const { useProject } = await import(/* @vite-ignore */ module);
|
|
const project = useProject.getState().project;
|
|
useProject.setState({
|
|
settings: { ...project.settings, general: { ...project.settings.general, openProjectIn: "ask" } },
|
|
});
|
|
const attached = {
|
|
...project,
|
|
displayName: "acme-api, other",
|
|
workspace: { version: 1, name: project.name, projects: [project.root, "/dev/other"] },
|
|
};
|
|
let current = project;
|
|
window.__ipc.handlers.project_trust_status = ({ path }) => ({ root: path, trusted: true, decided: true });
|
|
window.__ipc.handlers.workspace_attach = () => {
|
|
current = attached;
|
|
return project.root;
|
|
};
|
|
window.__ipc.handlers.project_current = () => current;
|
|
window.__ipc.handlers.menu_refresh = () => null;
|
|
window.__ipc.handlers.project_rename = ({ name }) => {
|
|
current = { ...attached, name, displayName: `${name}, other` };
|
|
return current;
|
|
};
|
|
window.__ipc.handlers.workspace_update = () => {
|
|
current = project;
|
|
return project;
|
|
};
|
|
window.__ipc.handlers.project_roots = () => ({ folders: [] });
|
|
window.__ipc.handlers.fs_read_dir = () => [];
|
|
window.__ipc.handlers.run_configs = () => [];
|
|
window.__ipc.handlers.recent_projects = () => [
|
|
{ path: current.root, name: current.displayName || current.name, branch: null, openedAtMs: Date.now() },
|
|
];
|
|
const windows = "/src/features/app/windows.ts";
|
|
void (await import(/* @vite-ignore */ windows)).openProjectWithPolicy("/dev/other");
|
|
});
|
|
const dialog = page.getByRole("dialog", { name: "Open Project" });
|
|
await expect(dialog).toBeVisible();
|
|
await page.screenshot({ path: test.info().outputPath("attach-dialog.png"), animations: "disabled" });
|
|
await dialog.getByRole("button", { name: "Attach", exact: true }).click();
|
|
await expect(
|
|
page.locator("header").first().getByRole("button", { name: "acme-api, other", exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.getByRole("treeitem", { name: /^other / })).toBeVisible();
|
|
await expect(editor.locator(".cm-content")).toContainText("unsaved attach text");
|
|
expect(await ipcCalls(page, "open_project")).toHaveLength(0);
|
|
expect(await ipcCalls(page, "workspace_attach")).toHaveLength(1);
|
|
await page.evaluate(async () => {
|
|
const actions = "/src/platform/actions.ts";
|
|
void (await import(/* @vite-ignore */ actions)).executeAction("file.renameProject");
|
|
});
|
|
const rename = page.getByRole("dialog", { name: "Rename Project", exact: true });
|
|
await expect(rename.getByRole("textbox")).toHaveValue("acme-api");
|
|
await rename.getByRole("button", { name: "Rename", exact: true }).click();
|
|
await expect(
|
|
page.locator("header").first().getByRole("button", { name: "acme-api, other", exact: true }),
|
|
).toBeVisible();
|
|
expect((await ipcCalls(page, "project_rename"))[0]!.name).toBe("acme-api");
|
|
await page.screenshot({ path: test.info().outputPath("attached-project.png"), animations: "disabled" });
|
|
await page.locator("header").first().getByRole("button", { name: "acme-api, other", exact: true }).click();
|
|
await expect(page.getByText("/dev/acme-api", { exact: true }).last()).toBeVisible();
|
|
await page.screenshot({ path: test.info().outputPath("attached-recent.png"), animations: "disabled" });
|
|
await page.keyboard.press("Escape");
|
|
await page.getByRole("treeitem", { name: /^other / }).click({ button: "right" });
|
|
await expect(page.getByRole("menuitem", { name: "Detach Project", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("menuitem", { name: "Remove from Workspace" })).toHaveCount(0);
|
|
await page.screenshot({ path: test.info().outputPath("detach-menu.png"), animations: "disabled" });
|
|
await page.getByRole("menuitem", { name: "Detach Project", exact: true }).click();
|
|
const detach = page.getByRole("dialog", { name: "Detach Project" });
|
|
await expect(detach).toBeVisible();
|
|
await detach.getByRole("button", { name: "Detach", exact: true }).click();
|
|
await expect(page.locator("header").first().getByRole("button", { name: "acme-api", exact: true })).toBeVisible();
|
|
await expect(editor.locator(".cm-content")).toContainText("unsaved attach text");
|
|
await expect(page.getByRole("treeitem", { name: /^other / })).toHaveCount(0);
|
|
await page.screenshot({ path: test.info().outputPath("detached-project.png"), animations: "disabled" });
|
|
});
|
|
|
|
test("restricting an attached folder leaves the current project untouched", async ({ workbench: page }) => {
|
|
await stubDesktop(page);
|
|
await page.evaluate(async () => {
|
|
const projectModule = "/src/platform/project.ts";
|
|
const { useProject } = await import(/* @vite-ignore */ projectModule);
|
|
useProject.setState({ settings: { ...useProject.getState().settings, general: { openProjectIn: "ask" } } });
|
|
let decided = false;
|
|
window.__ipc.handlers.project_trust_status = ({ path }) => ({ root: path, trusted: false, decided });
|
|
window.__ipc.handlers.project_trust_set = () => {
|
|
decided = true;
|
|
};
|
|
const module = "/src/features/app/windows.ts";
|
|
void (await import(/* @vite-ignore */ module)).openProjectWithPolicy("/dev/untrusted");
|
|
});
|
|
await page.getByRole("dialog", { name: "Open Project" }).getByRole("button", { name: "Attach", exact: true }).click();
|
|
const trust = page.getByRole("dialog", { name: "Trust and Open Project?" });
|
|
await expect(trust).toBeVisible();
|
|
await trust.getByRole("button", { name: "Preview in Restricted Mode" }).click();
|
|
await expect(trust).toBeHidden();
|
|
expect(await ipcCalls(page, "workspace_attach")).toHaveLength(0);
|
|
expect(await ipcCalls(page, "open_project")).toHaveLength(0);
|
|
await expect(page.locator("header").first().getByRole("button", { name: "acme-api", exact: true })).toBeVisible();
|
|
});
|
|
|
|
test("restricted attachments can be trusted individually from the banner", async ({ workbench: page }) => {
|
|
await stubDesktop(page);
|
|
await page.evaluate(async () => {
|
|
const m = "/src/platform/project.ts";
|
|
const { useProject } = await import(/* @vite-ignore */ m);
|
|
const primary = useProject.getState().project;
|
|
let current = {
|
|
...primary,
|
|
trusted: false,
|
|
displayName: "acme-api, other...",
|
|
workspace: { version: 1, name: primary.name, projects: [primary.root, "/dev/other", "/dev/third"] },
|
|
};
|
|
const trusted = new Set([primary.root]);
|
|
window.__ipc.handlers.project_trust_status = ({ path }) => ({
|
|
root: path,
|
|
trusted: trusted.has(path),
|
|
decided: true,
|
|
});
|
|
window.__ipc.handlers.project_trust_set = ({ path, trusted: allow }) => {
|
|
if (allow) trusted.add(path);
|
|
current = { ...current, trusted: current.workspace.projects.every((path) => trusted.has(path)) };
|
|
};
|
|
window.__ipc.handlers.project_current = () => current;
|
|
window.__ipc.handlers.fs_read_dir = () => [];
|
|
window.__ipc.handlers.project_roots = () => ({ folders: [] });
|
|
window.__ipc.handlers.run_configs = () => [];
|
|
useProject.setState({ project: current });
|
|
});
|
|
const banner = page.getByRole("status").filter({ hasText: "Restricted Mode" });
|
|
await expect(banner).toContainText("/dev/other");
|
|
await page.screenshot({ path: test.info().outputPath("restricted-member.png"), animations: "disabled" });
|
|
await banner.getByRole("button", { name: "Trust Project" }).click();
|
|
const dialog = page.getByRole("dialog", { name: "Trust and Open Project?" });
|
|
await expect(dialog).toContainText("/dev/other");
|
|
await dialog.getByRole("button", { name: "Don't Open" }).click();
|
|
expect(await ipcCalls(page, "project_trust_set")).toHaveLength(0);
|
|
await banner.getByRole("button", { name: "Trust Project" }).click();
|
|
await dialog.getByRole("button", { name: "Trust Project", exact: true }).click();
|
|
await expect(banner).toContainText("/dev/third");
|
|
expect(await ipcCalls(page, "project_trust_set")).toEqual([{ path: "/dev/other", trusted: true, folder: false }]);
|
|
await banner.getByRole("button", { name: "Trust Project" }).click();
|
|
await expect(dialog).toContainText("/dev/third");
|
|
await dialog.getByRole("button", { name: "Trust Project", exact: true }).click();
|
|
await expect(banner).toBeHidden();
|
|
await page.screenshot({ path: test.info().outputPath("trusted-members.png"), animations: "disabled" });
|
|
});
|