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:
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user