mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(ui): prevent encoded-path readiness timeouts (#130666)
This commit is contained in:
committed by
GitHub
parent
cd321938f6
commit
6b842df52e
@@ -3,7 +3,12 @@ import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { decodeResumeHandoff } from "../../../src/shared/resume-handoff.js";
|
||||
import type { ChatPaneElement } from "../pages/chat/route-draft-focus-handoff.ts";
|
||||
import { controlUiSessionUrl, installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import {
|
||||
controlUiSessionPath,
|
||||
controlUiSessionUrl,
|
||||
installMockGateway,
|
||||
waitForControlUiRoute,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({
|
||||
@@ -100,6 +105,13 @@ suite.define(() => {
|
||||
await expect
|
||||
.poll(() => activePane.evaluate((pane) => (pane as ChatPaneElement).sessionKey))
|
||||
.toBe(sessionKey);
|
||||
await waitForControlUiRoute(page, {
|
||||
routeId: "chat",
|
||||
pathname: controlUiSessionPath(sessionKey, basePath),
|
||||
pathnamePrefix: `${basePath}/chat/`,
|
||||
search: "",
|
||||
hash: "",
|
||||
});
|
||||
await activePane.getByText("Ready for terminal continuation.").waitFor({ timeout: 10_000 });
|
||||
|
||||
const menuTrigger = activePane.getByRole("button", {
|
||||
@@ -182,6 +194,7 @@ suite.define(() => {
|
||||
await expect
|
||||
.poll(() => activePane.evaluate((pane) => (pane as ChatPaneElement).sessionKey))
|
||||
.toBe(sessionKey);
|
||||
await waitForControlUiRoute(page, { routeId: "chat" });
|
||||
await activePane
|
||||
.getByRole("paragraph")
|
||||
.filter({ hasText: /^Mobile session menu proof\.$/ })
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect, it } from "vitest";
|
||||
import type { ChatPaneElement } from "../pages/chat/route-draft-focus-handoff.ts";
|
||||
import {
|
||||
controlUiSessionUrl,
|
||||
installMockGateway,
|
||||
navigateToControlUiSession,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({ name: "Control UI route readiness" });
|
||||
|
||||
suite.define(() => {
|
||||
it.each([
|
||||
{ name: "root", basePath: "" },
|
||||
{ name: "encoded mount", basePath: "/nested/$&;=()+,![]{}'`/%25PATH%25" },
|
||||
])("navigates exact session keys at the $name", async ({ basePath }) => {
|
||||
await suite.withPage({ viewport: { width: 1200, height: 800 } }, async ({ page }) => {
|
||||
const initialSessionKey = "agent:runner:route:initial";
|
||||
await installMockGateway(page, { basePath, sessionKey: initialSessionKey });
|
||||
await page.goto(controlUiSessionUrl(suite.server.baseUrl, initialSessionKey));
|
||||
const visiblePane = page.locator("openclaw-chat-pane.chat-pane-cache__pane--visible");
|
||||
await expect
|
||||
.poll(() => visiblePane.evaluate((pane) => (pane as ChatPaneElement).sessionKey))
|
||||
.toBe(initialSessionKey);
|
||||
|
||||
const mountUrl = new URL(suite.server.baseUrl);
|
||||
mountUrl.pathname = basePath || "/";
|
||||
const encodedBase = basePath ? mountUrl.pathname : "";
|
||||
for (const [rest, suffix] of [
|
||||
["a/b", "a%2Fb"],
|
||||
["a:b", "a/b"],
|
||||
["%2F%25%3F%23", "%252F%2525%253F%2523"],
|
||||
["a?b#c", "a%3Fb%23c"],
|
||||
]) {
|
||||
const sessionKey = `agent:runner:route:${rest}`;
|
||||
await navigateToControlUiSession(page, sessionKey);
|
||||
expect(new URL(page.url()).pathname).toBe(`${encodedBase}/chat/runner/route/${suffix}`);
|
||||
expect(await visiblePane.evaluate((pane) => (pane as ChatPaneElement).sessionKey)).toBe(
|
||||
sessionKey,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,10 +34,11 @@ export function controlUiSessionUrl(baseUrl: string, sessionKey: string): string
|
||||
}
|
||||
|
||||
export async function navigateToControlUiSession(page: Page, sessionKey: string): Promise<void> {
|
||||
await page.evaluate((pathname) => {
|
||||
const expectedPathname = await page.evaluate((sessionPath) => {
|
||||
const app = document.querySelector("openclaw-app") as HTMLElement & {
|
||||
runtime?: {
|
||||
context: {
|
||||
basePath: string;
|
||||
navigate: (routeId: string, options: { pathname: string }) => void;
|
||||
};
|
||||
};
|
||||
@@ -45,9 +46,13 @@ export async function navigateToControlUiSession(page: Page, sessionKey: string)
|
||||
if (!app.runtime) {
|
||||
throw new Error("OpenClaw application runtime is unavailable");
|
||||
}
|
||||
const pathname = `${app.runtime.context.basePath}${sessionPath}`;
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = pathname;
|
||||
app.runtime.context.navigate("chat", { pathname });
|
||||
return url.pathname;
|
||||
}, controlUiSessionPath(sessionKey));
|
||||
await page.waitForURL((url) => url.pathname === controlUiSessionPath(sessionKey));
|
||||
await page.waitForURL((url) => url.pathname === expectedPathname);
|
||||
await page.waitForFunction(
|
||||
(targetSessionKey) =>
|
||||
[...document.querySelectorAll<HTMLElement>("openclaw-chat-pane")].some(
|
||||
@@ -109,13 +114,22 @@ export async function waitForControlUiRoute(page: Page, target: ControlUiRouteTa
|
||||
};
|
||||
const state = app.runtime?.router.getState();
|
||||
const pathname = window.location.pathname;
|
||||
// Router paths retain literal characters that browser history percent-encodes.
|
||||
// Serialize as a pathname; decoding would alias encoded delimiters and percent data.
|
||||
const browserPathname = (value: string) => {
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = value;
|
||||
return url.pathname;
|
||||
};
|
||||
return (
|
||||
state?.status === "success" &&
|
||||
state.matches[0]?.routeId === expected.routeId &&
|
||||
state.resolvedLocation?.pathname === pathname &&
|
||||
state.resolvedLocation !== null &&
|
||||
browserPathname(state.resolvedLocation.pathname) === pathname &&
|
||||
state.pendingMatches.length === 0 &&
|
||||
(expected.pathname === undefined || pathname === expected.pathname) &&
|
||||
(expected.pathnamePrefix === undefined || pathname.startsWith(expected.pathnamePrefix)) &&
|
||||
(expected.pathname === undefined || pathname === browserPathname(expected.pathname)) &&
|
||||
(expected.pathnamePrefix === undefined ||
|
||||
pathname.startsWith(browserPathname(expected.pathnamePrefix))) &&
|
||||
(expected.search === undefined || window.location.search === expected.search) &&
|
||||
(expected.hash === undefined || window.location.hash === expected.hash)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user