Files
paseo/packages/app/e2e/helpers/project-grouping.ts
T
800bd2bef1 feat: add host-local custom project icons (#2335)
* feat(projects): add local icons and colors

* refactor(projects): simplify appearance handling

* fix(projects): serialize appearance saves

* test(app): update project appearance expectations

* test: fix project appearance CI expectations

* refactor(projects): organize appearance code

* fix(server): adopt main's reconcile model in project appearance updates

* fix(projects): preserve host-local appearance targets

* fix(desktop): restore Linux development startup

Ubuntu can block Electron's unprivileged user-namespace fallback while the npm-installed sandbox helper is not SUID. Disable the Chromium process sandbox only in the Linux development runner so packaged desktop policy remains unchanged.

* feat(projects): add custom project icons

Keep URL acquisition in the client so the host daemon only validates uploaded bytes. Project name and icon changes share one adaptive edit sheet.

* refactor(settings): scope projects by host

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-01 01:34:42 +08:00

137 lines
5.1 KiB
TypeScript

import { expect, type Locator, type Page } from "@playwright/test";
import { gotoAppShell } from "./app";
import { addConnectedHostsAndReload, waitForConnectedHost } from "./hosts";
import { openProjectSettings } from "./project-settings";
import { selectSettingsHost } from "./settings";
import { waitForSidebarHydration } from "./workspace-ui";
import { buildProjectsSettingsRoute } from "@/utils/host-routes";
const PROJECT_VISIBILITY_TIMEOUT = 30_000;
function projectGroup(page: Page, projectName: string): Locator {
return page.getByRole("group", { name: projectName, exact: true });
}
function projectGroupWithWorkspace(
page: Page,
projectName: string,
workspaceName: string,
): Locator {
return projectGroup(page, projectName).filter({ hasText: workspaceName });
}
export async function openProjectDirectory(page: Page): Promise<void> {
await gotoAppShell(page);
await waitForSidebarHydration(page);
}
export async function openProjectDirectoryWithHosts(
page: Page,
input: {
hosts: Array<{ serverId: string; label: string; port: number }>;
primaryLabel?: string;
},
): Promise<void> {
await gotoAppShell(page);
await addConnectedHostsAndReload(page, input.hosts, { primaryLabel: input.primaryLabel });
for (const host of input.hosts) {
await waitForConnectedHost(page, {
serverId: host.serverId,
endpoint: `localhost:${host.port}`,
});
}
await waitForSidebarHydration(page);
}
export async function expectProjectContainsWorkspaces(
page: Page,
input: { projectName: string; workspaceNames: string[] },
): Promise<void> {
const group = projectGroup(page, input.projectName);
await expect(group).toHaveCount(1, { timeout: PROJECT_VISIBILITY_TIMEOUT });
for (const workspaceName of input.workspaceNames) {
await expect(group.getByRole("button").filter({ hasText: workspaceName })).toBeVisible({
timeout: PROJECT_VISIBILITY_TIMEOUT,
});
}
}
export async function expectSeparateProjects(
page: Page,
projects: Array<{ projectName: string; workspaceName: string }>,
): Promise<void> {
for (const project of projects) {
await expect(
projectGroupWithWorkspace(page, project.projectName, project.workspaceName),
).toHaveCount(1, { timeout: PROJECT_VISIBILITY_TIMEOUT });
}
}
export async function beginWorkspaceFromProject(page: Page, projectName: string): Promise<void> {
const group = projectGroup(page, projectName);
await expect(group).toBeVisible({ timeout: PROJECT_VISIBILITY_TIMEOUT });
await group.hover();
await group.getByLabel(`Create a new workspace for ${projectName}`).click();
await expect(page.getByRole("button", { name: "Workspace project", exact: true })).toContainText(
projectName,
{ timeout: PROJECT_VISIBILITY_TIMEOUT },
);
}
export async function selectWorkspaceHost(page: Page, hostName: string): Promise<void> {
await page.getByRole("button", { name: "Host", exact: true }).click();
await page.getByRole("button", { name: hostName, exact: true }).click();
await expect(page.getByRole("button", { name: "Host", exact: true })).toContainText(hostName);
}
export async function createWorkspaceWithoutAgent(page: Page): Promise<void> {
await page.getByRole("button", { name: "Create", exact: true }).click();
}
export async function expectProjectWorkspaceCountForHost(
page: Page,
input: { projectName: string; hostName: string; count: number },
): Promise<void> {
const group = projectGroup(page, input.projectName);
await expect(group.getByRole("button").filter({ hasText: input.hostName })).toHaveCount(
input.count,
{ timeout: PROJECT_VISIBILITY_TIMEOUT },
);
}
export async function renameProject(page: Page, name: string): Promise<void> {
await page.getByRole("button", { name: "Edit project", exact: true }).click();
await page.getByRole("textbox", { name: "Project name", exact: true }).fill(name);
await page.getByRole("button", { name: "Save changes", exact: true }).click();
await expect(page.getByText("Project updated", { exact: true })).toBeVisible({
timeout: PROJECT_VISIBILITY_TIMEOUT,
});
}
export async function openGroupedProjectSettings(
page: Page,
input: { serverId: string; projectName: string },
): Promise<void> {
await page.getByRole("button", { name: "Settings", exact: true }).click();
await expect(page).toHaveURL(/\/settings\/general$/);
await selectSettingsHost(page, input.serverId);
await page.locator('[data-testid="settings-host-section-projects"]:visible').click();
await expect(page).toHaveURL(/\/settings\/hosts\/[^/]+\/projects$/);
await openProjectSettings(page, input.projectName);
}
export async function openProjectsForSettingsHost(
page: Page,
input: { serverId: string; projectName: string },
): Promise<void> {
await selectSettingsHost(page, input.serverId);
await expect(page).toHaveURL(buildProjectsSettingsRoute(input.serverId));
await openProjectSettings(page, input.projectName);
}
export async function expectProjectSettingsName(page: Page, projectName: string): Promise<void> {
await expect(page.getByRole("main").getByText(projectName, { exact: true })).toBeVisible({
timeout: PROJECT_VISIBILITY_TIMEOUT,
});
}