mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(ui): repair shared vitest lane cross-file leaks (#123453)
The non-isolated |ui| lane resets the module graph per test file while the jsdom document and customElements registry persist for the whole worker. Three producers leaked across that lifetime mismatch: - github-link-hovercard-registration.ts: each file's module instance adds document-level bootstrap listeners; on the first real focusin every stale instance raced customElements.define -> unhandled NotSupportedError. Guard the define inside the single-flight loader. - app-host.test.ts locale retry asserted through createElement, which serves a sibling graph's shell class bound to a different i18n singleton; construct ShellGatewayOwner directly so spy and callee share one graph. - route-transition.test.ts assigned updateComplete over the real Lit outlet's getter-only accessor when a sibling had registered it; define an own data property instead (byte-identical to the hunk in PR #123347). Adds a boundary regression test reproducing the duplicate-module-instance order; it failed pre-fix with 2 registry defines and the NotSupportedError.
This commit is contained in:
committed by
GitHub
parent
63bc139872
commit
92eed63f58
+26
-13
@@ -19,6 +19,7 @@ import { SESSION_FACE_PREFERENCE_PARAM } from "../lib/sessions/route-navigation.
|
||||
import { createStorageMock } from "../test-helpers/storage.ts";
|
||||
import { selectShellRouteState } from "./app-host-route-state.ts";
|
||||
import { resetAppHostTestGlobals, type ShellKeyboardState } from "./app-host.test-support.ts";
|
||||
import { ShellGatewayOwner, type ShellGatewayHost } from "./app-shell-gateway.ts";
|
||||
import "./app-host.ts";
|
||||
import type {
|
||||
ApplicationContext,
|
||||
@@ -60,11 +61,6 @@ type ShellInitializationState = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ShellGatewaySynchronizationState = {
|
||||
outboxStoreImport: { load: () => Promise<unknown> };
|
||||
synchronizeGateway: (snapshot: ApplicationGatewaySnapshot) => void;
|
||||
};
|
||||
|
||||
type I18nRecoveryWiring = {
|
||||
localeLoadRecovery?: {
|
||||
isUnrecoverableError: (error: unknown) => boolean;
|
||||
@@ -319,10 +315,27 @@ describe("OpenClaw shell source initialization", () => {
|
||||
|
||||
it("retries a pending locale once when the Gateway becomes connected", () => {
|
||||
const retryPendingLocale = vi.spyOn(i18n, "retryPendingLocale").mockImplementation(() => {});
|
||||
const shell = document.createElement(
|
||||
"openclaw-app-shell",
|
||||
) as unknown as ShellGatewaySynchronizationState;
|
||||
shell.outboxStoreImport = { load: vi.fn(async () => undefined) };
|
||||
// Owner-direct: the shared jsdom lane can retain a sibling graph's
|
||||
// openclaw-app-shell class bound to a different i18n instance; constructing
|
||||
// the owner keeps the spy and the callee in the current module graph.
|
||||
const host = {
|
||||
activeSessionKey: "",
|
||||
agentRosterRefreshTimer: null,
|
||||
agentsListClient: null,
|
||||
agentsListSource: null,
|
||||
context: undefined,
|
||||
criticalNoticeRuntime: null,
|
||||
lastLocalePrefSignature: null,
|
||||
outboxStoreImport: { load: vi.fn(async () => undefined) },
|
||||
previousGatewayPhase: null,
|
||||
routeState: {},
|
||||
runtimeConfigClient: null,
|
||||
runtimeConfigSource: null,
|
||||
sessionKeyClient: null,
|
||||
sidebarWorkboardRuntime: null,
|
||||
syncSidebarWorkboard: vi.fn(),
|
||||
} as unknown as ShellGatewayHost;
|
||||
const owner = new ShellGatewayOwner(host);
|
||||
const reconnecting = {
|
||||
client: null,
|
||||
phase: "reconnecting",
|
||||
@@ -334,10 +347,10 @@ describe("OpenClaw shell source initialization", () => {
|
||||
sessionKey: "",
|
||||
} as ApplicationGatewaySnapshot;
|
||||
|
||||
shell.synchronizeGateway(reconnecting);
|
||||
shell.synchronizeGateway(connected);
|
||||
shell.synchronizeGateway({ ...connected });
|
||||
shell.synchronizeGateway({ ...connected });
|
||||
owner.synchronizeGateway(reconnecting);
|
||||
owner.synchronizeGateway(connected);
|
||||
owner.synchronizeGateway({ ...connected });
|
||||
owner.synchronizeGateway({ ...connected });
|
||||
|
||||
expect(retryPendingLocale).toHaveBeenCalledOnce();
|
||||
retryPendingLocale.mockRestore();
|
||||
|
||||
@@ -100,6 +100,45 @@ describe("native link routing", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("defines the hovercard once across duplicate bootstrap module instances", async () => {
|
||||
// Regression: the non-isolated jsdom lane evaluates the registration
|
||||
// module once per sibling file against one persistent document, so stale
|
||||
// bootstrap listeners fire alongside this file's own. Reproduce that order
|
||||
// and require a single registry definition.
|
||||
vi.resetModules();
|
||||
await import("../components/github-link-hovercard-registration.ts");
|
||||
const define = vi.spyOn(customElements, "define");
|
||||
const provider = document.createElement(
|
||||
"openclaw-github-link-hovercard-provider",
|
||||
) as GitHubLinkHovercardProvider;
|
||||
provider.client = {
|
||||
request: vi.fn().mockResolvedValue({
|
||||
comments: 1,
|
||||
createdAt: "2026-07-09T10:00:00Z",
|
||||
kind: "issue",
|
||||
login: "octocat",
|
||||
number: 102691,
|
||||
owner: "openclaw",
|
||||
repo: "openclaw",
|
||||
state: "open",
|
||||
title: "Open links in a sidebar browser",
|
||||
updatedAt: "2026-07-09T10:00:00Z",
|
||||
}),
|
||||
} as unknown as GatewayBrowserClient;
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = "https://github.com/openclaw/openclaw/issues/102691";
|
||||
anchor.textContent = "#102691";
|
||||
provider.append(anchor);
|
||||
document.body.append(provider);
|
||||
anchor.focus();
|
||||
await vi.waitFor(() => expect(document.querySelector(".github-link-hovercard")).not.toBeNull());
|
||||
const hovercardDefines = define.mock.calls.filter(
|
||||
([tag]) => tag === "openclaw-github-link-hovercard-provider",
|
||||
);
|
||||
expect(hovercardDefines).toHaveLength(1);
|
||||
define.mockRestore();
|
||||
});
|
||||
|
||||
it("closes an active GitHub hovercard after routing its link", async () => {
|
||||
const bridge = installBridge();
|
||||
routing = startNativeLinkRouting();
|
||||
|
||||
@@ -5,9 +5,11 @@ function testDocumentWithOutlet(animate = vi.fn()) {
|
||||
const outlet = document.createElement("openclaw-router-outlet") as HTMLElement & {
|
||||
updateComplete: Promise<void>;
|
||||
};
|
||||
// Own data property: the real OpenClawRouterOutlet may already be registered by a
|
||||
// sibling test in this worker, and Lit's updateComplete is a getter-only accessor.
|
||||
Object.defineProperty(outlet, "updateComplete", {
|
||||
configurable: true,
|
||||
value: Promise.resolve(),
|
||||
configurable: true,
|
||||
});
|
||||
outlet.animate = animate;
|
||||
document.body.append(outlet);
|
||||
|
||||
@@ -40,7 +40,12 @@ async function activateHovercard(event: Event, trigger: "focus" | "pointer"): Pr
|
||||
);
|
||||
await ensureCustomElementDefined(HOVERCARD_TAG, async () => {
|
||||
const runtime = await import("./github-link-hovercard.runtime.ts");
|
||||
customElements.define(HOVERCARD_TAG, runtime.GitHubLinkHovercardProvider);
|
||||
// Sibling module instances can share one document + registry (the
|
||||
// non-isolated jsdom lane evaluates this module once per test file), so a
|
||||
// concurrent instance may have defined the tag while our load resolved.
|
||||
if (!customElements.get(HOVERCARD_TAG)) {
|
||||
customElements.define(HOVERCARD_TAG, runtime.GitHubLinkHovercardProvider);
|
||||
}
|
||||
for (const [provider, client] of pendingClients) {
|
||||
provider.client = client;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user