diff --git a/ui/src/pages/usage/view-overview.test.ts b/ui/src/pages/usage/view-overview.test.ts index 700c965eff96..b8b0a343a3d8 100644 --- a/ui/src/pages/usage/view-overview.test.ts +++ b/ui/src/pages/usage/view-overview.test.ts @@ -82,6 +82,7 @@ function renderDailyChart( afterEach(() => { document.body.replaceChildren(); vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); function directText(element: Element | null | undefined): string | undefined { @@ -557,7 +558,16 @@ describe("renderCostWindowComparison", () => { describe("renderSessionsCard", () => { const noop = () => {}; - it("renders named native session toggles while preserving shift selection and separate copy", () => { + it.each([ + { copied: true, feedback: "Copied!" }, + { copied: false, feedback: "Copy failed" }, + ])("keeps session selection separate while showing $feedback", async ({ copied, feedback }) => { + const writeText = vi.fn(async () => { + if (!copied) { + throw new Error("Clipboard access denied"); + } + }); + vi.stubGlobal("navigator", { clipboard: { writeText } }); const container = document.createElement("div"); document.body.append(container); const onSelectSession = vi.fn<(key: string, shiftKey: boolean) => void>(); @@ -616,7 +626,15 @@ describe("renderSessionsCard", () => { sessions.map((s) => s.key), ); - rows[0]?.querySelector(".session-bar-actions button")?.click(); + const copyButton = rows[0]?.querySelector(".session-bar-actions button"); + copyButton?.click(); + await vi.waitFor(() => { + expect(copyButton?.textContent?.trim()).toBe(feedback); + expect(copyButton?.getAttribute("aria-label")).toBe(feedback); + }); + expect(writeText).toHaveBeenCalledWith("Selected thread"); + expect(copyButton?.dataset[copied ? "copied" : "error"]).toBe("1"); + expect(copyButton?.dataset[copied ? "error" : "copied"]).toBeUndefined(); expect(onSelectSession).toHaveBeenCalledOnce(); rows[0]?.querySelector(".session-bar-value")?.click(); expect(onSelectSession).toHaveBeenCalledWith( diff --git a/ui/src/pages/usage/view-overview.ts b/ui/src/pages/usage/view-overview.ts index fddd2f4db7de..7b1d9e4ff92f 100644 --- a/ui/src/pages/usage/view-overview.ts +++ b/ui/src/pages/usage/view-overview.ts @@ -4,10 +4,10 @@ import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; // Control UI view renders usage render overview screen content. import { html, nothing } from "lit"; import { ifDefined } from "lit/directives/if-defined.js"; +import { handleCopyButton } from "../../components/copy-button.ts"; import { renderSettingsSection } from "../../components/settings-ui.ts"; import { t } from "../../i18n/index.ts"; import "../../components/tooltip.ts"; -import { copyToClipboard } from "../../lib/clipboard.ts"; import { formatDurationCompact } from "../../lib/format.ts"; import { buildUsageCostWindows, @@ -926,10 +926,10 @@ function renderSessionsCard( class="btn btn--sm btn--ghost" @click=${(e: MouseEvent) => { e.stopPropagation(); - void copyToClipboard(formatSessionListLabel(s)); + void handleCopyButton(e, formatSessionListLabel(s), t("usage.sessions.copy")); }} > - ${t("usage.sessions.copy")} + ${t("usage.sessions.copy")}
${isTokenMode ? formatUsageTokens(value) : formatAnalysisCost(value)}