@copilotkit/shared 1.55.3 → 1.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/a2ui-prompts.cjs +31 -22
  2. package/dist/a2ui-prompts.cjs.map +1 -1
  3. package/dist/a2ui-prompts.d.cts +2 -2
  4. package/dist/a2ui-prompts.d.cts.map +1 -1
  5. package/dist/a2ui-prompts.d.mts +2 -2
  6. package/dist/a2ui-prompts.d.mts.map +1 -1
  7. package/dist/a2ui-prompts.mjs +31 -22
  8. package/dist/a2ui-prompts.mjs.map +1 -1
  9. package/dist/debug.cjs +38 -0
  10. package/dist/debug.cjs.map +1 -0
  11. package/dist/debug.d.cts +29 -0
  12. package/dist/debug.d.cts.map +1 -0
  13. package/dist/debug.d.mts +29 -0
  14. package/dist/debug.d.mts.map +1 -0
  15. package/dist/debug.mjs +37 -0
  16. package/dist/debug.mjs.map +1 -0
  17. package/dist/index.cjs +4 -0
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +3 -1
  20. package/dist/index.d.cts.map +1 -1
  21. package/dist/index.d.mts +3 -1
  22. package/dist/index.d.mts.map +1 -1
  23. package/dist/index.mjs +3 -1
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/index.umd.js +137 -30
  26. package/dist/index.umd.js.map +1 -1
  27. package/dist/package.cjs +1 -1
  28. package/dist/package.mjs +1 -1
  29. package/dist/standard-schema.cjs +4 -2
  30. package/dist/standard-schema.cjs.map +1 -1
  31. package/dist/standard-schema.d.cts +3 -2
  32. package/dist/standard-schema.d.cts.map +1 -1
  33. package/dist/standard-schema.d.mts +3 -2
  34. package/dist/standard-schema.d.mts.map +1 -1
  35. package/dist/standard-schema.mjs +4 -2
  36. package/dist/standard-schema.mjs.map +1 -1
  37. package/dist/utils/clipboard.cjs +28 -0
  38. package/dist/utils/clipboard.cjs.map +1 -0
  39. package/dist/utils/clipboard.d.cts +14 -0
  40. package/dist/utils/clipboard.d.cts.map +1 -0
  41. package/dist/utils/clipboard.d.mts +14 -0
  42. package/dist/utils/clipboard.d.mts.map +1 -0
  43. package/dist/utils/clipboard.mjs +27 -0
  44. package/dist/utils/clipboard.mjs.map +1 -0
  45. package/dist/utils/index.cjs +1 -0
  46. package/dist/utils/index.cjs.map +1 -1
  47. package/dist/utils/index.d.cts +1 -0
  48. package/dist/utils/index.d.cts.map +1 -1
  49. package/dist/utils/index.d.mts +1 -0
  50. package/dist/utils/index.d.mts.map +1 -1
  51. package/dist/utils/index.mjs +1 -0
  52. package/dist/utils/index.mjs.map +1 -1
  53. package/dist/utils/json-schema.cjs +36 -5
  54. package/dist/utils/json-schema.cjs.map +1 -1
  55. package/dist/utils/json-schema.d.cts +1 -1
  56. package/dist/utils/json-schema.d.cts.map +1 -1
  57. package/dist/utils/json-schema.d.mts +1 -1
  58. package/dist/utils/json-schema.d.mts.map +1 -1
  59. package/dist/utils/json-schema.mjs +36 -5
  60. package/dist/utils/json-schema.mjs.map +1 -1
  61. package/dist/utils/types.cjs.map +1 -1
  62. package/dist/utils/types.d.cts +3 -0
  63. package/dist/utils/types.d.cts.map +1 -1
  64. package/dist/utils/types.d.mts +3 -0
  65. package/dist/utils/types.d.mts.map +1 -1
  66. package/dist/utils/types.mjs.map +1 -1
  67. package/package.json +1 -1
  68. package/src/__tests__/debug.test.ts +116 -0
  69. package/src/__tests__/standard-schema.test.ts +92 -0
  70. package/src/a2ui-prompts.ts +31 -22
  71. package/src/debug.ts +55 -0
  72. package/src/index.ts +1 -0
  73. package/src/standard-schema.ts +9 -3
  74. package/src/utils/__tests__/clipboard.test.ts +87 -0
  75. package/src/utils/__tests__/json-schema.test.ts +250 -1
  76. package/src/utils/clipboard.ts +23 -0
  77. package/src/utils/index.ts +1 -0
  78. package/src/utils/json-schema.ts +84 -3
  79. package/src/utils/types.ts +3 -0
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../src/utils/types.ts"],"sourcesContent":["export type MaybePromise<T> = T | PromiseLike<T>;\n\n/**\n * More specific utility for records with at least one key\n */\nexport type NonEmptyRecord<T> =\n T extends Record<string, unknown>\n ? keyof T extends never\n ? never\n : T\n : never;\n\n/**\n * Type representing an agent's basic information\n */\nexport interface AgentDescription {\n name: string;\n className: string;\n description: string;\n}\n\nexport type RuntimeMode = \"sse\" | \"intelligence\";\n\nexport const RUNTIME_MODE_SSE = \"sse\" as const;\nexport const RUNTIME_MODE_INTELLIGENCE = \"intelligence\" as const;\n\nexport interface IntelligenceRuntimeInfo {\n wsUrl: string;\n}\n\nexport type RuntimeLicenseStatus =\n | \"valid\"\n | \"none\"\n | \"expired\"\n | \"expiring\"\n | \"invalid\"\n | \"unknown\";\n\nexport interface RuntimeInfo {\n version: string;\n agents: Record<string, AgentDescription>;\n audioFileTranscriptionEnabled: boolean;\n mode: RuntimeMode;\n intelligence?: IntelligenceRuntimeInfo;\n a2uiEnabled?: boolean;\n openGenerativeUIEnabled?: boolean;\n licenseStatus?: RuntimeLicenseStatus;\n}\n"],"mappings":";AAuBA,MAAa,mBAAmB;AAChC,MAAa,4BAA4B"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../../src/utils/types.ts"],"sourcesContent":["import type { AgentCapabilities } from \"@ag-ui/core\";\n\nexport type MaybePromise<T> = T | PromiseLike<T>;\n\n/**\n * More specific utility for records with at least one key\n */\nexport type NonEmptyRecord<T> =\n T extends Record<string, unknown>\n ? keyof T extends never\n ? never\n : T\n : never;\n\n/**\n * Type representing an agent's basic information\n */\nexport interface AgentDescription {\n name: string;\n className: string;\n description: string;\n capabilities?: AgentCapabilities;\n}\n\nexport type RuntimeMode = \"sse\" | \"intelligence\";\n\nexport const RUNTIME_MODE_SSE = \"sse\" as const;\nexport const RUNTIME_MODE_INTELLIGENCE = \"intelligence\" as const;\n\nexport interface IntelligenceRuntimeInfo {\n wsUrl: string;\n}\n\nexport type RuntimeLicenseStatus =\n | \"valid\"\n | \"none\"\n | \"expired\"\n | \"expiring\"\n | \"invalid\"\n | \"unknown\";\n\nexport interface RuntimeInfo {\n version: string;\n agents: Record<string, AgentDescription>;\n audioFileTranscriptionEnabled: boolean;\n mode: RuntimeMode;\n intelligence?: IntelligenceRuntimeInfo;\n a2uiEnabled?: boolean;\n openGenerativeUIEnabled?: boolean;\n licenseStatus?: RuntimeLicenseStatus;\n}\n"],"mappings":";AA0BA,MAAa,mBAAmB;AAChC,MAAa,4BAA4B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/shared",
3
- "version": "1.55.3",
3
+ "version": "1.56.1",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveDebugConfig } from "../debug";
3
+
4
+ describe("resolveDebugConfig", () => {
5
+ it("returns all-off for undefined", () => {
6
+ expect(resolveDebugConfig(undefined)).toEqual({
7
+ enabled: false,
8
+ events: false,
9
+ lifecycle: false,
10
+ verbose: false,
11
+ });
12
+ });
13
+
14
+ it("returns all-off for false", () => {
15
+ expect(resolveDebugConfig(false)).toEqual({
16
+ enabled: false,
17
+ events: false,
18
+ lifecycle: false,
19
+ verbose: false,
20
+ });
21
+ });
22
+
23
+ it("returns events + lifecycle on but verbose off for true (PII safety)", () => {
24
+ expect(resolveDebugConfig(true)).toEqual({
25
+ enabled: true,
26
+ events: true,
27
+ lifecycle: true,
28
+ verbose: false,
29
+ });
30
+ });
31
+
32
+ it("allows explicit verbose opt-in via object shorthand", () => {
33
+ expect(resolveDebugConfig({ verbose: true })).toEqual({
34
+ enabled: true,
35
+ events: true,
36
+ lifecycle: true,
37
+ verbose: true,
38
+ });
39
+ });
40
+
41
+ it("allows explicit verbose opt-in with events", () => {
42
+ expect(resolveDebugConfig({ events: true, verbose: true })).toEqual({
43
+ enabled: true,
44
+ events: true,
45
+ lifecycle: true,
46
+ verbose: true,
47
+ });
48
+ });
49
+
50
+ it("defaults events and lifecycle to true, verbose to false for empty object", () => {
51
+ expect(resolveDebugConfig({})).toEqual({
52
+ enabled: true,
53
+ events: true,
54
+ lifecycle: true,
55
+ verbose: false,
56
+ });
57
+ });
58
+
59
+ it("respects explicit events: true", () => {
60
+ expect(resolveDebugConfig({ events: true })).toEqual({
61
+ enabled: true,
62
+ events: true,
63
+ lifecycle: true,
64
+ verbose: false,
65
+ });
66
+ });
67
+
68
+ it("respects explicit events: false (lifecycle still defaults true)", () => {
69
+ expect(resolveDebugConfig({ events: false })).toEqual({
70
+ enabled: true,
71
+ events: false,
72
+ lifecycle: true,
73
+ verbose: false,
74
+ });
75
+ });
76
+
77
+ it("respects explicit lifecycle: false (events still defaults true)", () => {
78
+ expect(resolveDebugConfig({ lifecycle: false })).toEqual({
79
+ enabled: true,
80
+ events: true,
81
+ lifecycle: false,
82
+ verbose: false,
83
+ });
84
+ });
85
+
86
+ it("sets enabled to false when both events and lifecycle are false", () => {
87
+ expect(resolveDebugConfig({ events: false, lifecycle: false })).toEqual({
88
+ enabled: false,
89
+ events: false,
90
+ lifecycle: false,
91
+ verbose: false,
92
+ });
93
+ });
94
+
95
+ it("clamps verbose to false when enabled is false (events and lifecycle both false)", () => {
96
+ expect(
97
+ resolveDebugConfig({ events: false, lifecycle: false, verbose: true }),
98
+ ).toEqual({
99
+ enabled: false,
100
+ events: false,
101
+ lifecycle: false,
102
+ verbose: false,
103
+ });
104
+ });
105
+
106
+ it("handles mixed config: events true, lifecycle false, verbose true", () => {
107
+ expect(
108
+ resolveDebugConfig({ events: true, lifecycle: false, verbose: true }),
109
+ ).toEqual({
110
+ enabled: true,
111
+ events: true,
112
+ lifecycle: false,
113
+ verbose: true,
114
+ });
115
+ });
116
+ });
@@ -210,6 +210,98 @@ describe("schemaToJsonSchema", () => {
210
210
  });
211
211
  });
212
212
 
213
+ describe("Zod v4 schemas (via toJSONSchema method)", () => {
214
+ it("calls toJSONSchema() when the method exists on the schema", () => {
215
+ const expectedOutput = {
216
+ type: "object",
217
+ properties: { name: { type: "string" } },
218
+ required: ["name"],
219
+ };
220
+
221
+ const mockZod4Schema: StandardSchemaV1 = {
222
+ "~standard": {
223
+ version: 1,
224
+ vendor: "zod",
225
+ validate: (value: unknown) => ({ value }),
226
+ },
227
+ toJSONSchema: () => expectedOutput,
228
+ } as any;
229
+
230
+ const result = schemaToJsonSchema(mockZod4Schema);
231
+ expect(result).toEqual(expectedOutput);
232
+ });
233
+
234
+ it("uses toJSONSchema() even without zodToJsonSchema option", () => {
235
+ const mockZod4Schema: StandardSchemaV1 = {
236
+ "~standard": {
237
+ version: 1,
238
+ vendor: "zod",
239
+ validate: (value: unknown) => ({ value }),
240
+ },
241
+ toJSONSchema: () => ({
242
+ type: "object",
243
+ properties: { city: { type: "string" } },
244
+ }),
245
+ } as any;
246
+
247
+ // No options passed — toJSONSchema() should still work
248
+ const result = schemaToJsonSchema(mockZod4Schema);
249
+ expect(result).toHaveProperty("properties.city.type", "string");
250
+ });
251
+
252
+ it("prefers toJSONSchema() over zodToJsonSchema fallback for Zod v4", () => {
253
+ const mockZod4Schema: StandardSchemaV1 = {
254
+ "~standard": {
255
+ version: 1,
256
+ vendor: "zod",
257
+ validate: (value: unknown) => ({ value }),
258
+ },
259
+ toJSONSchema: () => ({
260
+ type: "object",
261
+ properties: { fromNative: { type: "boolean" } },
262
+ }),
263
+ } as any;
264
+
265
+ const zodFallback = () => ({
266
+ type: "object",
267
+ properties: { fromFallback: { type: "boolean" } },
268
+ });
269
+
270
+ const result = schemaToJsonSchema(mockZod4Schema, {
271
+ zodToJsonSchema: zodFallback,
272
+ });
273
+
274
+ expect(result).toHaveProperty("properties.fromNative");
275
+ expect(result).not.toHaveProperty("properties.fromFallback");
276
+ });
277
+
278
+ it("prefers ~standard.jsonSchema over toJSONSchema()", () => {
279
+ const mockSchema = {
280
+ "~standard": {
281
+ version: 1,
282
+ vendor: "zod",
283
+ validate: (value: unknown) => ({ value }),
284
+ jsonSchema: {
285
+ input: () => ({
286
+ type: "object",
287
+ properties: { fromStandard: { type: "boolean" } },
288
+ }),
289
+ },
290
+ },
291
+ toJSONSchema: () => ({
292
+ type: "object",
293
+ properties: { fromToJSONSchema: { type: "boolean" } },
294
+ }),
295
+ };
296
+
297
+ const result = schemaToJsonSchema(mockSchema);
298
+
299
+ // Standard JSON Schema V1 should take priority
300
+ expect(result).toHaveProperty("properties.fromStandard");
301
+ expect(result).not.toHaveProperty("properties.fromToJSONSchema");
302
+ });
303
+ });
304
+
213
305
  describe("Error handling", () => {
214
306
  it("throws when schema has no jsonSchema support and no zodToJsonSchema", () => {
215
307
  const mockSchema: StandardSchemaV1 = {
@@ -47,29 +47,38 @@ CRITICAL: Do NOT use "/name" (absolute) inside templates — use "name" (relativ
47
47
  The container's path ("/items") uses a leading slash (absolute), but all
48
48
  components INSIDE the template use paths WITHOUT leading slash.
49
49
 
50
- DATA MODEL:
51
- The "data" key in the tool args is a plain JSON object that initializes the surface
52
- data model. Components bound to paths (e.g. "value": { "path": "/form/name" })
53
- read from and write to this data model. Examples:
54
- For forms: "data": { "form": { "name": "Alice", "email": "" } }
55
- For lists: "data": { "items": [{"name": "Product A"}, {"name": "Product B"}] }
56
- For mixed: "data": { "form": { "query": "" }, "results": [...] }
50
+ COMPONENT VALUES — DEFAULT RULE:
51
+ Use inline literal values for ALL component properties. Pass strings, numbers,
52
+ arrays, and objects directly on the component. Do NOT use { "path": "..." }
53
+ objects unless the property's schema explicitly allows it (see exception below).
54
+ CRITICAL: USING { "path": "..." } ON A PROPERTY THAT DOES NOT DECLARE PATH
55
+ SUPPORT IN ITS SCHEMA WILL CAUSE A RUNTIME CRASH AND BREAK THE ENTIRE UI.
56
+ ALWAYS CHECK THE COMPONENT SCHEMA FIRST IF THE PROPERTY ONLY ACCEPTS A
57
+ PLAIN TYPE, YOU MUST USE A LITERAL VALUE.
58
+ VERY IMPORTANT: THE APPLICATION WILL BREAK IF YOU DO NOT FOLLOW THIS RULE!
57
59
 
58
- FORMS AND TWO-WAY DATA BINDING:
59
- To create editable forms, bind input components to data model paths using { "path": "..." }.
60
- The client automatically writes user input back to the data model at the bound path.
61
- CRITICAL: Using a literal value (e.g. "value": "") makes the field READ-ONLY.
62
- You MUST use { "path": "..." } to make inputs editable.
60
+ For example, a chart's "data" must always be an inline array:
61
+ "data": [{"label": "Jan", "value": 100}, {"label": "Feb", "value": 200}]
62
+ A metric's "value" must always be an inline string:
63
+ "value": "$1,200"
63
64
 
64
- Input components use "value" as the binding property:
65
- "value": { "path": "/form/fieldName" }
65
+ PATH BINDING EXCEPTION SCHEMA-DRIVEN:
66
+ A few properties accept { "path": "/some/path" } as an alternative to a literal
67
+ value. You can identify these in the Available Components schema: the property
68
+ will list BOTH a literal type AND an object-with-path option. If a property only
69
+ shows a single type (string, number, array, etc.), it does NOT support path
70
+ binding — use a literal value only.
66
71
 
67
- To retrieve form values when a button is clicked, include "context" with path references
68
- in the button's action. Paths are resolved to their current values at click time:
69
- "action": { "event": { "name": "submit", "context": { "userName": { "path": "/form/name" } } } }
72
+ Path binding is typically used for editable form inputs so the client can write
73
+ user input back to the data model. When building forms:
74
+ - Bind input "value" to a data model path: "value": { "path": "/form/name" }
75
+ - Pre-fill via the "data" tool argument: "data": { "form": { "name": "Alice" } }
76
+ - Capture values on submit via button action context:
77
+ "action": { "event": { "name": "submit", "context": { "name": { "path": "/form/name" } } } }
70
78
 
71
- To pre-fill form values, pass initial data via the "data" tool argument:
72
- "data": { "form": { "name": "Markus" } }`;
79
+ REPEATING CONTENT uses a structural children format (not the same as value binding):
80
+ children: { componentId: "card-id", path: "/items" }
81
+ Components inside templates use RELATIVE paths (no leading slash): { "path": "name" }.`;
73
82
 
74
83
  /**
75
84
  * Design guidelines — visual design rules, component hierarchy tips,
@@ -94,8 +103,8 @@ Design principles:
94
103
  "action": { "event": { "name": "myAction", "context": { "key": "value" } } }
95
104
  The "event" key holds an OBJECT with "name" (required) and "context" (optional).
96
105
  Do NOT use a flat format like {"event": "name"} — "event" must be an object.
97
- - For forms: every input MUST use path binding on the "value" property
98
- (e.g. "value": { "path": "/form/name" }) to be editable. The submit button's
99
- action context MUST reference the same paths to capture the user's input.
106
+ - For forms: check the component schema if an input's "value" property
107
+ supports path binding, use it for editable fields. The submit button's
108
+ action context should reference the same paths to capture user input.
100
109
 
101
110
  Use the SAME surfaceId as the main surface. Match action names to button action event names.`;
package/src/debug.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Granular debug configuration for CopilotKit runtime and client.
3
+ * Pass `true` to enable events + lifecycle logging (but NOT verbose payloads),
4
+ * or an object for granular control including `verbose: true` for full payloads.
5
+ */
6
+ export type DebugConfig =
7
+ | boolean
8
+ | {
9
+ /** Log every event emitted/received. Default: true */
10
+ events?: boolean;
11
+ /** Log request/run lifecycle. Default: true */
12
+ lifecycle?: boolean;
13
+ /** Log full event payloads instead of summaries. Default: false — must be explicitly opted in */
14
+ verbose?: boolean;
15
+ };
16
+
17
+ /** Normalized debug configuration — all fields resolved to booleans. */
18
+ export interface ResolvedDebugConfig {
19
+ enabled: boolean;
20
+ events: boolean;
21
+ lifecycle: boolean;
22
+ verbose: boolean;
23
+ }
24
+
25
+ /** The all-off config used when debug is falsy. */
26
+ const DEBUG_OFF: ResolvedDebugConfig = {
27
+ enabled: false,
28
+ events: false,
29
+ lifecycle: false,
30
+ verbose: false,
31
+ };
32
+
33
+ /**
34
+ * Normalizes a DebugConfig value into a ResolvedDebugConfig.
35
+ *
36
+ * - `false` / `undefined` → all off
37
+ * - `true` → events + lifecycle on, verbose off (no PII in logs)
38
+ * - object → merges with defaults (events: true, lifecycle: true, verbose: false)
39
+ */
40
+ export function resolveDebugConfig(
41
+ debug: DebugConfig | undefined,
42
+ ): ResolvedDebugConfig {
43
+ if (!debug) return DEBUG_OFF;
44
+
45
+ if (debug === true) {
46
+ return { enabled: true, events: true, lifecycle: true, verbose: false };
47
+ }
48
+
49
+ const events = debug.events ?? true;
50
+ const lifecycle = debug.lifecycle ?? true;
51
+ const enabled = events || lifecycle;
52
+ const verbose = enabled && (debug.verbose ?? false);
53
+
54
+ return { enabled, events, lifecycle, verbose };
55
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from "./types";
2
2
  export * from "./utils";
3
3
  export * from "./constants";
4
4
  export * from "./telemetry";
5
+ export * from "./debug";
5
6
  export * from "./standard-schema";
6
7
  export * from "./attachments";
7
8
 
@@ -48,9 +48,10 @@ function hasStandardJsonSchema(
48
48
  * Strategy:
49
49
  * 1. If the schema implements Standard JSON Schema V1 (`~standard.jsonSchema`),
50
50
  * call `schema['~standard'].jsonSchema.input({ target: 'draft-07' })`.
51
- * 2. If the schema is a Zod v3 schema (`~standard.vendor === 'zod'`), use the
51
+ * 2. If the schema exposes a `toJSONSchema()` method (Zod v4), call it directly.
52
+ * 3. If the schema is a Zod v3 schema (`~standard.vendor === 'zod'`), use the
52
53
  * injected `zodToJsonSchema()` function.
53
- * 3. Otherwise throw a descriptive error.
54
+ * 4. Otherwise throw a descriptive error.
54
55
  */
55
56
  export function schemaToJsonSchema(
56
57
  schema: StandardSchemaV1,
@@ -61,7 +62,12 @@ export function schemaToJsonSchema(
61
62
  return schema["~standard"].jsonSchema.input({ target: "draft-07" });
62
63
  }
63
64
 
64
- // 2. Zod v3 fallback
65
+ // 2. Zod v4 native — exposes toJSONSchema() on the schema itself
66
+ if (typeof (schema as any).toJSONSchema === "function") {
67
+ return (schema as any).toJSONSchema() as Record<string, unknown>;
68
+ }
69
+
70
+ // 3. Zod v3 fallback
65
71
  const vendor = schema["~standard"].vendor;
66
72
  if (vendor === "zod" && options?.zodToJsonSchema) {
67
73
  return options.zodToJsonSchema(schema, { $refStrategy: "none" });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { copyToClipboard } from "../clipboard";
3
+
4
+ // Mock navigator for Node 20 environments where it doesn't exist
5
+ if (typeof globalThis.navigator === "undefined") {
6
+ Object.defineProperty(globalThis, "navigator", {
7
+ value: { clipboard: { writeText: vi.fn() } },
8
+ writable: true,
9
+ configurable: true,
10
+ });
11
+ }
12
+
13
+ describe("copyToClipboard", () => {
14
+ let originalClipboard: Clipboard;
15
+
16
+ beforeEach(() => {
17
+ originalClipboard = navigator.clipboard;
18
+ });
19
+
20
+ afterEach(() => {
21
+ Object.defineProperty(navigator, "clipboard", {
22
+ value: originalClipboard,
23
+ writable: true,
24
+ configurable: true,
25
+ });
26
+ });
27
+
28
+ it("returns true on successful clipboard write", async () => {
29
+ const writeTextMock = vi.fn().mockResolvedValue(undefined);
30
+ Object.defineProperty(navigator, "clipboard", {
31
+ value: { writeText: writeTextMock },
32
+ writable: true,
33
+ configurable: true,
34
+ });
35
+
36
+ const result = await copyToClipboard("hello");
37
+ expect(result).toBe(true);
38
+ expect(writeTextMock).toHaveBeenCalledWith("hello");
39
+ });
40
+
41
+ it("returns false when clipboard API is unavailable", async () => {
42
+ Object.defineProperty(navigator, "clipboard", {
43
+ value: undefined,
44
+ writable: true,
45
+ configurable: true,
46
+ });
47
+
48
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
49
+ const result = await copyToClipboard("hello");
50
+ expect(result).toBe(false);
51
+ expect(consoleSpy).toHaveBeenCalledWith("Clipboard API is not available");
52
+ consoleSpy.mockRestore();
53
+ });
54
+
55
+ it("returns false when writeText is not available", async () => {
56
+ Object.defineProperty(navigator, "clipboard", {
57
+ value: {},
58
+ writable: true,
59
+ configurable: true,
60
+ });
61
+
62
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
63
+ const result = await copyToClipboard("hello");
64
+ expect(result).toBe(false);
65
+ consoleSpy.mockRestore();
66
+ });
67
+
68
+ it("returns false when writeText rejects", async () => {
69
+ const writeTextMock = vi
70
+ .fn()
71
+ .mockRejectedValue(new Error("Permission denied"));
72
+ Object.defineProperty(navigator, "clipboard", {
73
+ value: { writeText: writeTextMock },
74
+ writable: true,
75
+ configurable: true,
76
+ });
77
+
78
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
79
+ const result = await copyToClipboard("hello");
80
+ expect(result).toBe(false);
81
+ expect(consoleSpy).toHaveBeenCalledWith(
82
+ "Failed to copy to clipboard:",
83
+ expect.any(Error),
84
+ );
85
+ consoleSpy.mockRestore();
86
+ });
87
+ });