Merge branch 'config-work' into dev
This commit is contained in:
@@ -200,16 +200,11 @@ export const gotoHome = async (page: Page) => {
|
||||
};
|
||||
|
||||
export const openSettings = async (page: Page) => {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
if (!serverId) {
|
||||
throw new Error("E2E_SERVER_ID is not set (expected from Playwright globalSetup).");
|
||||
}
|
||||
|
||||
// Navigate through the real app control so route changes stay aligned with UI behavior.
|
||||
const settingsButton = page.locator('[data-testid="sidebar-settings"]:visible').first();
|
||||
await expect(settingsButton).toBeVisible();
|
||||
await settingsButton.click();
|
||||
await expect(page).toHaveURL(new RegExp(`/h/${escapeRegex(serverId)}/settings$`));
|
||||
await expect(page).toHaveURL(/\/settings\/general$/);
|
||||
};
|
||||
|
||||
export const setWorkingDirectory = async (page: Page, directory: string) => {
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { test, expect, type Page } from "./fixtures";
|
||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||
import { TEST_HOST_LABEL } from "./helpers/daemon-registry";
|
||||
|
||||
function getSeededServerId(): string {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
if (!serverId) {
|
||||
throw new Error("E2E_SERVER_ID is not set (expected from Playwright globalSetup).");
|
||||
}
|
||||
return serverId;
|
||||
}
|
||||
|
||||
function getSeededDaemonPort(): string {
|
||||
const port = process.env.E2E_DAEMON_PORT;
|
||||
if (!port) {
|
||||
throw new Error("E2E_DAEMON_PORT is not set (expected from Playwright globalSetup).");
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
async function openHostPage(page: Page, serverId: string) {
|
||||
await page.getByTestId(`settings-host-entry-${serverId}`).click();
|
||||
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("Settings host page", () => {
|
||||
test("host page shows seeded label, connection endpoint, inject MCP toggle, and all action rows", async ({
|
||||
page,
|
||||
}) => {
|
||||
const serverId = getSeededServerId();
|
||||
const port = getSeededDaemonPort();
|
||||
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
await openHostPage(page, serverId);
|
||||
|
||||
// Label renders as a title with a pencil edit affordance; the input is hidden until edit.
|
||||
await expect(page.getByTestId("host-page-label-card")).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-label-edit-button")).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-label-input")).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("host-page-label-card").getByText(TEST_HOST_LABEL, { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// Desktop detail pane shows a ScreenHeader with the host label.
|
||||
await expect(page.getByTestId("settings-detail-header-title")).toHaveText(TEST_HOST_LABEL);
|
||||
|
||||
// Connections is its own section with a "Connections" heading and the seeded endpoint row.
|
||||
const connectionsCard = page.getByTestId("host-page-connections-card");
|
||||
await expect(connectionsCard).toBeVisible();
|
||||
await expect(page.getByText("Connections", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
connectionsCard.getByText(new RegExp(`TCP \\((localhost|127\\.0\\.0\\.1):${port}\\)`)),
|
||||
).toBeVisible();
|
||||
|
||||
const injectMcpCard = page.getByTestId("host-page-inject-mcp-card");
|
||||
await expect(injectMcpCard).toBeVisible();
|
||||
await expect(injectMcpCard.getByRole("button", { name: "On", exact: true })).toBeVisible();
|
||||
await expect(injectMcpCard.getByRole("button", { name: "Off", exact: true })).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("host-page-restart-card")).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-restart-button")).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-providers-card")).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-remove-host-button")).toBeVisible();
|
||||
});
|
||||
|
||||
test("clicking the label pencil reveals the inline editor", async ({ page }) => {
|
||||
const serverId = getSeededServerId();
|
||||
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
await openHostPage(page, serverId);
|
||||
|
||||
await expect(page.getByTestId("host-page-label-input")).toHaveCount(0);
|
||||
|
||||
await page.getByTestId("host-page-label-edit-button").click();
|
||||
|
||||
await expect(page.getByTestId("host-page-label-input")).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-label-input")).toHaveValue(TEST_HOST_LABEL);
|
||||
await expect(page.getByTestId("host-page-label-save")).toBeVisible();
|
||||
});
|
||||
|
||||
test("host page does not render pair-device or daemon-lifecycle rows for a remote daemon", async ({
|
||||
page,
|
||||
}) => {
|
||||
const serverId = getSeededServerId();
|
||||
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
await openHostPage(page, serverId);
|
||||
|
||||
// TODO: add local-daemon fixture for positive Pair/Daemon coverage.
|
||||
// Pair-device now lives behind a row that only the local host sees
|
||||
// (gated by useIsLocalDaemon); the seeded host is remote, so it must
|
||||
// not appear. The daemon-lifecycle card is still local-host only.
|
||||
await expect(page.getByTestId("host-page-pair-device-row")).toHaveCount(0);
|
||||
await expect(page.getByTestId("host-page-daemon-lifecycle-card")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("settings sidebar does not expose retired top-level sections", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
|
||||
const sidebar = page.getByTestId("settings-sidebar");
|
||||
await expect(sidebar).toBeVisible();
|
||||
|
||||
await expect(sidebar.getByRole("button", { name: "Hosts", exact: true })).toHaveCount(0);
|
||||
await expect(sidebar.getByRole("button", { name: "Providers", exact: true })).toHaveCount(0);
|
||||
await expect(sidebar.getByRole("button", { name: "Pair device", exact: true })).toHaveCount(0);
|
||||
await expect(sidebar.getByRole("button", { name: "Daemon", exact: true })).toHaveCount(0);
|
||||
|
||||
await expect(sidebar.getByRole("button", { name: "General", exact: true })).toBeVisible();
|
||||
await expect(sidebar.getByRole("button", { name: "Diagnostics", exact: true })).toBeVisible();
|
||||
await expect(sidebar.getByRole("button", { name: "About", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("navigating to /settings/hosts/[serverId] directly renders the host page", async ({
|
||||
page,
|
||||
}) => {
|
||||
const serverId = getSeededServerId();
|
||||
|
||||
await gotoAppShell(page);
|
||||
await page.goto(`/settings/hosts/${encodeURIComponent(serverId)}`);
|
||||
|
||||
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-label-card")).toBeVisible();
|
||||
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible();
|
||||
});
|
||||
|
||||
test("sidebar pins the local daemon host first with a Local marker", async ({ page }) => {
|
||||
const serverId = getSeededServerId();
|
||||
|
||||
// Simulate the Electron desktop bridge so `useIsLocalDaemon` resolves the
|
||||
// seeded host to the local daemon. `manageBuiltInDaemon: false` bypasses
|
||||
// the desktop bootstrap flow so only the sidebar's status query runs.
|
||||
await page.addInitScript((localServerId) => {
|
||||
localStorage.setItem(
|
||||
"@paseo:app-settings",
|
||||
JSON.stringify({ theme: "auto", manageBuiltInDaemon: false, sendBehavior: "interrupt" }),
|
||||
);
|
||||
(window as unknown as { paseoDesktop: unknown }).paseoDesktop = {
|
||||
platform: "darwin",
|
||||
invoke: async (command: string) => {
|
||||
if (command === "desktop_daemon_status") {
|
||||
return {
|
||||
serverId: localServerId,
|
||||
status: "running",
|
||||
listen: null,
|
||||
hostname: null,
|
||||
pid: null,
|
||||
home: "",
|
||||
version: null,
|
||||
desktopManaged: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getPendingOpenProject: async () => null,
|
||||
events: { on: async () => () => undefined },
|
||||
};
|
||||
}, serverId);
|
||||
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
|
||||
const sidebar = page.getByTestId("settings-sidebar");
|
||||
await expect(sidebar).toBeVisible({ timeout: 15000 });
|
||||
|
||||
const hostEntries = sidebar.locator('[data-testid^="settings-host-entry-"]');
|
||||
await expect(hostEntries.first()).toHaveAttribute(
|
||||
"data-testid",
|
||||
`settings-host-entry-${serverId}`,
|
||||
);
|
||||
|
||||
const localHostEntry = page.getByTestId(`settings-host-entry-${serverId}`);
|
||||
await expect(localHostEntry.getByTestId("settings-host-local-marker")).toBeVisible();
|
||||
await expect(localHostEntry.getByText("Local", { exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { test, expect, type Page } from "./fixtures";
|
||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||
|
||||
function escapeRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function getServerId(): string {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
if (!serverId) {
|
||||
throw new Error("E2E_SERVER_ID is not set (expected from Playwright globalSetup).");
|
||||
}
|
||||
return serverId;
|
||||
}
|
||||
|
||||
async function clickSidebarSection(page: Page, label: string) {
|
||||
const sidebar = page.getByTestId("settings-sidebar");
|
||||
await expect(sidebar).toBeVisible();
|
||||
await sidebar.getByRole("button", { name: label, exact: true }).click();
|
||||
}
|
||||
|
||||
test.describe("Settings sidebar navigation", () => {
|
||||
test("clicking a sidebar section updates the URL and renders the section", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
|
||||
await clickSidebarSection(page, "Diagnostics");
|
||||
await expect(page).toHaveURL(/\/settings\/diagnostics$/);
|
||||
await expect(page.getByRole("button", { name: "Play test" })).toBeVisible();
|
||||
await expect(page.getByTestId("settings-detail-header-title")).toHaveText("Diagnostics");
|
||||
|
||||
await clickSidebarSection(page, "About");
|
||||
await expect(page).toHaveURL(/\/settings\/about$/);
|
||||
await expect(page.getByText("Version", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByTestId("settings-detail-header-title")).toHaveText("About");
|
||||
|
||||
await clickSidebarSection(page, "General");
|
||||
await expect(page).toHaveURL(/\/settings\/general$/);
|
||||
await expect(page.getByText("Theme", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByTestId("settings-detail-header-title")).toHaveText("General");
|
||||
});
|
||||
|
||||
test("/h/[serverId]/settings redirects to /settings/hosts/[serverId]", async ({ page }) => {
|
||||
const serverId = getServerId();
|
||||
await gotoAppShell(page);
|
||||
await page.goto(`/h/${encodeURIComponent(serverId)}/settings`);
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/settings/hosts/${escapeRegex(encodeURIComponent(serverId))}$`),
|
||||
);
|
||||
});
|
||||
|
||||
test("the + Add host button opens the add-host method modal", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
|
||||
await page.getByTestId("settings-add-host").click();
|
||||
await expect(page.getByText("Add connection", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Direct connection" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Paste pairing link" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("sidebar shows a Back to workspace row that leaves /settings", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
|
||||
const backRow = page.getByTestId("settings-back-to-workspace");
|
||||
await expect(backRow).toBeVisible();
|
||||
|
||||
await backRow.click();
|
||||
await expect(page).not.toHaveURL(/\/settings(\/|$)/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Settings — compact master-detail", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
|
||||
async function openCompactSettingsRoot(page: Page) {
|
||||
await gotoAppShell(page);
|
||||
// Wait for bootstrap to settle on a host route so storeReady is true before
|
||||
// we navigate into the protected settings stack. A direct page.goto("/settings")
|
||||
// on a cold load is eaten by Stack.Protected while bootstrap is still running.
|
||||
await expect(page).toHaveURL(/\/h\/|\/welcome/, { timeout: 15000 });
|
||||
|
||||
// Drive navigation the same way a user would: open the mobile drawer and tap
|
||||
// the Settings footer icon. This preserves the in-app router state instead of
|
||||
// triggering a full reload through Stack.Protected.
|
||||
await page.getByRole("button", { name: "Open menu", exact: true }).first().click();
|
||||
const sidebarSettingsButton = page.locator('[data-testid="sidebar-settings"]:visible').first();
|
||||
await expect(sidebarSettingsButton).toBeVisible();
|
||||
await sidebarSettingsButton.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/settings$/);
|
||||
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
||||
}
|
||||
|
||||
test("/settings renders only the sidebar list (no section content)", async ({ page }) => {
|
||||
await openCompactSettingsRoot(page);
|
||||
|
||||
// Sidebar rows are present.
|
||||
await expect(
|
||||
page.getByTestId("settings-sidebar").getByRole("button", { name: "General", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("settings-sidebar")
|
||||
.getByRole("button", { name: "Diagnostics", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("settings-sidebar").getByRole("button", { name: "About", exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// Section detail content is NOT rendered at the root.
|
||||
await expect(page.getByText("Theme", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Play test" })).toHaveCount(0);
|
||||
await expect(page.getByTestId("host-page-label-card")).toHaveCount(0);
|
||||
|
||||
// Root shows the menu header, not a back button.
|
||||
await expect(page.getByRole("button", { name: "Back", exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("tapping a section pushes /settings/[section] and shows a back button", async ({ page }) => {
|
||||
await openCompactSettingsRoot(page);
|
||||
|
||||
await page
|
||||
.getByTestId("settings-sidebar")
|
||||
.getByRole("button", { name: "Diagnostics", exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL(/\/settings\/diagnostics$/);
|
||||
await expect(page.getByRole("button", { name: "Play test" })).toBeVisible();
|
||||
// Sidebar is no longer visible — we are on a detail screen.
|
||||
// (Expo Router stack keeps the previous screen in the DOM but hidden; check
|
||||
// only visible instances.)
|
||||
await expect(page.locator('[data-testid="settings-sidebar"]:visible')).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Back", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("back from a section detail returns to the /settings list", async ({ page }) => {
|
||||
await openCompactSettingsRoot(page);
|
||||
|
||||
await page
|
||||
.getByTestId("settings-sidebar")
|
||||
.getByRole("button", { name: "About", exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL(/\/settings\/about$/);
|
||||
|
||||
await page.getByRole("button", { name: "Back", exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/settings$/);
|
||||
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back", exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("tapping a host entry pushes /settings/hosts/[serverId]", async ({ page }) => {
|
||||
const serverId = getServerId();
|
||||
await openCompactSettingsRoot(page);
|
||||
|
||||
await page.getByTestId(`settings-host-entry-${serverId}`).click();
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/settings/hosts/${escapeRegex(encodeURIComponent(serverId))}$`),
|
||||
);
|
||||
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back", exact: true })).toBeVisible();
|
||||
await expect(page.locator('[data-testid="settings-sidebar"]:visible')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("back from a host detail returns to the /settings list", async ({ page }) => {
|
||||
const serverId = getServerId();
|
||||
await openCompactSettingsRoot(page);
|
||||
|
||||
await page.getByTestId(`settings-host-entry-${serverId}`).click();
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/settings/hosts/${escapeRegex(encodeURIComponent(serverId))}$`),
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Back", exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/settings$/);
|
||||
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user