fix(ui): use clipboard fallback in setup wizards (#110139)

* fix(ui): use clipboard fallback in setup wizards

* test(ui): verify wizard clipboard payloads

Co-authored-by: WhatsSkiLL <284122573+IWhatsskill@users.noreply.github.com>

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: WhatsSkiLL <284122573+IWhatsskill@users.noreply.github.com>
This commit is contained in:
WhatsSkiLL
2026-07-18 21:36:30 +02:00
committed by GitHub
parent be80cc747c
commit 2632969aae
4 changed files with 100 additions and 6 deletions

View File

@@ -0,0 +1,69 @@
/* @vitest-environment jsdom */
import { nothing, render } from "lit";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { i18n } from "../../i18n/index.ts";
import { renderChannelWizard } from "./wizard-view.ts";
describe("renderChannelWizard", () => {
beforeEach(async () => {
await i18n.setLocale("en");
});
afterEach(() => {
for (const container of document.body.querySelectorAll("div")) {
render(nothing, container);
}
document.body.replaceChildren();
vi.unstubAllGlobals();
delete (document as unknown as { execCommand?: unknown }).execCommand;
});
it("copies setup text through the plain-HTTP clipboard fallback", async () => {
vi.stubGlobal("navigator", {});
let copiedText: string | undefined;
const execCommand = vi.fn().mockImplementation(() => {
copiedText = document.querySelector<HTMLTextAreaElement>("textarea")?.value;
return true;
});
(document as unknown as { execCommand: typeof execCommand }).execCommand = execCommand;
const container = document.createElement("div");
document.body.append(container);
render(
renderChannelWizard({
wizard: {
phase: "step",
channel: null,
step: {
id: "copy-command",
type: "note",
message: "openclaw channels add",
},
stepIndex: 1,
busy: false,
validationError: null,
},
channelLabel: (channelId) => channelId,
multiselectValues: [],
onToggleMultiselect: vi.fn(),
onAnswer: vi.fn(),
onClose: vi.fn(),
whatsappQrDataUrl: null,
whatsappMessage: null,
whatsappConnected: null,
whatsappBusy: false,
onWhatsAppStart: vi.fn(),
onWhatsAppWait: vi.fn(),
}),
container,
);
const copy = container.querySelector<HTMLButtonElement>(".channels-wizard__links button");
expect(copy).not.toBeNull();
copy?.click();
await vi.waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
expect(copiedText).toBe("openclaw channels add");
expect(document.querySelector("textarea")).toBeNull();
});
});

View File

@@ -5,6 +5,7 @@ import "@awesome.me/webawesome/dist/components/radio-group/radio-group.js";
import { html, nothing, type TemplateResult } from "lit";
import { t } from "../../i18n/index.ts";
import "../../components/modal-dialog.ts";
import { copyToClipboard } from "../../lib/clipboard.ts";
import { channelDocsUrl, channelHubMeta, renderChannelArt } from "./hub-meta.ts";
import type {
ChannelWizardState,
@@ -52,11 +53,7 @@ function renderNoteStep(step: ChannelWizardStep, props: ChannelWizardViewProps)
${message
? html`
<div class="channels-wizard__links">
<button
type="button"
class="btn btn--sm"
@click=${() => void navigator.clipboard?.writeText(message)}
>
<button type="button" class="btn btn--sm" @click=${() => void copyToClipboard(message)}>
${t("channels.setup.copyText")}
</button>
</div>

View File

@@ -139,6 +139,8 @@ describe("renderModelSetup", () => {
render(nothing, container);
}
document.body.replaceChildren();
vi.unstubAllGlobals();
delete (document as unknown as { execCommand?: unknown }).execCommand;
});
it("renders candidate, unavailable, sign-in, and manual sections", () => {
@@ -336,6 +338,31 @@ describe("renderModelSetup", () => {
expect(text(container)).toContain("Expires in 10 minutes");
});
it("copies device codes through the plain-HTTP clipboard fallback", async () => {
vi.stubGlobal("navigator", {});
let copiedText: string | undefined;
const execCommand = vi.fn().mockImplementation(() => {
copiedText = document.querySelector<HTMLTextAreaElement>("textarea")?.value;
return true;
});
(document as unknown as { execCommand: typeof execCommand }).execCommand = execCommand;
const container = wizardStep({
id: "device",
type: "note",
deviceCode: { code: "ABCD-EFGH" },
});
const copy = Array.from(container.querySelectorAll<HTMLButtonElement>("button")).find(
(button) => button.textContent?.trim() === "Copy",
);
expect(copy).toBeDefined();
copy?.click();
await vi.waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
expect(copiedText).toBe("ABCD-EFGH");
expect(document.querySelector("textarea")).toBeNull();
});
it("renders sensitive text, select, and confirm steps", () => {
const sensitive = wizardStep(
{ id: "token", type: "text", sensitive: true, placeholder: "Paste token" },

View File

@@ -1,6 +1,7 @@
import { html, nothing, type TemplateResult } from "lit";
import { t } from "../../i18n/index.ts";
import "../../components/modal-dialog.ts";
import { copyToClipboard } from "../../lib/clipboard.ts";
import type { ModelSetupWizardState } from "./state.ts";
type WizardViewProps = {
@@ -25,7 +26,7 @@ function renderDeviceCode(step: Extract<ModelSetupWizardState, { phase: "step" }
<button
type="button"
class="btn btn--sm"
@click=${() => void navigator.clipboard?.writeText(step.deviceCode!.code)}
@click=${() => void copyToClipboard(step.deviceCode!.code)}
>
${t("modelSetup.wizard.copy")}
</button>