mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Random host-local project IDs had replaced the persisted cross-host value carried by projectKey. Keep projectId for routing while reconciliation owns and persists projectKey for grouping.
246 lines
9.8 KiB
TypeScript
246 lines
9.8 KiB
TypeScript
import path from "node:path";
|
|
import { existsSync } from "node:fs";
|
|
import { test, expect, type Page } from "./fixtures";
|
|
import { gotoAppShell } from "./helpers/app";
|
|
import {
|
|
addProjectFlowInput,
|
|
chooseAddProjectMethod,
|
|
openAddProjectFlow,
|
|
} from "./helpers/add-project-flow";
|
|
import { expectOpenedProject } from "./helpers/project-picker-ui";
|
|
import { connectSeedClient, seedWorkspace } from "./helpers/seed-client";
|
|
import { getServerId } from "./helpers/server-id";
|
|
import { createTempGitRepo } from "./helpers/workspace";
|
|
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
|
|
|
function workspaceRowTestId(workspaceId: string): string {
|
|
return `sidebar-workspace-row-${getServerId()}:${workspaceId}`;
|
|
}
|
|
|
|
async function archiveWorkspaceFromSidebar(page: Page, workspaceId: string): Promise<void> {
|
|
const serverId = getServerId();
|
|
const row = page.getByTestId(workspaceRowTestId(workspaceId));
|
|
await expect(row).toBeVisible({ timeout: 30_000 });
|
|
await row.hover();
|
|
|
|
const kebab = page.getByTestId(`sidebar-workspace-kebab-${serverId}:${workspaceId}`);
|
|
await expect(kebab).toBeVisible({ timeout: 10_000 });
|
|
await kebab.click();
|
|
|
|
const archiveItem = page.getByTestId(`sidebar-workspace-menu-archive-${serverId}:${workspaceId}`);
|
|
await expect(archiveItem).toBeVisible({ timeout: 10_000 });
|
|
await archiveItem.click();
|
|
}
|
|
|
|
async function removeProjectFromSidebar(page: Page, projectKey: string): Promise<void> {
|
|
const projectRow = page.getByTestId(`sidebar-project-row-${projectKey}`);
|
|
await expect(projectRow).toBeVisible({ timeout: 30_000 });
|
|
await projectRow.hover();
|
|
|
|
const kebab = page.getByTestId(`sidebar-project-kebab-${projectKey}`);
|
|
await expect(kebab).toBeVisible({ timeout: 10_000 });
|
|
await kebab.click();
|
|
|
|
// Removing a project raises a browser confirm; accept it so the
|
|
// user-confirmed removal proceeds deterministically.
|
|
page.once("dialog", (dialog) => void dialog.accept());
|
|
|
|
const removeItem = page.getByTestId(`sidebar-project-menu-remove-${projectKey}`);
|
|
await expect(removeItem).toBeVisible({ timeout: 10_000 });
|
|
await removeItem.click();
|
|
}
|
|
|
|
async function addProjectFromPicker(page: Page, projectPath: string): Promise<string> {
|
|
await openAddProjectFlow(page);
|
|
await chooseAddProjectMethod(page, "directory-search");
|
|
|
|
const input = addProjectFlowInput(page);
|
|
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-", "");
|
|
}
|
|
|
|
async function waitForSidebarProjectListReady(page: Page): Promise<void> {
|
|
await page
|
|
.locator('[data-testid="sidebar-project-empty-state"], [data-testid^="sidebar-project-row-"]')
|
|
.first()
|
|
.waitFor({ state: "visible", timeout: 60_000 });
|
|
}
|
|
|
|
test.describe("Project picker search", () => {
|
|
test("opens a project from a fuzzy directory-name search", async ({
|
|
page,
|
|
projectPickerFixture,
|
|
}) => {
|
|
await gotoAppShell(page);
|
|
await waitForSidebarProjectListReady(page);
|
|
await openAddProjectFlow(page);
|
|
await chooseAddProjectMethod(page, "directory-search");
|
|
|
|
const input = addProjectFlowInput(page);
|
|
await input.fill(projectPickerFixture.fuzzyQuery);
|
|
|
|
const suggestion = page.getByText(projectPickerFixture.projectName, { exact: false }).first();
|
|
await expect(suggestion).toBeVisible({ timeout: 30_000 });
|
|
await suggestion.click();
|
|
|
|
const projectId = await expectOpenedProject(page, projectPickerFixture.projectName);
|
|
projectPickerFixture.rememberProjectId(projectId);
|
|
});
|
|
|
|
test("shows a loading state after typing while directory suggestions are pending", async ({
|
|
page,
|
|
}) => {
|
|
await gotoAppShell(page);
|
|
await waitForSidebarProjectListReady(page);
|
|
await openAddProjectFlow(page);
|
|
await chooseAddProjectMethod(page, "directory-search");
|
|
|
|
const input = addProjectFlowInput(page);
|
|
await input.fill("paseo-loading-state-no-match");
|
|
|
|
await expect(page.getByText("Start typing a path", { exact: true })).toHaveCount(0);
|
|
await expect(page.getByText("Loading...", { exact: true })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// 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 waitForSidebarProjectListReady(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 client.removeProject(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,
|
|
}) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "empty-project-persists-" });
|
|
|
|
try {
|
|
const projectRow = page.getByTestId(`sidebar-project-row-${workspace.projectKey}`);
|
|
const newWorkspaceRow = page.getByTestId(
|
|
`sidebar-project-new-workspace-row-${workspace.projectKey}`,
|
|
);
|
|
const globalNewWorkspace = page.getByTestId("sidebar-global-new-workspace");
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await expect(projectRow).toBeVisible({ timeout: 30_000 });
|
|
const workspaceRow = page.getByTestId(workspaceRowTestId(workspace.workspaceId));
|
|
await expect(workspaceRow).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await workspaceRow.click();
|
|
await expect(page.getByTestId("changes-primary-cta")).toHaveCount(0);
|
|
|
|
await archiveWorkspaceFromSidebar(page, workspace.workspaceId);
|
|
|
|
// 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,
|
|
});
|
|
expect(existsSync(workspace.repoPath)).toBe(true);
|
|
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 });
|
|
|
|
// 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 expect(newWorkspaceRow).toBeVisible({ timeout: 30_000 });
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe("Project remove", () => {
|
|
test("removing a project from project actions removes it from the sidebar", async ({ page }) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "project-remove-sidebar-" });
|
|
let readdedProjectId: string | null = null;
|
|
|
|
try {
|
|
const projectRow = page.getByTestId(`sidebar-project-row-${workspace.projectKey}`);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await expect(projectRow).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.getByTestId(workspaceRowTestId(workspace.workspaceId))).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
|
|
await removeProjectFromSidebar(page, workspace.projectKey);
|
|
|
|
await expect(page.getByTestId(workspaceRowTestId(workspace.workspaceId))).toHaveCount(0, {
|
|
timeout: 30_000,
|
|
});
|
|
await expect(projectRow).toHaveCount(0, { timeout: 30_000 });
|
|
|
|
await page.reload();
|
|
await waitForSidebarProjectListReady(page);
|
|
await expect(projectRow).toHaveCount(0, { timeout: 30_000 });
|
|
|
|
const readded = await workspace.client.addProject(workspace.repoPath);
|
|
expect(readded.error).toBeNull();
|
|
expect(readded.project).not.toBeNull();
|
|
readdedProjectId = readded.project?.projectId ?? "";
|
|
const readdedProjectKey = readded.project?.projectKey ?? "";
|
|
expect(readdedProjectId).not.toBe(workspace.projectId);
|
|
expect(readdedProjectKey).toBe(workspace.projectKey);
|
|
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-${readdedProjectKey}`),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
} finally {
|
|
if (readdedProjectId) {
|
|
await workspace.client.removeProject(readdedProjectId).catch(() => undefined);
|
|
}
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
});
|