diff --git a/ui/src/pages/channels/wizard-view.test.ts b/ui/src/pages/channels/wizard-view.test.ts new file mode 100644 index 000000000000..2bf896ed0a9f --- /dev/null +++ b/ui/src/pages/channels/wizard-view.test.ts @@ -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("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(".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(); + }); +}); diff --git a/ui/src/pages/channels/wizard-view.ts b/ui/src/pages/channels/wizard-view.ts index 3b460787dbea..ba40eb03feec 100644 --- a/ui/src/pages/channels/wizard-view.ts +++ b/ui/src/pages/channels/wizard-view.ts @@ -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` diff --git a/ui/src/pages/model-setup/view.test.ts b/ui/src/pages/model-setup/view.test.ts index a389b2ccc7c5..bc4a99dc26e4 100644 --- a/ui/src/pages/model-setup/view.test.ts +++ b/ui/src/pages/model-setup/view.test.ts @@ -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("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("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" }, diff --git a/ui/src/pages/model-setup/wizard-view.ts b/ui/src/pages/model-setup/wizard-view.ts index bc948f2ca737..e94cf63a5915 100644 --- a/ui/src/pages/model-setup/wizard-view.ts +++ b/ui/src/pages/model-setup/wizard-view.ts @@ -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 void navigator.clipboard?.writeText(step.deviceCode!.code)} + @click=${() => void copyToClipboard(step.deviceCode!.code)} > ${t("modelSetup.wizard.copy")}