@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.
- package/README.md +333 -0
- package/dist/browser.cjs +1939 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +213 -0
- package/dist/browser.d.ts +213 -0
- package/dist/browser.js +1900 -0
- package/dist/browser.js.map +1 -0
- package/dist/cdn/perspective.global.js +406 -0
- package/dist/cdn/perspective.global.js.map +1 -0
- package/dist/constants.cjs +142 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +104 -0
- package/dist/constants.d.ts +104 -0
- package/dist/constants.js +127 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.cjs +1596 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +155 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +1579 -0
- package/dist/index.js.map +1 -0
- package/package.json +83 -0
- package/src/browser.test.ts +388 -0
- package/src/browser.ts +509 -0
- package/src/config.test.ts +81 -0
- package/src/config.ts +95 -0
- package/src/constants.ts +214 -0
- package/src/float.test.ts +332 -0
- package/src/float.ts +231 -0
- package/src/fullpage.test.ts +224 -0
- package/src/fullpage.ts +126 -0
- package/src/iframe.test.ts +1037 -0
- package/src/iframe.ts +421 -0
- package/src/index.ts +61 -0
- package/src/loading.ts +90 -0
- package/src/popup.test.ts +344 -0
- package/src/popup.ts +157 -0
- package/src/slider.test.ts +277 -0
- package/src/slider.ts +158 -0
- package/src/styles.ts +395 -0
- package/src/types.ts +148 -0
- package/src/utils.test.ts +162 -0
- package/src/utils.ts +86 -0
- package/src/widget.test.ts +375 -0
- package/src/widget.ts +195 -0
package/src/constants.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for Perspective Embed SDK
|
|
3
|
+
* This file is SSR-safe - no DOM access at import time
|
|
4
|
+
* Used by both SDK bundle and the main Perspective app
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// SDK Version & Features
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/** SDK version for handshake protocol */
|
|
12
|
+
export const SDK_VERSION = "1.0.0";
|
|
13
|
+
|
|
14
|
+
/** Feature flags as bitset for version negotiation */
|
|
15
|
+
export const FEATURES = {
|
|
16
|
+
RESIZE: 1 << 0, // 0b0001
|
|
17
|
+
THEME_SYNC: 1 << 1, // 0b0010
|
|
18
|
+
ANON_ID: 1 << 2, // 0b0100
|
|
19
|
+
SCROLLBAR_STYLES: 1 << 3, // 0b1000
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/** Current SDK feature set */
|
|
23
|
+
export const CURRENT_FEATURES =
|
|
24
|
+
FEATURES.RESIZE |
|
|
25
|
+
FEATURES.THEME_SYNC |
|
|
26
|
+
FEATURES.ANON_ID |
|
|
27
|
+
FEATURES.SCROLLBAR_STYLES;
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// URL Parameter Keys
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export const PARAM_KEYS = {
|
|
34
|
+
// User identification
|
|
35
|
+
email: "email",
|
|
36
|
+
name: "name",
|
|
37
|
+
|
|
38
|
+
// Navigation
|
|
39
|
+
returnUrl: "returnUrl",
|
|
40
|
+
|
|
41
|
+
// Interview behavior
|
|
42
|
+
voice: "voice",
|
|
43
|
+
scroll: "scroll",
|
|
44
|
+
hideProgress: "hideProgress",
|
|
45
|
+
hideGreeting: "hideGreeting",
|
|
46
|
+
hideBranding: "hideBranding",
|
|
47
|
+
|
|
48
|
+
// Interview mode & auth
|
|
49
|
+
mode: "mode",
|
|
50
|
+
invite: "invite",
|
|
51
|
+
|
|
52
|
+
// System (internal)
|
|
53
|
+
embed: "embed",
|
|
54
|
+
embedType: "embed_type",
|
|
55
|
+
theme: "theme",
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
export type ParamKey = (typeof PARAM_KEYS)[keyof typeof PARAM_KEYS];
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Brand Color Keys
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
export const BRAND_KEYS = {
|
|
65
|
+
// Light mode
|
|
66
|
+
primary: "brand.primary",
|
|
67
|
+
secondary: "brand.secondary",
|
|
68
|
+
bg: "brand.bg",
|
|
69
|
+
text: "brand.text",
|
|
70
|
+
|
|
71
|
+
// Dark mode
|
|
72
|
+
darkPrimary: "brand.dark.primary",
|
|
73
|
+
darkSecondary: "brand.dark.secondary",
|
|
74
|
+
darkBg: "brand.dark.bg",
|
|
75
|
+
darkText: "brand.dark.text",
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
78
|
+
export type BrandKey = (typeof BRAND_KEYS)[keyof typeof BRAND_KEYS];
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// UTM Parameters (auto-forwarded from parent URL)
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
export const UTM_PARAMS = [
|
|
85
|
+
"utm_source",
|
|
86
|
+
"utm_medium",
|
|
87
|
+
"utm_campaign",
|
|
88
|
+
"utm_term",
|
|
89
|
+
"utm_content",
|
|
90
|
+
] as const;
|
|
91
|
+
|
|
92
|
+
export type UtmParam = (typeof UTM_PARAMS)[number];
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Reserved Parameters (cannot be overridden via custom params)
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
export const RESERVED_PARAMS: Set<string> = new Set([
|
|
99
|
+
PARAM_KEYS.embed,
|
|
100
|
+
PARAM_KEYS.embedType,
|
|
101
|
+
PARAM_KEYS.theme,
|
|
102
|
+
BRAND_KEYS.primary,
|
|
103
|
+
BRAND_KEYS.secondary,
|
|
104
|
+
BRAND_KEYS.bg,
|
|
105
|
+
BRAND_KEYS.text,
|
|
106
|
+
BRAND_KEYS.darkPrimary,
|
|
107
|
+
BRAND_KEYS.darkSecondary,
|
|
108
|
+
BRAND_KEYS.darkBg,
|
|
109
|
+
BRAND_KEYS.darkText,
|
|
110
|
+
...UTM_PARAMS,
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Data Attributes (HTML declarative initialization)
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
export const DATA_ATTRS = {
|
|
118
|
+
widget: "data-perspective-widget",
|
|
119
|
+
popup: "data-perspective-popup",
|
|
120
|
+
slider: "data-perspective-slider",
|
|
121
|
+
float: "data-perspective-float", // Primary name
|
|
122
|
+
chat: "data-perspective-chat", // Legacy alias
|
|
123
|
+
fullpage: "data-perspective-fullpage",
|
|
124
|
+
params: "data-perspective-params",
|
|
125
|
+
brand: "data-perspective-brand",
|
|
126
|
+
brandDark: "data-perspective-brand-dark",
|
|
127
|
+
theme: "data-perspective-theme",
|
|
128
|
+
noStyle: "data-perspective-no-style",
|
|
129
|
+
} as const;
|
|
130
|
+
|
|
131
|
+
export type DataAttr = (typeof DATA_ATTRS)[keyof typeof DATA_ATTRS];
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// PostMessage Event Types
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
export const MESSAGE_TYPES = {
|
|
138
|
+
// SDK -> Iframe (initialization)
|
|
139
|
+
init: "perspective:init",
|
|
140
|
+
|
|
141
|
+
// Iframe -> SDK
|
|
142
|
+
ready: "perspective:ready",
|
|
143
|
+
resize: "perspective:resize",
|
|
144
|
+
submit: "perspective:submit",
|
|
145
|
+
close: "perspective:close",
|
|
146
|
+
error: "perspective:error",
|
|
147
|
+
redirect: "perspective:redirect",
|
|
148
|
+
|
|
149
|
+
// SDK -> Iframe (internal)
|
|
150
|
+
anonId: "perspective:anon-id",
|
|
151
|
+
injectStyles: "perspective:inject-styles",
|
|
152
|
+
themeChange: "perspective:theme-change",
|
|
153
|
+
|
|
154
|
+
// Iframe -> SDK (internal)
|
|
155
|
+
requestScrollbarStyles: "perspective:request-scrollbar-styles",
|
|
156
|
+
} as const;
|
|
157
|
+
|
|
158
|
+
export type MessageType = (typeof MESSAGE_TYPES)[keyof typeof MESSAGE_TYPES];
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Error Codes
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
export const ERROR_CODES = {
|
|
165
|
+
SDK_OUTDATED: "SDK_OUTDATED",
|
|
166
|
+
INVALID_RESEARCH: "INVALID_RESEARCH",
|
|
167
|
+
UNKNOWN: "UNKNOWN",
|
|
168
|
+
} as const;
|
|
169
|
+
|
|
170
|
+
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Param Values (for boolean-like string params)
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
export const PARAM_VALUES = {
|
|
177
|
+
disabled: "0",
|
|
178
|
+
enabled: "1",
|
|
179
|
+
true: "true",
|
|
180
|
+
false: "false",
|
|
181
|
+
} as const;
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Theme Values
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
export const THEME_VALUES = {
|
|
188
|
+
dark: "dark",
|
|
189
|
+
light: "light",
|
|
190
|
+
system: "system",
|
|
191
|
+
} as const;
|
|
192
|
+
|
|
193
|
+
export type ThemeValue = (typeof THEME_VALUES)[keyof typeof THEME_VALUES];
|
|
194
|
+
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// Interview Mode Values (for mode param)
|
|
197
|
+
// ============================================================================
|
|
198
|
+
|
|
199
|
+
export const MODE_VALUES = {
|
|
200
|
+
preview: "preview",
|
|
201
|
+
restart: "restart",
|
|
202
|
+
normal: "normal",
|
|
203
|
+
simulated: "simulated",
|
|
204
|
+
} as const;
|
|
205
|
+
|
|
206
|
+
export type ModeValue = (typeof MODE_VALUES)[keyof typeof MODE_VALUES];
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// localStorage Keys
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
export const STORAGE_KEYS = {
|
|
213
|
+
anonId: "perspective-anon-id",
|
|
214
|
+
} as const;
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { createFloatBubble, createChatBubble } from "./float";
|
|
3
|
+
import * as config from "./config";
|
|
4
|
+
|
|
5
|
+
describe("createFloatBubble", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
document.body.innerHTML = "";
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
document.body.innerHTML = "";
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("creates float bubble", () => {
|
|
16
|
+
const handle = createFloatBubble({
|
|
17
|
+
researchId: "test-research-id",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(handle.researchId).toBe("test-research-id");
|
|
21
|
+
expect(handle.type).toBe("float");
|
|
22
|
+
expect(handle.isOpen).toBe(false);
|
|
23
|
+
expect(document.querySelector(".perspective-float-bubble")).toBeTruthy();
|
|
24
|
+
|
|
25
|
+
handle.unmount();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns no-op handle when no DOM", () => {
|
|
29
|
+
vi.spyOn(config, "hasDom").mockReturnValue(false);
|
|
30
|
+
|
|
31
|
+
const handle = createFloatBubble({
|
|
32
|
+
researchId: "test-research-id",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(handle.iframe).toBeNull();
|
|
36
|
+
expect(handle.container).toBeNull();
|
|
37
|
+
expect(handle.isOpen).toBe(false);
|
|
38
|
+
expect(handle.unmount).toBeInstanceOf(Function);
|
|
39
|
+
expect(() => handle.unmount()).not.toThrow();
|
|
40
|
+
expect(() => handle.open()).not.toThrow();
|
|
41
|
+
expect(() => handle.close()).not.toThrow();
|
|
42
|
+
expect(() => handle.toggle()).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("open() opens float window", () => {
|
|
46
|
+
const handle = createFloatBubble({
|
|
47
|
+
researchId: "test-research-id",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(handle.isOpen).toBe(false);
|
|
51
|
+
expect(document.querySelector(".perspective-float-window")).toBeFalsy();
|
|
52
|
+
|
|
53
|
+
handle.open();
|
|
54
|
+
|
|
55
|
+
expect(handle.isOpen).toBe(true);
|
|
56
|
+
expect(document.querySelector(".perspective-float-window")).toBeTruthy();
|
|
57
|
+
|
|
58
|
+
handle.unmount();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("close() closes float window", () => {
|
|
62
|
+
const onClose = vi.fn();
|
|
63
|
+
const handle = createFloatBubble({
|
|
64
|
+
researchId: "test-research-id",
|
|
65
|
+
onClose,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
handle.open();
|
|
69
|
+
expect(handle.isOpen).toBe(true);
|
|
70
|
+
expect(document.querySelector(".perspective-float-window")).toBeTruthy();
|
|
71
|
+
|
|
72
|
+
handle.close();
|
|
73
|
+
|
|
74
|
+
expect(handle.isOpen).toBe(false);
|
|
75
|
+
expect(document.querySelector(".perspective-float-window")).toBeFalsy();
|
|
76
|
+
expect(onClose).toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("toggle() toggles float window", () => {
|
|
80
|
+
const handle = createFloatBubble({
|
|
81
|
+
researchId: "test-research-id",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(handle.isOpen).toBe(false);
|
|
85
|
+
|
|
86
|
+
handle.toggle();
|
|
87
|
+
expect(handle.isOpen).toBe(true);
|
|
88
|
+
expect(document.querySelector(".perspective-float-window")).toBeTruthy();
|
|
89
|
+
|
|
90
|
+
handle.toggle();
|
|
91
|
+
expect(handle.isOpen).toBe(false);
|
|
92
|
+
expect(document.querySelector(".perspective-float-window")).toBeFalsy();
|
|
93
|
+
|
|
94
|
+
handle.unmount();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("clicking bubble toggles float window", () => {
|
|
98
|
+
const handle = createFloatBubble({
|
|
99
|
+
researchId: "test-research-id",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const bubble = document.querySelector(
|
|
103
|
+
".perspective-float-bubble"
|
|
104
|
+
) as HTMLButtonElement;
|
|
105
|
+
expect(bubble).toBeTruthy();
|
|
106
|
+
|
|
107
|
+
bubble.click();
|
|
108
|
+
expect(handle.isOpen).toBe(true);
|
|
109
|
+
expect(document.querySelector(".perspective-float-window")).toBeTruthy();
|
|
110
|
+
|
|
111
|
+
bubble.click();
|
|
112
|
+
expect(handle.isOpen).toBe(false);
|
|
113
|
+
expect(document.querySelector(".perspective-float-window")).toBeFalsy();
|
|
114
|
+
|
|
115
|
+
handle.unmount();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("close button in float window closes it", () => {
|
|
119
|
+
const onClose = vi.fn();
|
|
120
|
+
const handle = createFloatBubble({
|
|
121
|
+
researchId: "test-research-id",
|
|
122
|
+
onClose,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
handle.open();
|
|
126
|
+
|
|
127
|
+
const closeBtn = document.querySelector(
|
|
128
|
+
".perspective-float-window .perspective-close"
|
|
129
|
+
) as HTMLButtonElement;
|
|
130
|
+
expect(closeBtn).toBeTruthy();
|
|
131
|
+
|
|
132
|
+
closeBtn.click();
|
|
133
|
+
|
|
134
|
+
expect(handle.isOpen).toBe(false);
|
|
135
|
+
expect(document.querySelector(".perspective-float-window")).toBeFalsy();
|
|
136
|
+
expect(onClose).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("unmount removes bubble and window", () => {
|
|
140
|
+
const handle = createFloatBubble({
|
|
141
|
+
researchId: "test-research-id",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
handle.open();
|
|
145
|
+
expect(document.querySelector(".perspective-float-bubble")).toBeTruthy();
|
|
146
|
+
expect(document.querySelector(".perspective-float-window")).toBeTruthy();
|
|
147
|
+
|
|
148
|
+
handle.unmount();
|
|
149
|
+
|
|
150
|
+
expect(document.querySelector(".perspective-float-bubble")).toBeFalsy();
|
|
151
|
+
expect(document.querySelector(".perspective-float-window")).toBeFalsy();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("destroy is alias for unmount", () => {
|
|
155
|
+
const handle = createFloatBubble({
|
|
156
|
+
researchId: "test-research-id",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(document.querySelector(".perspective-float-bubble")).toBeTruthy();
|
|
160
|
+
|
|
161
|
+
handle.destroy();
|
|
162
|
+
|
|
163
|
+
expect(document.querySelector(".perspective-float-bubble")).toBeFalsy();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("open when already open is no-op", () => {
|
|
167
|
+
const handle = createFloatBubble({
|
|
168
|
+
researchId: "test-research-id",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
handle.open();
|
|
172
|
+
const window1 = document.querySelector(".perspective-float-window");
|
|
173
|
+
|
|
174
|
+
handle.open();
|
|
175
|
+
const window2 = document.querySelector(".perspective-float-window");
|
|
176
|
+
|
|
177
|
+
expect(window1).toBe(window2);
|
|
178
|
+
|
|
179
|
+
handle.unmount();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("close when already closed is no-op", () => {
|
|
183
|
+
const onClose = vi.fn();
|
|
184
|
+
const handle = createFloatBubble({
|
|
185
|
+
researchId: "test-research-id",
|
|
186
|
+
onClose,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
handle.close();
|
|
190
|
+
handle.close();
|
|
191
|
+
handle.close();
|
|
192
|
+
|
|
193
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
194
|
+
|
|
195
|
+
handle.unmount();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("uses custom host", () => {
|
|
199
|
+
const handle = createFloatBubble({
|
|
200
|
+
researchId: "test-research-id",
|
|
201
|
+
host: "https://custom.example.com",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
handle.open();
|
|
205
|
+
|
|
206
|
+
const iframe = handle.iframe as HTMLIFrameElement;
|
|
207
|
+
expect(iframe.src).toContain("https://custom.example.com");
|
|
208
|
+
|
|
209
|
+
handle.unmount();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("update modifies config", () => {
|
|
213
|
+
const onSubmit1 = vi.fn();
|
|
214
|
+
const onSubmit2 = vi.fn();
|
|
215
|
+
|
|
216
|
+
const handle = createFloatBubble({
|
|
217
|
+
researchId: "test-research-id",
|
|
218
|
+
onSubmit: onSubmit1,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(() => handle.update({ onSubmit: onSubmit2 })).not.toThrow();
|
|
222
|
+
|
|
223
|
+
handle.unmount();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("update() behavior", () => {
|
|
227
|
+
const host = "https://getperspective.ai";
|
|
228
|
+
const researchId = "test-research-id";
|
|
229
|
+
|
|
230
|
+
const sendMessage = (
|
|
231
|
+
iframe: HTMLIFrameElement,
|
|
232
|
+
type: string,
|
|
233
|
+
extra?: Record<string, unknown>
|
|
234
|
+
) => {
|
|
235
|
+
window.dispatchEvent(
|
|
236
|
+
new MessageEvent("message", {
|
|
237
|
+
data: { type, researchId, ...extra },
|
|
238
|
+
origin: host,
|
|
239
|
+
source: iframe.contentWindow,
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
it("update changes which callback is invoked", () => {
|
|
245
|
+
const onSubmit1 = vi.fn();
|
|
246
|
+
const onSubmit2 = vi.fn();
|
|
247
|
+
|
|
248
|
+
const handle = createFloatBubble({
|
|
249
|
+
researchId,
|
|
250
|
+
host,
|
|
251
|
+
onSubmit: onSubmit1,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
handle.open();
|
|
255
|
+
handle.update({ onSubmit: onSubmit2 });
|
|
256
|
+
|
|
257
|
+
sendMessage(handle.iframe!, "perspective:submit");
|
|
258
|
+
|
|
259
|
+
expect(onSubmit2).toHaveBeenCalledTimes(1);
|
|
260
|
+
expect(onSubmit1).not.toHaveBeenCalled();
|
|
261
|
+
|
|
262
|
+
handle.unmount();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("sequential updates only use latest callback", () => {
|
|
266
|
+
const fn1 = vi.fn();
|
|
267
|
+
const fn2 = vi.fn();
|
|
268
|
+
const fn3 = vi.fn();
|
|
269
|
+
|
|
270
|
+
const handle = createFloatBubble({
|
|
271
|
+
researchId,
|
|
272
|
+
host,
|
|
273
|
+
onSubmit: fn1,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
handle.open();
|
|
277
|
+
handle.update({ onSubmit: fn2 });
|
|
278
|
+
handle.update({ onSubmit: fn3 });
|
|
279
|
+
|
|
280
|
+
sendMessage(handle.iframe!, "perspective:submit");
|
|
281
|
+
|
|
282
|
+
expect(fn3).toHaveBeenCalledTimes(1);
|
|
283
|
+
expect(fn2).not.toHaveBeenCalled();
|
|
284
|
+
expect(fn1).not.toHaveBeenCalled();
|
|
285
|
+
|
|
286
|
+
handle.unmount();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("close prevents further callback invocations from that window", () => {
|
|
290
|
+
const onSubmit = vi.fn();
|
|
291
|
+
const onClose = vi.fn();
|
|
292
|
+
|
|
293
|
+
const handle = createFloatBubble({
|
|
294
|
+
researchId,
|
|
295
|
+
host,
|
|
296
|
+
onSubmit,
|
|
297
|
+
onClose,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
handle.open();
|
|
301
|
+
const iframe = handle.iframe!;
|
|
302
|
+
|
|
303
|
+
handle.close();
|
|
304
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
305
|
+
|
|
306
|
+
sendMessage(iframe, "perspective:submit");
|
|
307
|
+
|
|
308
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("createChatBubble", () => {
|
|
314
|
+
afterEach(() => {
|
|
315
|
+
document.body.innerHTML = "";
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("is alias for createFloatBubble", () => {
|
|
319
|
+
expect(createChatBubble).toBe(createFloatBubble);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("creates float bubble", () => {
|
|
323
|
+
const handle = createChatBubble({
|
|
324
|
+
researchId: "test-research-id",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(handle.type).toBe("float");
|
|
328
|
+
expect(document.querySelector(".perspective-float-bubble")).toBeTruthy();
|
|
329
|
+
|
|
330
|
+
handle.unmount();
|
|
331
|
+
});
|
|
332
|
+
});
|