mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 21:23:04 +00:00
fix(test): align isolated test runtimes with Expo 57
Hoisted React consumers need the matching DOM renderer at the repository root. Automated Metro processes must not install native DevTools into disposable homes, and development toast overlays must not cover compact actions. Keep Explorer journeys on the shared host and synchronize setup fixtures on daemon output rather than a one-second file-marker window. A delayed real child start reproduces the readiness failure on every platform.
This commit is contained in:
+1
-1
@@ -193,7 +193,7 @@ Test suites in this repo are heavy. Running them in bulk freezes the machine, es
|
||||
- Playwright specs that exercise only the daemon import `daemonTest` from the shared fixtures so they do not create a browser context or page.
|
||||
- Helpers that create projects or workspaces own those records until cleanup. Their clients remove the daemon project on close, and an automatic fixture fails any test that still leaks a project record. Deleting only the temporary directory is not cleanup. Agent helpers pass the intended `workspaceId` through to agent creation; they never infer ownership from `cwd`.
|
||||
- Tests whose subject is daemon-global state, such as an empty history or daemon restart, start a dedicated host explicitly. Filenames and directories describe product behavior, never execution order or isolation mechanics.
|
||||
- Global setup accepts Metro as ready only when `/status` returns `packager-status:running`, then fetches the document's scripts so the cold bundle compilation finishes before Playwright's per-test timeout starts. A generic TCP listener is not sufficient readiness evidence. The browser suite uses direct local daemon connections and does not start a relay.
|
||||
- Global setup accepts Metro as ready only when `/status` returns `packager-status:running`, then fetches the document's scripts so the cold bundle compilation finishes before Playwright's per-test timeout starts. A generic TCP listener is not sufficient readiness evidence. The browser suite uses direct local daemon connections and does not start a relay. Automated Metro launches set `EXPO_UNSTABLE_HEADLESS=1` so Expo does not download the native DevTools shell into temporary test homes; its read-only cache breaks teardown. Browser fixtures dismiss Expo's developer error toast through its close button while retaining console logs, so it cannot cover compact bottom actions.
|
||||
- The app Playwright harness boots on Windows as well as POSIX. Spawn Node entrypoints through `process.execPath`, not `npx` or `node_modules/.bin` shims: Node refuses to spawn `.cmd` or `.bat` without `shell: true`, and shell mode concatenates argv without escaping and sends kill signals to `cmd.exe` instead of the real child.
|
||||
- Teardown kills the process tree, because a Windows signal reaches only the direct child and leaves forked workers holding the listening port.
|
||||
- The `asdf`-backed local Elixir relay stays POSIX-only, so the `relay-deployment` Playwright project is unavailable on Windows.
|
||||
|
||||
Generated
+19
-18
@@ -39,6 +39,7 @@
|
||||
"oxlint-tsgolint": "^0.22.1",
|
||||
"patch-package": "^8.0.1",
|
||||
"playwright": "^1.56.1",
|
||||
"react-dom": "19.2.3",
|
||||
"typescript": "^5.9.3",
|
||||
"ws": "^8.20.0"
|
||||
}
|
||||
@@ -29981,6 +29982,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom/node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
@@ -37730,18 +37749,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/app/node_modules/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"packages/app/node_modules/react-native-enriched-markdown": {
|
||||
"version": "1.2.0-nightly-20261014-766fc0e67",
|
||||
"resolved": "https://registry.npmjs.org/react-native-enriched-markdown/-/react-native-enriched-markdown-1.2.0-nightly-20261014-766fc0e67.tgz",
|
||||
@@ -37802,12 +37809,6 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"packages/app/node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/app/node_modules/turndown": {
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz",
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
"oxlint-tsgolint": "^0.22.1",
|
||||
"patch-package": "^8.0.1",
|
||||
"playwright": "^1.56.1",
|
||||
"react-dom": "19.2.3",
|
||||
"typescript": "^5.9.3",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
|
||||
@@ -45,10 +45,10 @@ async function openCompactActionsMenu(page: Page, workspaceId: string) {
|
||||
await page.goto(buildHostWorkspaceRoute(getServerId(), workspaceId));
|
||||
await page.reload();
|
||||
await page.getByTestId("workspace-explorer-toggle").first().click();
|
||||
const changesTab = page.getByTestId("explorer-tab-changes").filter({ visible: true });
|
||||
const changesTab = page.getByTestId("workspace-tab-changes_tree").filter({ visible: true });
|
||||
await expect(changesTab).toBeVisible({ timeout: 30_000 });
|
||||
await changesTab.click();
|
||||
const explorer = page.getByTestId("explorer-content-area").filter({ visible: true });
|
||||
const explorer = page.getByTestId("workspace-explorer-sidebar").filter({ visible: true });
|
||||
await expect(explorer.getByTestId("changes-header")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
const trigger = explorer.getByTestId("changes-actions-menu-trigger");
|
||||
|
||||
@@ -125,7 +125,7 @@ test("composer diff stat opens the compact explorer instead of a Changes tab", a
|
||||
});
|
||||
|
||||
const closeExplorer = page
|
||||
.getByTestId("explorer-header")
|
||||
.getByTestId("workspace-explorer-sidebar")
|
||||
.getByRole("button", { name: "Close Explorer sidebar" });
|
||||
await expect(closeExplorer).not.toBeInViewport();
|
||||
|
||||
@@ -809,7 +809,7 @@ async function expectUnmatchedFeedback(page: Page) {
|
||||
}
|
||||
|
||||
function compactReviewSurface(page: Page) {
|
||||
return page.getByTestId("explorer-content-area").filter({ visible: true });
|
||||
return page.getByTestId("workspace-explorer-sidebar").filter({ visible: true });
|
||||
}
|
||||
async function openCompactReviewExplorer(
|
||||
page: Page,
|
||||
@@ -847,7 +847,10 @@ async function scrollReviewToLastLine(page: Page) {
|
||||
|
||||
async function revealCompactChanges(page: Page) {
|
||||
await page.getByTestId("workspace-explorer-toggle").first().click();
|
||||
await page.getByTestId("explorer-header").getByText("Changes", { exact: true }).click();
|
||||
await page
|
||||
.getByTestId("workspace-explorer-sidebar")
|
||||
.getByTestId("workspace-tab-changes_tree")
|
||||
.click();
|
||||
await expect(compactReviewSurface(page).getByTestId("diff-file-0-body")).toBeVisible();
|
||||
}
|
||||
async function expectReviewEditorBody(page: Page, body: string) {
|
||||
|
||||
@@ -4,8 +4,8 @@ env APP_ID=sh.paseo.debug
|
||||
open "${APP_ID}"
|
||||
wait "id=\"workspace-header-menu-trigger\"" 20000
|
||||
press "id=\"workspace-explorer-toggle\""
|
||||
wait "id=\"explorer-tab-files\"" 10000
|
||||
press "id=\"explorer-tab-files\""
|
||||
wait "id=\"workspace-tab-files\"" 10000
|
||||
press "id=\"workspace-tab-files\""
|
||||
wait "id=\"file-explorer-row-0\"" 10000
|
||||
longpress "id=\"file-explorer-row-0\"" 600
|
||||
wait "id=\"file-explorer-row-0-context-menu-content\"" 10000
|
||||
|
||||
@@ -130,6 +130,13 @@ const test = daemonTest.extend<{
|
||||
await ws.close({ code: 1008, reason: "Blocked connection to localhost:6767 during e2e." });
|
||||
});
|
||||
|
||||
// Expo's development LogBox toast overlaps bottom actions on compact screens.
|
||||
// Dismiss its UI through the close button; console errors still go into the test log.
|
||||
const dismissDevToast = page
|
||||
.locator("#error-toast")
|
||||
.getByRole("button", { name: "", exact: true });
|
||||
await page.addLocatorHandler(dismissDevToast, () => dismissDevToast.click());
|
||||
|
||||
const entries: string[] = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
|
||||
@@ -134,6 +134,8 @@ function startMetro(port: number, buffer: ReturnType<typeof createLineBuffer>):
|
||||
env: {
|
||||
...process.env,
|
||||
BROWSER: "none",
|
||||
// Automation never opens native DevTools or installs its desktop shell.
|
||||
EXPO_UNSTABLE_HEADLESS: "1",
|
||||
...(process.env.E2E_DESKTOP_RUNTIME === "1" ? { PASEO_WEB_PLATFORM: "electron" } : {}),
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
|
||||
@@ -276,15 +276,22 @@ export async function swipeOpenMobileExplorer(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
export async function closeMobileExplorer(page: Page): Promise<void> {
|
||||
const closeButton = page.getByTestId("explorer-close");
|
||||
const closeButton = page
|
||||
.getByTestId("workspace-explorer-sidebar")
|
||||
.getByRole("button", { name: "Close Explorer sidebar", exact: true });
|
||||
await expect(closeButton).toBeInViewport({ ratio: 1, timeout: 5_000 });
|
||||
await closeButton.click();
|
||||
}
|
||||
|
||||
export async function expectMobileExplorerVisible(page: Page): Promise<void> {
|
||||
await expect(page.getByTestId("explorer-header")).toBeInViewport({ ratio: 1, timeout: 5_000 });
|
||||
await expect(page.getByTestId("workspace-explorer-sidebar")).toBeInViewport({
|
||||
ratio: 1,
|
||||
timeout: 5_000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function expectMobileExplorerHidden(page: Page): Promise<void> {
|
||||
await expect(page.getByTestId("explorer-header")).not.toBeInViewport({ timeout: 5_000 });
|
||||
await expect(page.getByTestId("workspace-explorer-sidebar")).not.toBeInViewport({
|
||||
timeout: 5_000,
|
||||
});
|
||||
}
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
/** @vitest-environment jsdom */
|
||||
import { act, useLayoutEffect, useState, useSyncExternalStore } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, expect, it } from "vitest";
|
||||
@@ -1173,6 +1173,7 @@ async function main() {
|
||||
cwd: rootDir,
|
||||
env: {
|
||||
...commonEnv,
|
||||
EXPO_UNSTABLE_HEADLESS: "1",
|
||||
EXPO_PORT: String(expoPort),
|
||||
EXPO_DEV_URL: `http://localhost:${expoPort}`,
|
||||
PASEO_ELECTRON_REMOTE_DEBUGGING_PORT: String(cdpPort),
|
||||
|
||||
@@ -35,6 +35,7 @@ export async function verifyAttachedDaemonControls({ repo, root, env, home, port
|
||||
env: {
|
||||
...env,
|
||||
EXPO_NO_DOTENV: "1",
|
||||
EXPO_UNSTABLE_HEADLESS: "1",
|
||||
CI: "1",
|
||||
PASEO_WEB_PLATFORM: "electron",
|
||||
EXPO_PUBLIC_LOCAL_DAEMON: `127.0.0.1:${port}`,
|
||||
|
||||
@@ -625,8 +625,9 @@ test("setup details follow one workspace/source and restore after reconnect with
|
||||
path.join(repoDir, "setup.cjs"),
|
||||
`
|
||||
const fs = require('node:fs');
|
||||
// Exercise a real child start beyond Vitest's default one-second polling window.
|
||||
setTimeout(() => {
|
||||
console.log('setup started');
|
||||
fs.writeFileSync('started.txt', 'ready');
|
||||
let wave = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (fs.existsSync('wave-' + wave)) {
|
||||
@@ -636,6 +637,7 @@ const timer = setInterval(() => {
|
||||
}
|
||||
if (fs.existsSync('finish.txt')) { clearInterval(timer); process.exit(1); }
|
||||
}, 20);
|
||||
}, 1500);
|
||||
`,
|
||||
);
|
||||
writeFileSync(path.join(repoDir, ".gitignore"), "*.txt\nwave-*\nemitted-*\n");
|
||||
@@ -672,15 +674,25 @@ const timer = setInterval(() => {
|
||||
const directory = quiet.observeWorkspaces();
|
||||
observations.push(directory);
|
||||
await directory.ready;
|
||||
const waitForSetupStarted = async (workspaceId: string) => {
|
||||
// Process startup is asynchronous, especially under Windows PowerShell. The daemon's
|
||||
// output snapshot is the readiness barrier; a marker file can precede PTY delivery.
|
||||
await expect
|
||||
.poll(async () => (await viewer.fetchWorkspaceSetupStatus(workspaceId)).snapshot, {
|
||||
timeout: 30000,
|
||||
})
|
||||
.toMatchObject({
|
||||
status: "running",
|
||||
detail: { log: expect.stringContaining("setup started") },
|
||||
});
|
||||
};
|
||||
for (const worktreeSlug of ["noisy-a", "noisy-b", "noisy-c"]) {
|
||||
const result = await creator.createWorkspace({
|
||||
source: { kind: "worktree", cwd: repoDir, worktreeSlug },
|
||||
});
|
||||
if (!result.workspace || result.error) throw new Error(result.error ?? "No workspace");
|
||||
workspaces.push(result.workspace);
|
||||
await expect
|
||||
.poll(() => existsSync(path.join(result.workspace!.workspaceDirectory!, "started.txt")))
|
||||
.toBe(true);
|
||||
await waitForSetupStarted(result.workspace.id);
|
||||
}
|
||||
const external = await mcp.callTool({
|
||||
name: "create_workspace",
|
||||
@@ -693,17 +705,8 @@ const timer = setInterval(() => {
|
||||
);
|
||||
if (!externalWorkspace) throw new Error("Missing MCP directory entry");
|
||||
workspaces.unshift(externalWorkspace);
|
||||
await expect
|
||||
.poll(() => existsSync(path.join(externalWorkspace.workspaceDirectory!, "started.txt")))
|
||||
.toBe(true);
|
||||
await waitForSetupStarted(externalWorkspace.id);
|
||||
const workspace = workspaces[0]!;
|
||||
// The child writes its marker before the terminal's output is delivered.
|
||||
// Wait for the daemon snapshot, rather than treating the file as an output barrier.
|
||||
await expect
|
||||
.poll(async () => (await viewer.fetchWorkspaceSetupStatus(workspace.id)).snapshot, {
|
||||
timeout: 30000,
|
||||
})
|
||||
.toMatchObject({ detail: { log: expect.stringContaining("setup started") } });
|
||||
const details = viewer.observeWorkspaceSetup(workspace.id);
|
||||
observations.push(details);
|
||||
const initial = await details.ready;
|
||||
|
||||
Reference in New Issue
Block a user