test(app,server): align e2e specs with settings redesign, drop rg dep
- worktree-core test scans the server source via a pure Node walker instead of shelling out to `rg`, which is not installed on CI runners. Invariant and scope are unchanged. - settings-host-page specs target the new detail-header label + rename affordance (settings-detail-header-title / host-page-label-edit-button) since the label card moved into the shared header as part of the modular settings refactor. - settings-navigation compact tests assert the new two-tier Back semantics: root Back exits settings, detail Back returns to /settings.
This commit is contained in:
@@ -23,6 +23,12 @@ async function openHostPage(page: Page, serverId: string) {
|
|||||||
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
|
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function expectHostLabelHeader(page: Page) {
|
||||||
|
await expect(page.getByTestId("settings-detail-header-title")).toHaveText(TEST_HOST_LABEL);
|
||||||
|
await expect(page.getByTestId("host-page-label-edit-button")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("host-page-label-input")).toHaveCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
test.describe("Settings host page", () => {
|
test.describe("Settings host page", () => {
|
||||||
test("host page shows seeded label, connection endpoint, inject MCP toggle, and all action rows", async ({
|
test("host page shows seeded label, connection endpoint, inject MCP toggle, and all action rows", async ({
|
||||||
page,
|
page,
|
||||||
@@ -34,16 +40,8 @@ test.describe("Settings host page", () => {
|
|||||||
await openSettings(page);
|
await openSettings(page);
|
||||||
await openHostPage(page, serverId);
|
await openHostPage(page, serverId);
|
||||||
|
|
||||||
// Label renders as a title with a pencil edit affordance; the input is hidden until edit.
|
// Label renders in the detail header with a pencil edit affordance; the input is hidden until edit.
|
||||||
await expect(page.getByTestId("host-page-label-card")).toBeVisible();
|
await expectHostLabelHeader(page);
|
||||||
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.
|
// Connections is its own section with a "Connections" heading and the seeded endpoint row.
|
||||||
const connectionsCard = page.getByTestId("host-page-connections-card");
|
const connectionsCard = page.getByTestId("host-page-connections-card");
|
||||||
@@ -124,7 +122,7 @@ test.describe("Settings host page", () => {
|
|||||||
await page.goto(`/settings/hosts/${encodeURIComponent(serverId)}`);
|
await page.goto(`/settings/hosts/${encodeURIComponent(serverId)}`);
|
||||||
|
|
||||||
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
|
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
|
||||||
await expect(page.getByTestId("host-page-label-card")).toBeVisible();
|
await expectHostLabelHeader(page);
|
||||||
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible();
|
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,15 @@ test.describe("Settings — compact master-detail", () => {
|
|||||||
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function expectCompactSettingsRootList(page: Page) {
|
||||||
|
await expect(page).toHaveURL(/\/settings$/);
|
||||||
|
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByText("Theme", { exact: true })).toHaveCount(0);
|
||||||
|
await expect(page.getByRole("button", { name: "Play test" })).toHaveCount(0);
|
||||||
|
await expect(page.locator('[data-testid^="settings-host-page-"]')).toHaveCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
test("/settings renders only the sidebar list (no section content)", async ({ page }) => {
|
test("/settings renders only the sidebar list (no section content)", async ({ page }) => {
|
||||||
await openCompactSettingsRoot(page);
|
await openCompactSettingsRoot(page);
|
||||||
|
|
||||||
@@ -110,12 +119,12 @@ test.describe("Settings — compact master-detail", () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Section detail content is NOT rendered at the root.
|
// Section detail content is NOT rendered at the root.
|
||||||
await expect(page.getByText("Theme", { exact: true })).toHaveCount(0);
|
await expectCompactSettingsRootList(page);
|
||||||
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.
|
const rootBackButton = page.getByRole("button", { name: "Back", exact: true });
|
||||||
await expect(page.getByRole("button", { name: "Back", exact: true })).toHaveCount(0);
|
await expect(rootBackButton).toBeVisible();
|
||||||
|
await rootBackButton.click();
|
||||||
|
await expect(page).not.toHaveURL(/\/settings(\/|$)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("tapping a section pushes /settings/[section] and shows a back button", async ({ page }) => {
|
test("tapping a section pushes /settings/[section] and shows a back button", async ({ page }) => {
|
||||||
@@ -144,9 +153,8 @@ test.describe("Settings — compact master-detail", () => {
|
|||||||
await expect(page).toHaveURL(/\/settings\/about$/);
|
await expect(page).toHaveURL(/\/settings\/about$/);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Back", exact: true }).click();
|
await page.getByRole("button", { name: "Back", exact: true }).click();
|
||||||
await expect(page).toHaveURL(/\/settings$/);
|
await expectCompactSettingsRootList(page);
|
||||||
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
await expect(page.getByRole("button", { name: "Back", exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Back", exact: true })).toHaveCount(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("tapping a host entry pushes /settings/hosts/[serverId]", async ({ page }) => {
|
test("tapping a host entry pushes /settings/hosts/[serverId]", async ({ page }) => {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdtempSync,
|
||||||
|
readdirSync,
|
||||||
|
readFileSync,
|
||||||
|
realpathSync,
|
||||||
|
rmSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -45,6 +53,42 @@ function createCoreDeps(options?: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findDirectCreateWorktreeCallSites(serverSrc: string): string[] {
|
||||||
|
const matches: string[] = [];
|
||||||
|
|
||||||
|
function walk(directory: string) {
|
||||||
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||||
|
const absolutePath = path.join(directory, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(absolutePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = path
|
||||||
|
.relative(serverSrc, absolutePath)
|
||||||
|
.split(path.sep)
|
||||||
|
.join(path.posix.sep);
|
||||||
|
|
||||||
|
if (relativePath === "utils/worktree.ts" || relativePath.endsWith(".test.ts")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep this literal in the test file so the invariant proves tests are allowed to inspect createWorktree(.
|
||||||
|
if (/createWorktree\(/.test(readFileSync(absolutePath, "utf8"))) {
|
||||||
|
matches.push(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(serverSrc);
|
||||||
|
return matches.sort();
|
||||||
|
}
|
||||||
|
|
||||||
function createGitRepo(): { tempDir: string; repoDir: string; paseoHome: string } {
|
function createGitRepo(): { tempDir: string; repoDir: string; paseoHome: string } {
|
||||||
const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "worktree-core-test-")));
|
const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "worktree-core-test-")));
|
||||||
const repoDir = path.join(tempDir, "repo");
|
const repoDir = path.join(tempDir, "repo");
|
||||||
@@ -435,21 +479,6 @@ describe.skipIf(process.platform === "win32")("createWorktreeCore", () => {
|
|||||||
|
|
||||||
test("keeps direct createWorktree calls isolated to the core layer", () => {
|
test("keeps direct createWorktree calls isolated to the core layer", () => {
|
||||||
const serverSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const serverSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
// Keep this literal in the test file so the invariant proves tests are allowed to inspect createWorktree(.
|
expect(findDirectCreateWorktreeCallSites(serverSrc)).toEqual(["server/worktree-core.ts"]);
|
||||||
const output = execSync(
|
|
||||||
"rg --files-with-matches 'createWorktree\\(' . --glob '!**/utils/worktree.ts' --glob '!**/*.test.ts'",
|
|
||||||
{
|
|
||||||
cwd: serverSrc,
|
|
||||||
stdio: "pipe",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => line.replace(/^\.\//, ""))
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
expect(output).toEqual(["server/worktree-core.ts"]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user