refactor(ui): trim pairing uncertainty state (#123856)

* refactor(ui): trim pairing uncertainty state

* test(ui): stabilize navigation and worker readiness

* test(ui): drop popover flip geometry replay
This commit is contained in:
Peter Steinberger
2026-08-14 16:16:57 -07:00
committed by GitHub
parent bd1814bede
commit 1cf932295c
8 changed files with 74 additions and 74 deletions
@@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
import {
DevicePairSetupCodeResultSchema,
DevicePairSetupCompletedEventSchema,
DevicePairSetupDeliveryUncertainEventSchema,
DevicePairSetupStatusParamsSchema,
DevicePairSetupStatusResultSchema,
} from "./devices.js";
@@ -43,7 +42,6 @@ describe("device pairing setup schemas", () => {
ts: 1_800_000_000_001,
};
expect(Value.Check(DevicePairSetupCompletedEventSchema, event)).toBe(true);
expect(Value.Check(DevicePairSetupDeliveryUncertainEventSchema, event)).toBe(true);
expect(
Value.Check(DevicePairSetupCompletedEventSchema, { ...event, bootstrapToken: "secret" }),
).toBe(false);
@@ -473,7 +473,6 @@ export function registerOverlayPairingAccessTests() {
});
expect(overlays.snapshot.devicePairSetupLifecycle).toEqual({
phase: "delivery-uncertain",
deviceName: "Phone",
access: "full",
});
overlays.dispose();
+1 -3
View File
@@ -198,9 +198,7 @@ suite.define(() => {
const dialog = page.getByRole("dialog", { name: "Pair a device" });
const qr = page.getByAltText("OpenClaw mobile pairing QR code");
await dialog.waitFor();
expect(await page.getByRole("button", { name: "Create setup code" }).isVisible()).toBe(
true,
);
await page.getByRole("button", { name: "Create setup code" }).waitFor();
const dialogBox = await page.locator(".device-pair-setup").boundingBox();
expect(dialogBox?.width).toBeLessThanOrEqual(390);
await captureUiProof(page, "01-mobile-access-selection.png");
+5 -11
View File
@@ -412,18 +412,12 @@ describeControlUiE2e("Control UI Models mocked Gateway E2E", () => {
skidding: Reflect.get(node, "skidding"),
})),
).toEqual({ distance: 8, placement: "top", skidding: 0 });
const helpButtonBox = await helpButton.boundingBox();
const labelBox = await utilityLabel.boundingBox();
const popoverBox = await popoverBody.boundingBox();
if (!helpButtonBox || !labelBox || !popoverBox) {
throw new Error("expected utility label, help trigger, and popover bounds");
}
expect(popoverBox.y + popoverBox.height).toBeLessThan(helpButtonBox.y);
const helpButtonCenter = helpButtonBox.x + helpButtonBox.width / 2;
const popoverCenter = popoverBox.x + popoverBox.width / 2;
expect(Math.abs(helpButtonCenter - popoverCenter)).toBeLessThanOrEqual(1);
if (recordVisuals) {
const labelBox = await utilityLabel.boundingBox();
const popoverBox = await popoverBody.boundingBox();
if (!labelBox || !popoverBox) {
throw new Error("expected utility label and popover bounds");
}
await page.screenshot({
animations: "disabled",
fullPage: true,
+21 -18
View File
@@ -8,6 +8,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
buildProductionControlUiE2e,
canRunPlaywrightChromium,
controlUiE2eWaitTimeoutMs,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startProductionControlUiE2eServer,
@@ -300,26 +301,28 @@ describe("Control UI service-worker production update E2E", () => {
await ensureControlledPage(page, pageErrors, buildA);
await expect.poll(() => readWorkerUpdateVersions(page)).toContain(buildA);
await expect
.poll(async () =>
page.evaluate(() => {
const panel = document.querySelector("openclaw-terminal-panel") as
| (HTMLElement & { available: boolean })
| null;
const shell = document.querySelector("openclaw-app-shell") as HTMLElement & {
runtime?: {
context?: {
config: { current: { terminalEnabled: boolean } };
gateway: { snapshot: { phase: string; hello: unknown } };
.poll(
async () =>
page.evaluate(() => {
const panel = document.querySelector("openclaw-terminal-panel") as
| (HTMLElement & { available: boolean })
| null;
const shell = document.querySelector("openclaw-app-shell") as HTMLElement & {
runtime?: {
context?: {
config: { current: { terminalEnabled: boolean } };
gateway: { snapshot: { phase: string; hello: unknown } };
};
};
};
};
return {
available: panel?.available ?? null,
phase: shell?.runtime?.context?.gateway.snapshot.phase ?? null,
terminalEnabled: shell?.runtime?.context?.config.current.terminalEnabled ?? null,
hasHello: shell?.runtime?.context?.gateway.snapshot.hello != null,
};
}),
return {
available: panel?.available ?? null,
phase: shell?.runtime?.context?.gateway.snapshot.phase ?? null,
terminalEnabled: shell?.runtime?.context?.config.current.terminalEnabled ?? null,
hasHello: shell?.runtime?.context?.gateway.snapshot.hello != null,
};
}),
{ timeout: controlUiE2eWaitTimeoutMs },
)
.toMatchObject({
available: true,
+1 -2
View File
@@ -361,12 +361,11 @@ describe("device pairing setup state", () => {
ts: 1,
});
expect(parsed).not.toBeNull();
expect(parsed).toEqual({ setupId: "setup-uncertain", access: "limited" });
expect(markDevicePairSetupDeliveryUncertain(state, parsed!)).toBe(true);
expect(state.devicePairSetupLifecycle).toEqual({
phase: "delivery-uncertain",
access: "limited",
deviceName: "Phone",
});
});
+3 -4
View File
@@ -24,7 +24,7 @@ type DevicePairSetupCompletion = Pick<DevicePairSetupCompletedEvent, "setupId" |
type DevicePairSetupDeliveryUncertain = Pick<
DevicePairSetupDeliveryUncertainEvent,
"setupId" | "access"
> & { deviceName?: string };
>;
export type DevicePairSetupLifecycle =
| { phase: "selection"; access: DevicePairSetupAccess }
@@ -51,7 +51,6 @@ export type DevicePairSetupLifecycle =
| {
phase: "delivery-uncertain";
access: DevicePairSetupDeliveryUncertain["access"];
deviceName?: string;
}
| { phase: "expired"; access: DevicePairSetupAccess };
export function requestDevicePairJoinSetup(client: GatewayRequestClient) {
@@ -263,7 +262,8 @@ export function parseDevicePairSetupCompletion(payload: unknown): DevicePairSetu
export function parseDevicePairSetupDeliveryUncertain(
payload: unknown,
): DevicePairSetupDeliveryUncertain | null {
return parseDevicePairSetupCompletion(payload);
const completion = parseDevicePairSetupCompletion(payload);
return completion ? { setupId: completion.setupId, access: completion.access } : null;
}
export function completeDevicePairSetup(
@@ -310,7 +310,6 @@ export function markDevicePairSetupDeliveryUncertain(
state.devicePairSetupLifecycle = {
phase: "delivery-uncertain",
access: outcome.access,
...(outcome.deviceName ? { deviceName: outcome.deviceName } : {}),
};
state.onDevicePairSetupChange();
return true;
+43 -33
View File
@@ -2444,42 +2444,52 @@ function createMockGatewayControls(
}, policy);
},
async waitForRequest(method) {
try {
await page.waitForFunction(
(targetMethod) => {
const gateway = (
window as Window & {
openclawControlUiE2eGateway?: {
requests: MockGatewayRequest[];
};
}
).openclawControlUiE2eGateway;
return Boolean(gateway?.requests.some((request) => request.method === targetMethod));
},
method,
// Request capture is non-rendering state. Interval polling avoids background-page
// requestAnimationFrame throttling when CI runs several headless pages concurrently.
{ polling: 25, timeout: controlUiE2eWaitTimeoutMs },
);
} catch (error) {
if (error instanceof Error && error.name === "TimeoutError") {
try {
await captureControlUiE2eRequestTimeout(page, method, error, diagnosticEvents);
} catch (captureError) {
console.error("[control-ui-e2e] failed to capture request-timeout diagnostics", {
captureError,
method,
});
const deadline = Date.now() + controlUiE2eWaitTimeoutMs;
for (let attempt = 0; attempt < 2; attempt += 1) {
try {
await page.waitForFunction(
(targetMethod) => {
const gateway = (
window as Window & {
openclawControlUiE2eGateway?: {
requests: MockGatewayRequest[];
};
}
).openclawControlUiE2eGateway;
return Boolean(gateway?.requests.some((request) => request.method === targetMethod));
},
method,
// Request capture is non-rendering state. Interval polling avoids background-page
// requestAnimationFrame throttling when CI runs several headless pages concurrently.
{ polling: 25, timeout: Math.max(1, deadline - Date.now()) },
);
const request = (await getRequests(method)).at(-1);
if (request) {
return request;
}
} catch (error) {
const contextReset =
error instanceof Error &&
(error.message.includes("Execution context was destroyed") ||
error.message.includes("Cannot find context with specified id"));
// Intentional stale-build reloads replace the page context once while connecting.
if (contextReset && attempt === 0 && !page.isClosed()) {
continue;
}
if (error instanceof Error && error.name === "TimeoutError") {
try {
await captureControlUiE2eRequestTimeout(page, method, error, diagnosticEvents);
} catch (captureError) {
console.error("[control-ui-e2e] failed to capture request-timeout diagnostics", {
captureError,
method,
});
}
}
throw error;
}
throw error;
}
const requests = await getRequests(method);
const request = requests.at(-1);
if (!request) {
throw new Error(`No mock Gateway request found for ${method}`);
}
return request;
throw new Error(`No mock Gateway request found for ${method}`);
},
};
}