diff --git a/src/langs/json/en.json b/src/langs/json/en.json index 484fbec88..d8d9017c8 100644 --- a/src/langs/json/en.json +++ b/src/langs/json/en.json @@ -544,6 +544,7 @@ "note": "Note", "copy": "Copy", "copiedToClipboard": "Copied to clipboard", + "couldNotCopyToClipboard": "Could not copy to clipboard", "permalink": "Permalink", "iframe": "Embed HTML iFrame", "preview": "Embed Preview", diff --git a/src/lib/components/documents/Share.svelte b/src/lib/components/documents/Share.svelte index 21f6e29e7..fbb9d54aa 100644 --- a/src/lib/components/documents/Share.svelte +++ b/src/lib/components/documents/Share.svelte @@ -20,22 +20,22 @@ Organization24, } from "svelte-octicons"; - import Button from "../common/Button.svelte"; + import Button from "$lib/components/common/Button.svelte"; import CustomizeEmbed, { embedSettings } from "./CustomizeEmbed.svelte"; - import Field from "../common/Field.svelte"; - import FieldLabel from "../common/FieldLabel.svelte"; - import Number from "../inputs/Number.svelte"; - import Select from "../inputs/Select.svelte"; - import Tab from "../common/Tab.svelte"; - import Text from "../inputs/Text.svelte"; - import TextArea from "../inputs/TextArea.svelte"; - import Tip from "../common/Tip.svelte"; + import Field from "$lib/components/common/Field.svelte"; + import FieldLabel from "$lib/components/common/FieldLabel.svelte"; + import Number from "$lib/components/inputs/Number.svelte"; + import Select from "$lib/components/inputs/Select.svelte"; + import Tab from "$lib/components/common/Tab.svelte"; + import Text from "$lib/components/inputs/Text.svelte"; + import TextArea from "$lib/components/inputs/TextArea.svelte"; + import Tip from "$lib/components/common/Tip.svelte"; - import Portal from "../layouts/Portal.svelte"; - import Modal from "../layouts/Modal.svelte"; - import Edit from "../forms/Edit.svelte"; + import Portal from "$lib/components/layouts/Portal.svelte"; + import Modal from "$lib/components/layouts/Modal.svelte"; + import Edit from "$lib/components/forms/Edit.svelte"; - import { toast } from "../layouts/Toaster.svelte"; + import copy from "$lib/utils/copy"; import { createEmbedSearchParams } from "$lib/utils/embed"; import { canonicalPageUrl, @@ -108,11 +108,6 @@ break; } } - - async function copy(text: string) { - await navigator.clipboard.writeText(text); - toast($_("share.copiedToClipboard")); - }
diff --git a/src/lib/components/documents/stories/Share.stories.svelte b/src/lib/components/documents/stories/Share.stories.svelte index 885ef933c..04ba88513 100644 --- a/src/lib/components/documents/stories/Share.stories.svelte +++ b/src/lib/components/documents/stories/Share.stories.svelte @@ -1,6 +1,7 @@
diff --git a/src/lib/utils/copy.ts b/src/lib/utils/copy.ts new file mode 100644 index 000000000..2c120475e --- /dev/null +++ b/src/lib/utils/copy.ts @@ -0,0 +1,12 @@ +import { unwrapFunctionStore, _ } from "svelte-i18n"; +import { toast } from "$lib/components/layouts/Toaster.svelte"; + +export default async function copy(text: string) { + const $_ = unwrapFunctionStore(_); + try { + await navigator.clipboard.writeText(text); + toast($_("share.copiedToClipboard")); + } catch { + toast($_("share.couldNotCopyToClipboard"), { status: "error" }); + } +} diff --git a/src/lib/utils/tests/copy.test.ts b/src/lib/utils/tests/copy.test.ts new file mode 100644 index 000000000..aa7dfc8a1 --- /dev/null +++ b/src/lib/utils/tests/copy.test.ts @@ -0,0 +1,70 @@ +import { + describe, + it, + vi, + beforeEach, + afterEach, + expect, + type Mock, +} from "vitest"; +import { toast } from "$lib/components/layouts/Toaster.svelte"; +import { unwrapFunctionStore, _ } from "svelte-i18n"; +import copy from "../copy"; + +// Mock the imports +vi.mock("$lib/components/layouts/Toaster.svelte", () => ({ + toast: vi.fn(), +})); + +vi.mock("svelte-i18n", () => ({ + unwrapFunctionStore: vi.fn(), + _: vi.fn(), +})); + +describe("copy", () => { + const mockUnwrapFunctionStore = unwrapFunctionStore as Mock; + const mockToast = toast as Mock; + + beforeEach(() => { + mockUnwrapFunctionStore.mockReturnValue((key: string) => key); + + // Mock navigator.clipboard + Object.assign(navigator, { + clipboard: { + writeText: vi.fn(), + }, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should copy text to clipboard and show success toast", async () => { + const writeTextSpy = vi + .spyOn(navigator.clipboard, "writeText") + .mockResolvedValueOnce(undefined); + + await copy("test text"); + + expect(writeTextSpy).toHaveBeenCalledWith("test text"); + expect(mockToast).toHaveBeenCalledWith("share.copiedToClipboard"); + }); + + it("should show error toast when clipboard write fails", async () => { + const writeTextSpy = vi + .spyOn(navigator.clipboard, "writeText") + .mockRejectedValueOnce( + new Error( + "NotAllowedError: Failed to execute 'writeText' on 'Clipboard'", + ), + ); + + expect(await copy("test text")).not.toThrowError; + + expect(writeTextSpy).toHaveBeenCalledWith("test text"); + expect(mockToast).toHaveBeenCalledWith("share.couldNotCopyToClipboard", { + status: "error", + }); + }); +});