fix(ui): repair shared test isolation (#122451)

This commit is contained in:
Peter Steinberger
2026-08-11 22:26:52 -07:00
committed by GitHub
parent 72e42eed48
commit f166d4ee98
6 changed files with 70 additions and 23 deletions
+1
View File
@@ -6,6 +6,7 @@ export const uiIsolatedTestFiles = [
"ui/src/app/bootstrap.test.ts",
"ui/src/app/router-outlet.test.ts",
"ui/src/components/resizable-divider.test.ts",
"ui/src/components/sidebar-update-card.test.ts",
"ui/src/components/viewer-facepile.test.ts",
"ui/src/pages/agents/memory/memory-panel.test.ts",
"ui/src/pages/chat/chat-page-attachment-handoff.test.ts",
+10 -13
View File
@@ -7,9 +7,10 @@ import {
NATIVE_UPDATE_DECLINED_EVENT,
} from "../app/native-link-routing.ts";
import {
answerConfirmDialog,
cancelOpenModalDialogs,
installDialogPolyfill,
nextFrame,
waitForRenderedModalDialog,
waitForConfirmDialogActions,
} from "../test-helpers/modal-dialog.ts";
import { createStorageMock } from "../test-helpers/storage.ts";
import "./sidebar-update-card.ts";
@@ -20,15 +21,9 @@ const DISMISS_KEY = "openclaw:control-ui:update-banner-dismissed:v1";
async function resolveUpdateConfirmation(
label: "Cancel" | "Update and restart" | "Update Mac app and restart",
) {
const { modal } = await waitForRenderedModalDialog(document.body);
const button = [...modal.querySelectorAll("button")].find(
(candidate) => candidate.textContent?.trim() === label,
);
if (!(button instanceof HTMLButtonElement)) {
throw new Error(`Expected ${label} button in the update confirmation`);
}
button.click();
await nextFrame();
const actions = await waitForConfirmDialogActions();
expect(actions.textContent).toContain(label);
answerConfirmDialog(actions, label === "Cancel" ? "cancel" : "confirm");
}
type SidebarUpdateCardElement = HTMLElement & {
@@ -79,6 +74,7 @@ beforeEach(() => {
afterEach(() => {
vi.useRealTimers();
cancelOpenModalDialogs();
document.body.replaceChildren();
restoreDialogPolyfill();
if (originalLocalStorage) {
@@ -91,6 +87,7 @@ afterEach(() => {
} else {
Reflect.deleteProperty(window, "webkit");
}
vi.resetModules();
});
describe("SidebarUpdateCard", () => {
@@ -163,7 +160,7 @@ describe("SidebarUpdateCard", () => {
expect(element.querySelector(".sidebar-update-card__subtitle")).toBeNull();
expect(element.querySelector(".sidebar-update-card__arrow")).toBeNull();
action?.click();
await nextFrame();
await waitForConfirmDialogActions();
expect(onUpdate).not.toHaveBeenCalled();
await resolveUpdateConfirmation("Update and restart");
@@ -239,7 +236,7 @@ describe("SidebarUpdateCard", () => {
expect(action?.textContent).toContain("Update Mac app + Gateway");
expect(action?.textContent).toContain("v2.0.0");
action?.click();
await nextFrame();
await waitForConfirmDialogActions();
expect(postMessage).not.toHaveBeenCalled();
await resolveUpdateConfirmation("Update Mac app and restart");
+18
View File
@@ -153,6 +153,24 @@ describe("openclaw-tooltip", () => {
expect(webAwesomeTooltip(tooltip)?.anchor).toBe(trigger);
});
it("recognizes an HTML trigger created by another document realm", async () => {
const frame = document.createElement("iframe");
document.body.append(frame);
const foreignDocument = frame.contentDocument;
if (!foreignDocument) {
throw new Error("Expected iframe document");
}
const tooltip = document.createElement("openclaw-tooltip") as TooltipElement;
tooltip.content = "Cross-realm tooltip";
const trigger = foreignDocument.createElement("button");
trigger.textContent = "trigger";
tooltip.append(trigger);
document.body.append(tooltip);
await tooltip.updateComplete;
expect(trigger.getAttribute("aria-describedby")).toBeTruthy();
});
it("restores the normal hover delay after the provider reconnects", async () => {
const provider = createProvider();
provider.delay = 40;
+6 -4
View File
@@ -24,6 +24,10 @@ function normalizeTooltipText(text: string) {
return text.replace(/\s+/gu, " ").trim();
}
function isHtmlElement(element: Element): element is HTMLElement {
return element.namespaceURI === "http://www.w3.org/1999/xhtml";
}
class TooltipProvider extends OpenClawLitElement {
@property({ type: Number }) delay = HOVER_DELAY;
@property({ type: Number }) skipDelay = SKIP_DELAY;
@@ -189,9 +193,7 @@ class Tooltip extends OpenClawLitElement {
private attachTrigger() {
const slot = this.renderRoot.querySelector<HTMLSlotElement>("slot:not([name])");
const trigger = slot
?.assignedElements({ flatten: true })
.find((element): element is HTMLElement => element instanceof HTMLElement);
const trigger = slot?.assignedElements({ flatten: true }).find(isHtmlElement);
if (trigger === this.triggerElement) {
return;
}
@@ -412,7 +414,7 @@ class Tooltip extends OpenClawLitElement {
const content = normalizeTooltipText(this.content);
const triggerText = normalizeTooltipText(trigger.textContent ?? "");
const clipsContent = [trigger, ...trigger.querySelectorAll("*")].some(
(element) => element instanceof HTMLElement && element.scrollWidth > element.clientWidth,
(element) => isHtmlElement(element) && element.scrollWidth > element.clientWidth,
);
return Boolean(content && triggerText && triggerText.includes(content) && !clipsContent);
}
@@ -8,12 +8,9 @@ import type {
} from "../../../../packages/gateway-protocol/src/index.js";
import { createDeferred } from "../../../../test/helpers/promise.js";
import type { GatewayBrowserClient } from "../../api/gateway.ts";
import { copyToClipboard } from "../../lib/clipboard.ts";
import type { SessionCapability } from "../../lib/sessions/index.ts";
import { createTestChatPane } from "./chat-pane.test-support.ts";
vi.mock("../../lib/clipboard.ts", () => ({ copyToClipboard: vi.fn() }));
const suggestion: TaskSuggestion = {
id: "task_123",
title: "Remove stale adapter",
@@ -27,16 +24,41 @@ const suggestion: TaskSuggestion = {
describe("chat pane task suggestion lifecycle", () => {
it("surfaces clipboard failure through the pane error path", async () => {
vi.mocked(copyToClipboard).mockResolvedValueOnce(false);
const originalClipboard = Object.getOwnPropertyDescriptor(navigator, "clipboard");
const originalExecCommand = Object.getOwnPropertyDescriptor(document, "execCommand");
const writeText = vi.fn().mockRejectedValue(new Error("denied"));
const execCommand = vi.fn(() => false);
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});
Object.defineProperty(document, "execCommand", {
configurable: true,
value: execCommand,
});
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
const { pane, state } = createTestChatPane({
client,
sessions: {} as SessionCapability,
});
await pane.copyTaskSuggestionPrompt(suggestion);
try {
await pane.copyTaskSuggestionPrompt(suggestion);
} finally {
if (originalClipboard) {
Object.defineProperty(navigator, "clipboard", originalClipboard);
} else {
Reflect.deleteProperty(navigator, "clipboard");
}
if (originalExecCommand) {
Object.defineProperty(document, "execCommand", originalExecCommand);
} else {
Reflect.deleteProperty(document, "execCommand");
}
}
expect(copyToClipboard).toHaveBeenCalledWith(suggestion.prompt);
expect(writeText).toHaveBeenCalledWith(suggestion.prompt);
expect(execCommand).toHaveBeenCalledWith("copy");
expect(state.lastError).toBe("Couldn't copy the prompt to the clipboard");
expect(state.chatError).toBe("Couldn't copy the prompt to the clipboard");
});
+7
View File
@@ -70,6 +70,13 @@ export function answerConfirmDialog(actions: HTMLElement, choice: "confirm" | "c
button.click();
}
/** Let each dialog owner release its module state before a test removes the DOM. */
export function cancelOpenModalDialogs() {
for (const dialog of document.body.querySelectorAll("openclaw-modal-dialog")) {
dialog.dispatchEvent(new CustomEvent("modal-cancel"));
}
}
/** Await a dialog whose owner loads it behind a lazy import, then read it. */
export async function waitForRenderedModalDialog(container: HTMLElement) {
await vi.waitFor(() => {