mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): repair shared test isolation (#122451)
This commit is contained in:
committed by
GitHub
parent
72e42eed48
commit
f166d4ee98
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user