fix(ui): usage copy actions silently hide clipboard failures (#129562)

* fix(ui): show usage copy success and failure

* test(ui): decouple usage copy proof from CSS classes
This commit is contained in:
Peter Steinberger
2026-08-26 18:14:54 -07:00
committed by GitHub
parent b13993af15
commit d8ff371780
2 changed files with 23 additions and 5 deletions
+20 -2
View File
@@ -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<HTMLButtonElement>(".session-bar-actions button")?.click();
const copyButton = rows[0]?.querySelector<HTMLButtonElement>(".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<HTMLElement>(".session-bar-value")?.click();
expect(onSelectSession).toHaveBeenCalledWith(
+3 -3
View File
@@ -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")}
<span data-copy-label>${t("usage.sessions.copy")}</span>
</button>
<div class="session-bar-value">
${isTokenMode ? formatUsageTokens(value) : formatAnalysisCost(value)}