mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
feat(ui): offer page refresh on protocol-mismatch failure (#111772)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ConnectErrorDetailCodes } from "../../../packages/gateway-protocol/src/connect-error-details.js";
|
||||
import "./login-gate.ts";
|
||||
|
||||
type LoginGateElement = HTMLElement & {
|
||||
props: Record<string, unknown>;
|
||||
updateComplete: Promise<boolean>;
|
||||
};
|
||||
|
||||
async function mountFailure(lastError: string, lastErrorCode: string | null) {
|
||||
const element = document.createElement("openclaw-login-gate") as LoginGateElement;
|
||||
element.props = {
|
||||
basePath: "",
|
||||
connected: false,
|
||||
lastError,
|
||||
lastErrorCode,
|
||||
hasToken: false,
|
||||
hasPassword: false,
|
||||
gatewayUrl: "ws://127.0.0.1:18789",
|
||||
token: "",
|
||||
password: "",
|
||||
showGatewayToken: false,
|
||||
showGatewayPassword: false,
|
||||
onGatewayUrlChange: vi.fn(),
|
||||
onTokenChange: vi.fn(),
|
||||
onPasswordChange: vi.fn(),
|
||||
onToggleGatewayToken: vi.fn(),
|
||||
onToggleGatewayPassword: vi.fn(),
|
||||
onConnect: vi.fn(),
|
||||
};
|
||||
document.body.append(element);
|
||||
await element.updateComplete;
|
||||
return element;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("login gate failure recovery", () => {
|
||||
it("offers page refresh for a protocol mismatch and reloads when selected", async () => {
|
||||
const element = await mountFailure(
|
||||
"protocol mismatch",
|
||||
ConnectErrorDetailCodes.PROTOCOL_MISMATCH,
|
||||
);
|
||||
const reload = vi.fn();
|
||||
vi.stubGlobal("window", { location: { reload } });
|
||||
|
||||
const failure = element.querySelector<HTMLElement>(
|
||||
'.login-gate__failure[data-kind="protocol-mismatch"]',
|
||||
);
|
||||
const refresh = failure?.querySelector<HTMLButtonElement>(".login-gate__failure-refresh");
|
||||
|
||||
expect(refresh?.textContent?.trim()).toBe("Refresh page");
|
||||
expect(failure?.querySelector(".login-gate__failure-steps")).not.toBeNull();
|
||||
expect(failure?.querySelector(".login-gate__failure-docs")).not.toBeNull();
|
||||
|
||||
refresh?.click();
|
||||
expect(reload).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"auth-required",
|
||||
"unauthorized: gateway token required",
|
||||
ConnectErrorDetailCodes.AUTH_REQUIRED,
|
||||
],
|
||||
["network", "WebSocket connection failed", null],
|
||||
[
|
||||
"insecure-context",
|
||||
"device identity required",
|
||||
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
||||
],
|
||||
])("does not offer page refresh for %s failures", async (kind, error, code) => {
|
||||
const element = await mountFailure(error, code);
|
||||
|
||||
expect(element.querySelector(".login-gate__failure")?.getAttribute("data-kind")).toBe(kind);
|
||||
expect(element.querySelector(".login-gate__failure-refresh")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,7 @@ type LoginFailureFeedback = {
|
||||
kind: LoginFailureKind;
|
||||
title: string;
|
||||
summary: string;
|
||||
refreshAction?: { label: string };
|
||||
steps: string[];
|
||||
docsHref: string;
|
||||
docsLabel: string;
|
||||
@@ -96,12 +97,14 @@ function buildFeedback(params: {
|
||||
summaryKey: string;
|
||||
stepKeys: string[];
|
||||
stepParams?: Record<string, string>;
|
||||
refreshAction?: { label: string };
|
||||
}): LoginFailureFeedback {
|
||||
const docsHref = params.docsHref ?? "https://docs.openclaw.ai/web/dashboard";
|
||||
return {
|
||||
kind: params.kind,
|
||||
title: t(params.titleKey, params.stepParams),
|
||||
summary: t(params.summaryKey, params.stepParams),
|
||||
refreshAction: params.refreshAction,
|
||||
steps: params.stepKeys.map((key) => t(key, params.stepParams)),
|
||||
docsHref,
|
||||
docsLabel: resolveDocsLabel(docsHref),
|
||||
@@ -209,6 +212,7 @@ function resolveLoginFailureFeedback(
|
||||
"https://docs.openclaw.ai/web/control-ui#debuggingtesting-dev-server--remote-gateway",
|
||||
titleKey: "login.failure.protocol.title",
|
||||
summaryKey: "login.failure.protocol.summary",
|
||||
refreshAction: { label: t("login.failure.protocol.refresh") },
|
||||
stepKeys: [
|
||||
"login.failure.protocol.stepDashboard",
|
||||
"login.failure.protocol.stepDevUi",
|
||||
@@ -264,6 +268,11 @@ function resolveLoginFailureFeedback(
|
||||
});
|
||||
}
|
||||
|
||||
function refreshLoginGatePage() {
|
||||
// The login gate blocks before the composer mounts, so there is no draft to preserve.
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function renderLoginFailure(feedback: LoginFailureFeedback) {
|
||||
return html`
|
||||
<div
|
||||
@@ -274,6 +283,17 @@ function renderLoginFailure(feedback: LoginFailureFeedback) {
|
||||
>
|
||||
<div class="login-gate__failure-title">${feedback.title}</div>
|
||||
<div class="login-gate__failure-summary">${feedback.summary}</div>
|
||||
${feedback.refreshAction
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary login-gate__failure-refresh"
|
||||
@click=${refreshLoginGatePage}
|
||||
>
|
||||
${feedback.refreshAction.label}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
<ol class="login-gate__failure-steps">
|
||||
${feedback.steps.map((step) => html`<li>${step}</li>`)}
|
||||
</ol>
|
||||
|
||||
@@ -3592,6 +3592,7 @@ export const en: TranslationMap = {
|
||||
title: "Protocol mismatch",
|
||||
summary:
|
||||
"The served Control UI and the running Gateway do not agree on the supported connection protocol.",
|
||||
refresh: "Refresh page",
|
||||
stepDashboard:
|
||||
"Reopen the served dashboard with openclaw dashboard so the UI and Gateway come from the same install.",
|
||||
stepDevUi:
|
||||
|
||||
Reference in New Issue
Block a user