mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
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:
committed by
GitHub
parent
bd1814bede
commit
1cf932295c
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user