fix(app): preserve workspace route during desktop refresh

This commit is contained in:
Mohamed Boudra
2026-06-11 16:08:50 +07:00
parent f5a7055e7e
commit d802534e7b
12 changed files with 395 additions and 235 deletions
+19 -5
View File
@@ -533,13 +533,23 @@ async function startRelay(excludedPorts: Set<number>): Promise<number> {
);
}
function startMetro(metroPort: number, buffer: ReturnType<typeof createLineBuffer>): ChildProcess {
function startMetro(input: {
metroPort: number;
daemonPort: number;
buffer: ReturnType<typeof createLineBuffer>;
}): ChildProcess {
const appDir = path.resolve(__dirname, "..");
const child = spawn("npx", ["expo", "start", "--web", "--port", String(metroPort)], {
const child = spawn("npx", ["expo", "start", "--web", "--port", String(input.metroPort)], {
cwd: appDir,
env: {
...process.env,
BROWSER: "none",
...(process.env.E2E_DESKTOP_RUNTIME === "1"
? {
PASEO_WEB_PLATFORM: "electron",
EXPO_PUBLIC_LOCAL_DAEMON: `127.0.0.1:${input.daemonPort}`,
}
: {}),
},
stdio: ["ignore", "pipe", "pipe"],
detached: false,
@@ -551,7 +561,7 @@ function startMetro(metroPort: number, buffer: ReturnType<typeof createLineBuffe
.split("\n")
.filter((line) => line.trim());
for (const line of lines) {
buffer.add(`[stdout] ${line}`);
input.buffer.add(`[stdout] ${line}`);
console.log(`[metro] ${line}`);
}
});
@@ -562,7 +572,7 @@ function startMetro(metroPort: number, buffer: ReturnType<typeof createLineBuffe
.split("\n")
.filter((line) => line.trim());
for (const line of lines) {
buffer.add(`[stderr] ${line}`);
input.buffer.add(`[stderr] ${line}`);
console.error(`[metro] ${line}`);
}
});
@@ -685,7 +695,11 @@ export default async function globalSetup() {
try {
const relayPort = await startRelay(new Set([port, metroPort]));
metroProcess = startMetro(metroPort, metroLineBuffer);
metroProcess = startMetro({
metroPort,
daemonPort: port,
buffer: metroLineBuffer,
});
daemonProcess = startDaemon({
port,
relayPort,
+18 -7
View File
@@ -59,6 +59,10 @@ export interface DesktopBridgeConfig {
daemonLogPath?: string;
/** Initial manageBuiltInDaemon setting. Defaults to false. */
manageBuiltInDaemon?: boolean;
/** Daemon listen address reported by desktop_daemon_status. Defaults to 127.0.0.1:6767. */
daemonListen?: string;
/** Keep start_desktop_daemon pending to hold the desktop startup blocker open. */
hangDaemonStart?: boolean;
/**
* Controls what dialog.ask returns when the daemon management confirm dialog
* fires. True = confirm (proceed with the action), false = cancel. Defaults to
@@ -124,7 +128,7 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
return {
serverId: cfg.serverId,
status: daemonRunning ? "running" : "stopped",
listen: "127.0.0.1:6767",
listen: cfg.daemonListen ?? "127.0.0.1:6767",
hostname: null,
pid: currentPid,
home: "",
@@ -134,6 +138,18 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
};
}
function startDesktopDaemon() {
if (cfg.hangDaemonStart) {
return new Promise(() => undefined);
}
startCount += 1;
daemonRunning = true;
// First start (bootstrap) returns the configured PID; subsequent starts
// (after a stop) get a fresh PID so tests can observe the change.
currentPid = (cfg.daemonPid ?? 10000) + (startCount - 1) * 1000;
return buildDaemonStatus();
}
const desktopBridge: {
platform: string;
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
@@ -218,12 +234,7 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
}
if (command === "start_desktop_daemon") {
startCount += 1;
daemonRunning = true;
// First start (bootstrap) returns the configured PID; subsequent starts
// (after a stop) get a fresh PID so tests can observe the change.
currentPid = (cfg.daemonPid ?? 10000) + (startCount - 1) * 1000;
return buildDaemonStatus();
return startDesktopDaemon();
}
return null;
@@ -9,7 +9,7 @@ import {
openWorkspaceWithAgents,
} from "./helpers/archive-tab";
import { expectComposerVisible } from "./helpers/composer";
import { daemonWsRoutePattern } from "./helpers/daemon-port";
import { daemonWsRoutePattern, getE2EDaemonPort } from "./helpers/daemon-port";
import { seedWorkspace } from "./helpers/seed-client";
import {
getVisibleWorkspaceAgentTabIds,
@@ -32,6 +32,7 @@ import {
} from "./helpers/workspace-ui";
import { clickSettingsBackToWorkspace } from "./helpers/settings";
import { getServerId } from "./helpers/server-id";
import { injectDesktopBridge } from "./helpers/desktop-updates";
const LOADING_WORKSPACE_TEXT_PATTERN = /Loading workspace/i;
@@ -79,6 +80,43 @@ async function closeFirstVisibleDraftTab(page: Page): Promise<void> {
await closeButton.first().click();
}
async function openWorkspaceThroughApp(
page: Page,
input: {
serverId: string;
workspace: Awaited<ReturnType<typeof seedWorkspace>>;
},
): Promise<void> {
await gotoAppShell(page);
await waitForSidebarHydration(page);
await switchWorkspaceViaSidebar({
page,
serverId: input.serverId,
targetWorkspacePath: input.workspace.workspaceId,
});
await waitForWorkspaceTabsVisible(page);
await expectWorkspaceLocation(page, input);
}
async function expectWorkspaceLocation(
page: Page,
input: {
serverId: string;
workspace: Awaited<ReturnType<typeof seedWorkspace>>;
},
): Promise<void> {
await expect(page).toHaveURL(
buildHostWorkspaceRoute(input.serverId, input.workspace.workspaceId),
{
timeout: 30_000,
},
);
await expectWorkspaceHeader(page, {
title: input.workspace.workspaceName,
subtitle: input.workspace.projectDisplayName,
});
}
async function installDaemonWebSocketGate(page: Page) {
let acceptingConnections = true;
const activeSockets = new Set<WebSocketRoute>();
@@ -260,6 +298,41 @@ test.describe("Workspace navigation regression", () => {
}
});
test("refresh keeps the user on the same workspace route", async ({ page }) => {
const serverId = getServerId();
const daemonGate = await installDaemonWebSocketGate(page);
const workspace = await seedWorkspace({ repoPrefix: "workspace-refresh-route-" });
try {
const agent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath,
title: `workspace-refresh-route-${Date.now()}`,
});
await injectDesktopBridge(page, {
serverId,
manageBuiltInDaemon: true,
hangDaemonStart: true,
daemonListen: `127.0.0.1:${getE2EDaemonPort()}`,
});
await openWorkspaceThroughApp(page, { serverId, workspace });
await waitForWorkspaceTabsVisible(page);
await expectWorkspaceTabVisible(page, agent.id);
await expectWorkspaceLocation(page, { serverId, workspace });
await daemonGate.drop();
await page.reload();
await expect(page.getByTestId("startup-splash")).toBeVisible({ timeout: 30_000 });
daemonGate.restore();
await waitForSidebarHydration(page);
await expectWorkspaceLocation(page, { serverId, workspace });
await waitForWorkspaceTabsVisible(page);
} finally {
daemonGate.restore();
await workspace.cleanup();
}
});
test("sidebar navigation and reload keep workspace selection and tabs aligned", async ({
page,
}) => {