@perspective-ai/sdk 1.0.0-alpha.2

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 (45) hide show
  1. package/README.md +333 -0
  2. package/dist/browser.cjs +1939 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.d.cts +213 -0
  5. package/dist/browser.d.ts +213 -0
  6. package/dist/browser.js +1900 -0
  7. package/dist/browser.js.map +1 -0
  8. package/dist/cdn/perspective.global.js +406 -0
  9. package/dist/cdn/perspective.global.js.map +1 -0
  10. package/dist/constants.cjs +142 -0
  11. package/dist/constants.cjs.map +1 -0
  12. package/dist/constants.d.cts +104 -0
  13. package/dist/constants.d.ts +104 -0
  14. package/dist/constants.js +127 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/index.cjs +1596 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +155 -0
  19. package/dist/index.d.ts +155 -0
  20. package/dist/index.js +1579 -0
  21. package/dist/index.js.map +1 -0
  22. package/package.json +83 -0
  23. package/src/browser.test.ts +388 -0
  24. package/src/browser.ts +509 -0
  25. package/src/config.test.ts +81 -0
  26. package/src/config.ts +95 -0
  27. package/src/constants.ts +214 -0
  28. package/src/float.test.ts +332 -0
  29. package/src/float.ts +231 -0
  30. package/src/fullpage.test.ts +224 -0
  31. package/src/fullpage.ts +126 -0
  32. package/src/iframe.test.ts +1037 -0
  33. package/src/iframe.ts +421 -0
  34. package/src/index.ts +61 -0
  35. package/src/loading.ts +90 -0
  36. package/src/popup.test.ts +344 -0
  37. package/src/popup.ts +157 -0
  38. package/src/slider.test.ts +277 -0
  39. package/src/slider.ts +158 -0
  40. package/src/styles.ts +395 -0
  41. package/src/types.ts +148 -0
  42. package/src/utils.test.ts +162 -0
  43. package/src/utils.ts +86 -0
  44. package/src/widget.test.ts +375 -0
  45. package/src/widget.ts +195 -0
@@ -0,0 +1,388 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ autoInit,
4
+ init,
5
+ mount,
6
+ destroy,
7
+ destroyAll,
8
+ createWidget,
9
+ openPopup,
10
+ openSlider,
11
+ createFloatBubble,
12
+ createChatBubble,
13
+ createFullpage,
14
+ } from "./browser";
15
+
16
+ describe("browser entry", () => {
17
+ beforeEach(() => {
18
+ document.body.innerHTML = "";
19
+ });
20
+
21
+ afterEach(() => {
22
+ destroyAll();
23
+ document.body.innerHTML = "";
24
+ });
25
+
26
+ describe("exports", () => {
27
+ it("exports all public functions", () => {
28
+ expect(autoInit).toBeInstanceOf(Function);
29
+ expect(init).toBeInstanceOf(Function);
30
+ expect(mount).toBeInstanceOf(Function);
31
+ expect(destroy).toBeInstanceOf(Function);
32
+ expect(destroyAll).toBeInstanceOf(Function);
33
+ expect(createWidget).toBeInstanceOf(Function);
34
+ expect(openPopup).toBeInstanceOf(Function);
35
+ expect(openSlider).toBeInstanceOf(Function);
36
+ expect(createFloatBubble).toBeInstanceOf(Function);
37
+ expect(createFullpage).toBeInstanceOf(Function);
38
+ });
39
+
40
+ it("exports createChatBubble as legacy alias", () => {
41
+ expect(createChatBubble).toBe(createFloatBubble);
42
+ });
43
+ });
44
+
45
+ describe("init", () => {
46
+ it("opens popup", () => {
47
+ const handle = init({ researchId: "test", type: "popup" });
48
+ expect(document.querySelector(".perspective-overlay")).toBeTruthy();
49
+ handle.unmount();
50
+ });
51
+
52
+ it("opens slider", () => {
53
+ const handle = init({ researchId: "test", type: "slider" });
54
+ expect(document.querySelector(".perspective-slider")).toBeTruthy();
55
+ handle.unmount();
56
+ });
57
+
58
+ it("creates float bubble", () => {
59
+ const handle = init({ researchId: "test", type: "float" });
60
+ expect(document.querySelector(".perspective-float-bubble")).toBeTruthy();
61
+ handle.unmount();
62
+ });
63
+
64
+ it("handles legacy chat type as float", () => {
65
+ const handle = init({ researchId: "test", type: "chat" });
66
+ expect(document.querySelector(".perspective-float-bubble")).toBeTruthy();
67
+ handle.unmount();
68
+ });
69
+
70
+ it("creates fullpage", () => {
71
+ const handle = init({ researchId: "test", type: "fullpage" });
72
+ expect(document.querySelector(".perspective-fullpage")).toBeTruthy();
73
+ handle.unmount();
74
+ });
75
+
76
+ it("throws for invalid type", () => {
77
+ expect(() =>
78
+ init({ researchId: "test", type: "invalid" as "popup" })
79
+ ).toThrow(/Unknown embed type/);
80
+ });
81
+
82
+ it("destroys previous instance with same researchId", () => {
83
+ init({ researchId: "test", type: "popup" });
84
+ expect(document.querySelectorAll(".perspective-overlay").length).toBe(1);
85
+
86
+ const handle2 = init({ researchId: "test", type: "popup" });
87
+ expect(document.querySelectorAll(".perspective-overlay").length).toBe(1);
88
+
89
+ handle2.unmount();
90
+ });
91
+ });
92
+
93
+ describe("mount", () => {
94
+ it("creates widget in container", () => {
95
+ const container = document.createElement("div");
96
+ document.body.appendChild(container);
97
+
98
+ const handle = mount(container, { researchId: "test" });
99
+ expect(container.querySelector("iframe[data-perspective]")).toBeTruthy();
100
+ handle.unmount();
101
+ });
102
+
103
+ it("accepts selector string", () => {
104
+ const container = document.createElement("div");
105
+ container.id = "embed-container";
106
+ document.body.appendChild(container);
107
+
108
+ const handle = mount("#embed-container", { researchId: "test" });
109
+ expect(container.querySelector("iframe[data-perspective]")).toBeTruthy();
110
+ handle.unmount();
111
+ });
112
+
113
+ it("throws for invalid selector", () => {
114
+ expect(() => mount("#nonexistent", { researchId: "test" })).toThrow(
115
+ /Container not found/
116
+ );
117
+ });
118
+
119
+ it("falls back to init for non-widget types", () => {
120
+ const container = document.createElement("div");
121
+ document.body.appendChild(container);
122
+
123
+ const handle = mount(container, { researchId: "test", type: "popup" });
124
+ expect(document.querySelector(".perspective-overlay")).toBeTruthy();
125
+ handle.unmount();
126
+ });
127
+ });
128
+
129
+ describe("destroy", () => {
130
+ it("destroys instance by researchId", () => {
131
+ init({ researchId: "test", type: "popup" });
132
+ expect(document.querySelector(".perspective-overlay")).toBeTruthy();
133
+
134
+ destroy("test");
135
+ expect(document.querySelector(".perspective-overlay")).toBeFalsy();
136
+ });
137
+
138
+ it("is no-op for unknown researchId", () => {
139
+ expect(() => destroy("unknown")).not.toThrow();
140
+ });
141
+ });
142
+
143
+ describe("destroyAll", () => {
144
+ it("destroys all instances", () => {
145
+ init({ researchId: "test1", type: "popup" });
146
+ init({ researchId: "test2", type: "slider" });
147
+ expect(document.querySelector(".perspective-overlay")).toBeTruthy();
148
+ expect(document.querySelector(".perspective-slider")).toBeTruthy();
149
+
150
+ destroyAll();
151
+
152
+ expect(document.querySelector(".perspective-overlay")).toBeFalsy();
153
+ expect(document.querySelector(".perspective-slider")).toBeFalsy();
154
+ });
155
+ });
156
+
157
+ describe("autoInit robustness", () => {
158
+ it("ignores element with empty widget attribute", () => {
159
+ document.body.innerHTML = `
160
+ <div data-perspective-widget=""></div>
161
+ `;
162
+
163
+ expect(() => autoInit()).not.toThrow();
164
+ expect(document.querySelector("iframe")).toBeFalsy();
165
+ });
166
+
167
+ it("treats whitespace-only widget attribute as truthy (creates iframe with empty researchId)", () => {
168
+ document.body.innerHTML = `
169
+ <div data-perspective-widget=" "></div>
170
+ `;
171
+
172
+ expect(() => autoInit()).not.toThrow();
173
+ expect(document.querySelector("iframe[data-perspective]")).toBeTruthy();
174
+ });
175
+
176
+ it("handles malformed params attribute gracefully", () => {
177
+ document.body.innerHTML = `
178
+ <div data-perspective-widget="test" data-perspective-params="malformed,,=value,key="></div>
179
+ `;
180
+
181
+ expect(() => autoInit()).not.toThrow();
182
+ expect(document.querySelector("iframe[data-perspective]")).toBeTruthy();
183
+ });
184
+
185
+ it("handles empty params attribute", () => {
186
+ document.body.innerHTML = `
187
+ <div data-perspective-widget="test" data-perspective-params=""></div>
188
+ `;
189
+
190
+ expect(() => autoInit()).not.toThrow();
191
+ expect(document.querySelector("iframe[data-perspective]")).toBeTruthy();
192
+ });
193
+
194
+ it("handles invalid theme attribute value", () => {
195
+ document.body.innerHTML = `
196
+ <div data-perspective-widget="test" data-perspective-theme="invalid-theme"></div>
197
+ `;
198
+
199
+ expect(() => autoInit()).not.toThrow();
200
+ expect(document.querySelector("iframe[data-perspective]")).toBeTruthy();
201
+ });
202
+
203
+ it("handles malformed brand attribute", () => {
204
+ document.body.innerHTML = `
205
+ <div data-perspective-widget="test" data-perspective-brand="not-valid-format"></div>
206
+ `;
207
+
208
+ expect(() => autoInit()).not.toThrow();
209
+ expect(document.querySelector("iframe[data-perspective]")).toBeTruthy();
210
+ });
211
+
212
+ it("calling autoInit twice does not duplicate widgets", () => {
213
+ document.body.innerHTML = `
214
+ <div data-perspective-widget="test-widget"></div>
215
+ `;
216
+
217
+ autoInit();
218
+ autoInit();
219
+
220
+ const iframes = document.querySelectorAll("iframe[data-perspective]");
221
+ expect(iframes.length).toBe(1);
222
+ });
223
+
224
+ it("calling autoInit twice does not duplicate popup handlers", () => {
225
+ document.body.innerHTML = `
226
+ <button data-perspective-popup="test-popup">Open</button>
227
+ `;
228
+
229
+ autoInit();
230
+ autoInit();
231
+
232
+ const button = document.querySelector(
233
+ "[data-perspective-popup]"
234
+ ) as HTMLButtonElement;
235
+ button.click();
236
+
237
+ expect(document.querySelectorAll(".perspective-overlay").length).toBe(1);
238
+ });
239
+
240
+ it("multiple elements with same researchId - second element does not create iframe (dedupe)", () => {
241
+ document.body.innerHTML = `
242
+ <div id="widget1" data-perspective-widget="same-research-id"></div>
243
+ <div id="widget2" data-perspective-widget="same-research-id"></div>
244
+ `;
245
+
246
+ autoInit();
247
+
248
+ const widget1Iframe = document.querySelector("#widget1 iframe");
249
+ const widget2Iframe = document.querySelector("#widget2 iframe");
250
+
251
+ expect(widget1Iframe).toBeTruthy();
252
+ expect(widget2Iframe).toBeFalsy();
253
+ });
254
+
255
+ it("widget with no-style attribute skips styling", () => {
256
+ document.body.innerHTML = `
257
+ <button data-perspective-popup="test" data-perspective-no-style>Open</button>
258
+ `;
259
+
260
+ autoInit();
261
+
262
+ const button = document.querySelector(
263
+ "[data-perspective-popup]"
264
+ ) as HTMLButtonElement;
265
+ expect(button.style.backgroundColor).toBeFalsy();
266
+ });
267
+ });
268
+
269
+ describe("autoInit", () => {
270
+ it("initializes widget from data-perspective-widget", () => {
271
+ document.body.innerHTML = `
272
+ <div data-perspective-widget="test-widget"></div>
273
+ `;
274
+
275
+ autoInit();
276
+
277
+ expect(
278
+ document.querySelector("[data-perspective-widget] iframe")
279
+ ).toBeTruthy();
280
+ });
281
+
282
+ it("initializes fullpage from data-perspective-fullpage", () => {
283
+ document.body.innerHTML = `
284
+ <div data-perspective-fullpage="test-fullpage"></div>
285
+ `;
286
+
287
+ autoInit();
288
+
289
+ expect(document.querySelector(".perspective-fullpage")).toBeTruthy();
290
+ });
291
+
292
+ it("attaches popup click handler from data-perspective-popup", () => {
293
+ document.body.innerHTML = `
294
+ <button data-perspective-popup="test-popup">Open</button>
295
+ `;
296
+
297
+ autoInit();
298
+
299
+ expect(document.querySelector(".perspective-overlay")).toBeFalsy();
300
+
301
+ const button = document.querySelector(
302
+ "[data-perspective-popup]"
303
+ ) as HTMLButtonElement;
304
+ button.click();
305
+
306
+ expect(document.querySelector(".perspective-overlay")).toBeTruthy();
307
+ });
308
+
309
+ it("attaches slider click handler from data-perspective-slider", () => {
310
+ document.body.innerHTML = `
311
+ <button data-perspective-slider="test-slider">Open</button>
312
+ `;
313
+
314
+ autoInit();
315
+
316
+ expect(document.querySelector(".perspective-slider")).toBeFalsy();
317
+
318
+ const button = document.querySelector(
319
+ "[data-perspective-slider]"
320
+ ) as HTMLButtonElement;
321
+ button.click();
322
+
323
+ expect(document.querySelector(".perspective-slider")).toBeTruthy();
324
+ });
325
+
326
+ it("initializes float from data-perspective-float", () => {
327
+ document.body.innerHTML = `
328
+ <div data-perspective-float="test-float"></div>
329
+ `;
330
+
331
+ autoInit();
332
+
333
+ expect(document.querySelector(".perspective-float-bubble")).toBeTruthy();
334
+ });
335
+
336
+ it("initializes float from legacy data-perspective-chat", () => {
337
+ document.body.innerHTML = `
338
+ <div data-perspective-chat="test-chat"></div>
339
+ `;
340
+
341
+ autoInit();
342
+
343
+ expect(document.querySelector(".perspective-float-bubble")).toBeTruthy();
344
+ });
345
+
346
+ it("parses params from data-perspective-params", () => {
347
+ document.body.innerHTML = `
348
+ <div data-perspective-widget="test" data-perspective-params="source=test,user=abc"></div>
349
+ `;
350
+
351
+ autoInit();
352
+
353
+ const iframe = document.querySelector(
354
+ "[data-perspective-widget] iframe"
355
+ ) as HTMLIFrameElement;
356
+ const url = new URL(iframe.src);
357
+ expect(url.searchParams.get("source")).toBe("test");
358
+ expect(url.searchParams.get("user")).toBe("abc");
359
+ });
360
+
361
+ it("parses theme from data-perspective-theme", () => {
362
+ document.body.innerHTML = `
363
+ <div data-perspective-widget="test" data-perspective-theme="dark"></div>
364
+ `;
365
+
366
+ autoInit();
367
+
368
+ const wrapper = document.querySelector(".perspective-embed-root");
369
+ expect(wrapper?.classList.contains("perspective-dark-theme")).toBe(true);
370
+ });
371
+
372
+ it("does not reinitialize popup buttons", () => {
373
+ document.body.innerHTML = `
374
+ <button data-perspective-popup="test">Open</button>
375
+ `;
376
+
377
+ autoInit();
378
+ autoInit();
379
+
380
+ const button = document.querySelector(
381
+ "[data-perspective-popup]"
382
+ ) as HTMLButtonElement;
383
+
384
+ button.click();
385
+ expect(document.querySelectorAll(".perspective-overlay").length).toBe(1);
386
+ });
387
+ });
388
+ });