Add projects without creating workspaces

Keep the legacy open-project request creating a workspace for old clients; new clients now add projects and create workspaces through separate flows.
This commit is contained in:
Mohamed Boudra
2026-06-20 11:56:28 +07:00
parent d1481833e6
commit 4534754617
36 changed files with 1653 additions and 1723 deletions
+71 -21
View File
@@ -1,8 +1,10 @@
import path from "node:path";
import { test, expect, type Page } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import { seedWorkspace } from "./helpers/seed-client";
import { connectSeedClient, seedWorkspace } from "./helpers/seed-client";
import { getServerId } from "./helpers/server-id";
import { waitForSidebarHydration } from "./helpers/workspace-ui";
import { createTempGitRepo } from "./helpers/workspace";
import { waitForNoProjectsInSidebar, waitForSidebarHydration } from "./helpers/workspace-ui";
function workspaceRowTestId(workspaceId: string): string {
return `sidebar-workspace-row-${getServerId()}:${workspaceId}`;
@@ -45,11 +47,58 @@ async function removeProjectFromSidebar(page: Page, projectId: string): Promise<
await removeItem.click();
}
// Model B makes the project a first-class parent: archiving its last workspace
// must not delete the project. The per-project "+ New workspace" row is gone;
// the empty project keeps its parent row, and creation stays reachable from the
// project row's own new-worktree icon (git projects) and the global button.
test.describe("Empty project persists", () => {
async function addProjectFromPicker(page: Page, projectPath: string): Promise<string> {
await page.getByTestId("sidebar-add-project").click();
const input = page.getByPlaceholder("Type a directory path...");
await expect(input).toBeVisible({ timeout: 30_000 });
await input.fill(projectPath);
await page.keyboard.press("Enter");
const projectRow = page
.locator('[data-testid^="sidebar-project-row-"]')
.filter({ hasText: path.basename(projectPath) })
.first();
await expect(projectRow).toBeVisible({ timeout: 30_000 });
const testId = await projectRow.getAttribute("data-testid");
expect(testId).not.toBeNull();
return testId!.replace("sidebar-project-row-", "");
}
// Projects are parents in the sidebar. Archiving the last workspace leaves the
// project row in place with a ghost "+ New workspace" child row.
test.describe("Project with no workspaces persists", () => {
test("adding a project starts with only a new-workspace child row", async ({ page }) => {
const repo = await createTempGitRepo("empty-project-add-");
const client = await connectSeedClient();
let projectId: string | null = null;
try {
await gotoAppShell(page);
await waitForNoProjectsInSidebar(page);
projectId = await addProjectFromPicker(page, repo.path);
const projectRow = page.getByTestId(`sidebar-project-row-${projectId}`);
await expect(projectRow).toBeVisible({ timeout: 30_000 });
await expect(projectRow).toContainText(path.basename(repo.path));
await expect(page.getByTestId(`sidebar-workspace-list-${projectId}`)).toHaveCount(0);
const newWorkspaceRow = page.getByTestId(`sidebar-project-new-workspace-row-${projectId}`);
await expect(newWorkspaceRow).toBeVisible({ timeout: 30_000 });
await expect(newWorkspaceRow).toContainText("New workspace");
const workspaces = await client.fetchWorkspaces({ filter: { projectId } });
expect(workspaces.entries).toEqual([]);
} finally {
if (projectId) {
await removeProjectFromSidebar(page, projectId).catch(() => undefined);
}
await client.close().catch(() => undefined);
await repo.cleanup().catch(() => undefined);
}
});
test("archiving the only workspace keeps the project row with creation still reachable", async ({
page,
}) => {
@@ -57,8 +106,8 @@ test.describe("Empty project persists", () => {
try {
const projectRow = page.getByTestId(`sidebar-project-row-${workspace.projectId}`);
const projectNewWorktreeIcon = page.getByTestId(
`sidebar-project-new-worktree-${workspace.projectId}`,
const newWorkspaceRow = page.getByTestId(
`sidebar-project-new-workspace-row-${workspace.projectId}`,
);
const globalNewWorkspace = page.getByTestId("sidebar-global-new-workspace");
@@ -71,24 +120,21 @@ test.describe("Empty project persists", () => {
await hideWorkspaceFromSidebar(page, workspace.workspaceId);
// The workspace row goes away, but its project parent stays as an empty
// project row. Creation is still reachable: the project row keeps its own
// new-worktree icon (revealed on hover) and the global button persists.
// The workspace row goes away, but its project parent stays and exposes a
// child row for creating the next workspace.
await expect(page.getByTestId(workspaceRowTestId(workspace.workspaceId))).toHaveCount(0, {
timeout: 30_000,
});
await expect(projectRow).toBeVisible({ timeout: 30_000 });
await expect(newWorkspaceRow).toBeVisible({ timeout: 30_000 });
await expect(newWorkspaceRow).toContainText("New workspace");
await expect(globalNewWorkspace).toBeVisible({ timeout: 30_000 });
await projectRow.hover();
await expect(projectNewWorktreeIcon).toBeVisible({ timeout: 30_000 });
// The empty project survives a reload — it is persisted, not a transient
// artifact of the just-archived workspace still lingering in memory.
// The project survives a reload after its last workspace is archived.
await page.reload();
await waitForSidebarHydration(page);
await expect(projectRow).toBeVisible({ timeout: 30_000 });
await projectRow.hover();
await expect(projectNewWorktreeIcon).toBeVisible({ timeout: 30_000 });
await expect(newWorkspaceRow).toBeVisible({ timeout: 30_000 });
} finally {
await workspace.cleanup();
}
@@ -120,16 +166,20 @@ test.describe("Project remove", () => {
await waitForSidebarHydration(page);
await expect(projectRow).toHaveCount(0, { timeout: 30_000 });
const reopened = await workspace.client.openProject(workspace.repoPath);
expect(reopened.error).toBeNull();
expect(reopened.workspace?.projectDisplayName).toBe(workspace.projectDisplayName);
const readded = await workspace.client.addProject(workspace.repoPath);
expect(readded.error).toBeNull();
expect(readded.project?.projectDisplayName).toBe(workspace.projectDisplayName);
await page.reload();
await waitForSidebarHydration(page);
await expect(projectRow).toBeVisible({ timeout: 30_000 });
await expect(projectRow).toContainText(workspace.projectDisplayName);
await expect(projectRow).not.toContainText(workspace.repoPath);
await expect(
page.getByTestId(`sidebar-project-new-workspace-row-${workspace.projectId}`),
).toBeVisible({ timeout: 30_000 });
} finally {
await removeProjectFromSidebar(page, workspace.projectId).catch(() => undefined);
await workspace.cleanup();
}
});