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",
+ });
+ });
+});