@beeport/widget 0.1.0 → 0.1.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/dist/index.cjs +4 -2
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -2
- package/package.json +5 -10
- package/src/index.ts +166 -0
- package/test/index.test.ts +388 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +13 -0
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ function init(config) {
|
|
|
34
34
|
throw new Error("BeePort widget requires a browser environment");
|
|
35
35
|
}
|
|
36
36
|
if (window.__beeport_loaded) return;
|
|
37
|
+
if (document.querySelector(`script[${SCRIPT_ATTR}]`)) return;
|
|
37
38
|
const script = document.createElement("script");
|
|
38
39
|
script.src = CDN_URL;
|
|
39
40
|
script.setAttribute(SCRIPT_ATTR, "true");
|
|
@@ -50,12 +51,13 @@ function init(config) {
|
|
|
50
51
|
if (config.text !== void 0) {
|
|
51
52
|
script.setAttribute("data-text", config.text);
|
|
52
53
|
}
|
|
54
|
+
if (config.apiBase !== void 0) {
|
|
55
|
+
script.setAttribute("data-api-base", config.apiBase);
|
|
56
|
+
}
|
|
53
57
|
document.body.appendChild(script);
|
|
54
|
-
window.__beeport_loaded = true;
|
|
55
58
|
}
|
|
56
59
|
function destroy() {
|
|
57
60
|
if (typeof window === "undefined") return;
|
|
58
|
-
window.dispatchEvent(new CustomEvent("beeport:destroy"));
|
|
59
61
|
document.body.querySelectorAll(`script[${SCRIPT_ATTR}]`).forEach((el) => el.remove());
|
|
60
62
|
document.body.querySelectorAll("[data-beeport-trigger]").forEach((el) => el.remove());
|
|
61
63
|
document.body.querySelectorAll("[data-beeport-card]").forEach((el) => el.remove());
|
package/dist/index.d.cts
CHANGED
|
@@ -21,6 +21,8 @@ interface BeePortConfig {
|
|
|
21
21
|
accent?: string;
|
|
22
22
|
/** Custom button label text. */
|
|
23
23
|
text?: string;
|
|
24
|
+
/** API base URL override (e.g. "https://api-dev.beeport.ai"). */
|
|
25
|
+
apiBase?: string;
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* Load and initialise the BeePort widget by injecting the CDN script tag.
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ interface BeePortConfig {
|
|
|
21
21
|
accent?: string;
|
|
22
22
|
/** Custom button label text. */
|
|
23
23
|
text?: string;
|
|
24
|
+
/** API base URL override (e.g. "https://api-dev.beeport.ai"). */
|
|
25
|
+
apiBase?: string;
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* Load and initialise the BeePort widget by injecting the CDN script tag.
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ function init(config) {
|
|
|
6
6
|
throw new Error("BeePort widget requires a browser environment");
|
|
7
7
|
}
|
|
8
8
|
if (window.__beeport_loaded) return;
|
|
9
|
+
if (document.querySelector(`script[${SCRIPT_ATTR}]`)) return;
|
|
9
10
|
const script = document.createElement("script");
|
|
10
11
|
script.src = CDN_URL;
|
|
11
12
|
script.setAttribute(SCRIPT_ATTR, "true");
|
|
@@ -22,12 +23,13 @@ function init(config) {
|
|
|
22
23
|
if (config.text !== void 0) {
|
|
23
24
|
script.setAttribute("data-text", config.text);
|
|
24
25
|
}
|
|
26
|
+
if (config.apiBase !== void 0) {
|
|
27
|
+
script.setAttribute("data-api-base", config.apiBase);
|
|
28
|
+
}
|
|
25
29
|
document.body.appendChild(script);
|
|
26
|
-
window.__beeport_loaded = true;
|
|
27
30
|
}
|
|
28
31
|
function destroy() {
|
|
29
32
|
if (typeof window === "undefined") return;
|
|
30
|
-
window.dispatchEvent(new CustomEvent("beeport:destroy"));
|
|
31
33
|
document.body.querySelectorAll(`script[${SCRIPT_ATTR}]`).forEach((el) => el.remove());
|
|
32
34
|
document.body.querySelectorAll("[data-beeport-trigger]").forEach((el) => el.remove());
|
|
33
35
|
document.body.querySelectorAll("[data-beeport-card]").forEach((el) => el.remove());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beeport/widget",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "BeePort feedback widget — npm package wrapper around CDN widget.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -13,19 +13,11 @@
|
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist",
|
|
18
|
-
"README.md"
|
|
19
|
-
],
|
|
20
|
-
"publishConfig": {
|
|
21
|
-
"access": "public"
|
|
22
|
-
},
|
|
23
16
|
"sideEffects": false,
|
|
24
17
|
"scripts": {
|
|
25
18
|
"test": "vitest run",
|
|
26
19
|
"test:watch": "vitest",
|
|
27
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
28
|
-
"prepack": "npm run build"
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
29
21
|
},
|
|
30
22
|
"devDependencies": {
|
|
31
23
|
"@types/node": "^22.0.0",
|
|
@@ -39,5 +31,8 @@
|
|
|
39
31
|
"feedback",
|
|
40
32
|
"widget"
|
|
41
33
|
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
42
37
|
"license": "UNLICENSED"
|
|
43
38
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beeport/widget
|
|
3
|
+
*
|
|
4
|
+
* npm package wrapper around the CDN-served BeePort widget.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { init } from '@beeport/widget';
|
|
8
|
+
* init({ project: 'prj_abc123' });
|
|
9
|
+
*
|
|
10
|
+
* SSR-safe: no window/document access at module import time.
|
|
11
|
+
* All browser APIs are only accessed inside function bodies.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface BeePortConfig {
|
|
19
|
+
/** Project API key (required). */
|
|
20
|
+
project: string;
|
|
21
|
+
/** Colour theme forwarded to the widget iframe. */
|
|
22
|
+
theme?: "light" | "dark" | "auto";
|
|
23
|
+
/** Position of the floating button. */
|
|
24
|
+
position?: "bottom-right" | "bottom-left";
|
|
25
|
+
/** Custom accent colour (any CSS colour string). */
|
|
26
|
+
accent?: string;
|
|
27
|
+
/** Custom button label text. */
|
|
28
|
+
text?: string;
|
|
29
|
+
/** API base URL override (e.g. "https://api-dev.beeport.ai"). */
|
|
30
|
+
apiBase?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Constants
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const CDN_URL = "https://cdn.beeport.ai/widget.js";
|
|
38
|
+
const SCRIPT_ATTR = "data-beeport-script";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Exported API
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load and initialise the BeePort widget by injecting the CDN script tag.
|
|
46
|
+
*
|
|
47
|
+
* Idempotent — calling init() multiple times is safe; the second call is a
|
|
48
|
+
* no-op if the widget is already loaded.
|
|
49
|
+
*
|
|
50
|
+
* @throws Error if called in a non-browser environment (SSR guard).
|
|
51
|
+
*/
|
|
52
|
+
export function init(config: BeePortConfig): void {
|
|
53
|
+
if (typeof window === "undefined") {
|
|
54
|
+
throw new Error("BeePort widget requires a browser environment");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Deduplication guard — check both the loaded flag (set by CDN script after
|
|
58
|
+
// init) and whether we've already injected the script tag (covers the window
|
|
59
|
+
// between injection and CDN script execution).
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
if ((window as any).__beeport_loaded) return;
|
|
62
|
+
if (document.querySelector(`script[${SCRIPT_ATTR}]`)) return;
|
|
63
|
+
|
|
64
|
+
const script = document.createElement("script");
|
|
65
|
+
script.src = CDN_URL;
|
|
66
|
+
script.setAttribute(SCRIPT_ATTR, "true");
|
|
67
|
+
script.setAttribute("data-project", config.project);
|
|
68
|
+
|
|
69
|
+
if (config.theme !== undefined) {
|
|
70
|
+
script.setAttribute("data-theme", config.theme);
|
|
71
|
+
}
|
|
72
|
+
if (config.position !== undefined) {
|
|
73
|
+
script.setAttribute("data-position", config.position);
|
|
74
|
+
}
|
|
75
|
+
if (config.accent !== undefined) {
|
|
76
|
+
script.setAttribute("data-accent", config.accent);
|
|
77
|
+
}
|
|
78
|
+
if (config.text !== undefined) {
|
|
79
|
+
script.setAttribute("data-text", config.text);
|
|
80
|
+
}
|
|
81
|
+
if (config.apiBase !== undefined) {
|
|
82
|
+
script.setAttribute("data-api-base", config.apiBase);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
document.body.appendChild(script);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remove all widget DOM elements and reset the loaded flag.
|
|
90
|
+
*
|
|
91
|
+
* After calling destroy(), init() can be called again to re-initialise.
|
|
92
|
+
* Safe to call before init() or in non-browser environments (no-op).
|
|
93
|
+
*/
|
|
94
|
+
export function destroy(): void {
|
|
95
|
+
if (typeof window === "undefined") return;
|
|
96
|
+
|
|
97
|
+
// Remove the injected script tag
|
|
98
|
+
document.body
|
|
99
|
+
.querySelectorAll(`script[${SCRIPT_ATTR}]`)
|
|
100
|
+
.forEach((el) => el.remove());
|
|
101
|
+
|
|
102
|
+
// Remove floating trigger button
|
|
103
|
+
document.body
|
|
104
|
+
.querySelectorAll("[data-beeport-trigger]")
|
|
105
|
+
.forEach((el) => el.remove());
|
|
106
|
+
|
|
107
|
+
// Remove desktop card
|
|
108
|
+
document.body
|
|
109
|
+
.querySelectorAll("[data-beeport-card]")
|
|
110
|
+
.forEach((el) => el.remove());
|
|
111
|
+
|
|
112
|
+
// Remove mobile bottom sheet
|
|
113
|
+
document.body
|
|
114
|
+
.querySelectorAll("[data-beeport-sheet]")
|
|
115
|
+
.forEach((el) => el.remove());
|
|
116
|
+
|
|
117
|
+
// Reset deduplication flag so init() can run again
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
+
delete (window as any).__beeport_loaded;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Programmatically open the feedback widget.
|
|
124
|
+
*
|
|
125
|
+
* Dispatches a `beeport:open` custom event on window. The loaded widget
|
|
126
|
+
* listens for this event and opens the iframe panel.
|
|
127
|
+
*
|
|
128
|
+
* Safe to call in non-browser environments (no-op).
|
|
129
|
+
*/
|
|
130
|
+
export function open(): void {
|
|
131
|
+
if (typeof window === "undefined") return;
|
|
132
|
+
window.dispatchEvent(new CustomEvent("beeport:open"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Programmatically close the feedback widget.
|
|
137
|
+
*
|
|
138
|
+
* Dispatches a `beeport:close` custom event on window. The loaded widget
|
|
139
|
+
* listens for this event and closes the iframe panel.
|
|
140
|
+
*
|
|
141
|
+
* Safe to call in non-browser environments (no-op).
|
|
142
|
+
*/
|
|
143
|
+
export function close(): void {
|
|
144
|
+
if (typeof window === "undefined") return;
|
|
145
|
+
window.dispatchEvent(new CustomEvent("beeport:close"));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// BeeportWidget namespace (default export)
|
|
150
|
+
//
|
|
151
|
+
// Provides BeeportWidget.init() usage pattern in addition to named exports.
|
|
152
|
+
// Both patterns use the same underlying functions — no duplication.
|
|
153
|
+
//
|
|
154
|
+
// ESM: import BeeportWidget from '@beeport/widget'
|
|
155
|
+
// CJS: const BeeportWidget = require('@beeport/widget').default
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* BeeportWidget namespace — use this for the BeeportWidget.init() call pattern.
|
|
160
|
+
*
|
|
161
|
+
* All methods are identical to the named exports and are tree-shakeable when
|
|
162
|
+
* destructured (e.g. `const { init } = BeeportWidget`).
|
|
163
|
+
*/
|
|
164
|
+
const BeeportWidget = { init, destroy, open, close } as const;
|
|
165
|
+
|
|
166
|
+
export default BeeportWidget;
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for @beeport/widget npm package.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - SSR safety: no window/document access at import time
|
|
6
|
+
* - init(): script tag injection, config attributes, deduplication guard
|
|
7
|
+
* - destroy(): removes script tag and widget elements
|
|
8
|
+
* - open() / close(): dispatches custom events
|
|
9
|
+
* - TypeScript types (compile-time, verified by import shape)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
13
|
+
import BeeportWidget, { init, destroy, open, close, type BeePortConfig } from "../src/index.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Setup / teardown
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
document.body.replaceChildren();
|
|
21
|
+
document.head.replaceChildren();
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
delete (window as any).__beeport_loaded;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
document.body.replaceChildren();
|
|
28
|
+
document.head.replaceChildren();
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
delete (window as any).__beeport_loaded;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// SSR safety
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
describe("SSR safety", () => {
|
|
38
|
+
it("module exports init, destroy, open, close functions", () => {
|
|
39
|
+
// If import itself accessed window/document it would throw in Node.
|
|
40
|
+
// Since we are running in jsdom this verifies the shape at minimum.
|
|
41
|
+
expect(typeof init).toBe("function");
|
|
42
|
+
expect(typeof destroy).toBe("function");
|
|
43
|
+
expect(typeof open).toBe("function");
|
|
44
|
+
expect(typeof close).toBe("function");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("init() throws a descriptive error when window is undefined", () => {
|
|
48
|
+
// Simulate SSR by temporarily removing window
|
|
49
|
+
const original = global.window;
|
|
50
|
+
// @ts-expect-error — intentionally deleting window for SSR test
|
|
51
|
+
delete global.window;
|
|
52
|
+
|
|
53
|
+
expect(() => init({ project: "prj_abc" })).toThrow(
|
|
54
|
+
"BeePort widget requires a browser environment"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Restore window
|
|
58
|
+
global.window = original;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// init()
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
describe("init()", () => {
|
|
67
|
+
it("injects a <script> tag into document.body", () => {
|
|
68
|
+
init({ project: "prj_abc" });
|
|
69
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
70
|
+
"script[data-beeport-script]"
|
|
71
|
+
);
|
|
72
|
+
expect(script).not.toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("script src points to the CDN widget URL", () => {
|
|
76
|
+
init({ project: "prj_abc" });
|
|
77
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
78
|
+
"script[data-beeport-script]"
|
|
79
|
+
);
|
|
80
|
+
expect(script?.src).toBe("https://cdn.beeport.ai/widget.js");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("sets data-project attribute on the script tag", () => {
|
|
84
|
+
init({ project: "prj_test123" });
|
|
85
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
86
|
+
"script[data-beeport-script]"
|
|
87
|
+
);
|
|
88
|
+
expect(script?.getAttribute("data-project")).toBe("prj_test123");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("sets data-theme attribute when provided", () => {
|
|
92
|
+
init({ project: "prj_abc", theme: "dark" });
|
|
93
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
94
|
+
"script[data-beeport-script]"
|
|
95
|
+
);
|
|
96
|
+
expect(script?.getAttribute("data-theme")).toBe("dark");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does not set data-theme when not provided", () => {
|
|
100
|
+
init({ project: "prj_abc" });
|
|
101
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
102
|
+
"script[data-beeport-script]"
|
|
103
|
+
);
|
|
104
|
+
expect(script?.hasAttribute("data-theme")).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("sets data-position when provided", () => {
|
|
108
|
+
init({ project: "prj_abc", position: "bottom-left" });
|
|
109
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
110
|
+
"script[data-beeport-script]"
|
|
111
|
+
);
|
|
112
|
+
expect(script?.getAttribute("data-position")).toBe("bottom-left");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("does not set data-position when not provided", () => {
|
|
116
|
+
init({ project: "prj_abc" });
|
|
117
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
118
|
+
"script[data-beeport-script]"
|
|
119
|
+
);
|
|
120
|
+
expect(script?.hasAttribute("data-position")).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("sets data-accent when provided", () => {
|
|
124
|
+
init({ project: "prj_abc", accent: "#ff0000" });
|
|
125
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
126
|
+
"script[data-beeport-script]"
|
|
127
|
+
);
|
|
128
|
+
expect(script?.getAttribute("data-accent")).toBe("#ff0000");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("does not set data-accent when not provided", () => {
|
|
132
|
+
init({ project: "prj_abc" });
|
|
133
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
134
|
+
"script[data-beeport-script]"
|
|
135
|
+
);
|
|
136
|
+
expect(script?.hasAttribute("data-accent")).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("sets data-text when provided", () => {
|
|
140
|
+
init({ project: "prj_abc", text: "Feedback" });
|
|
141
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
142
|
+
"script[data-beeport-script]"
|
|
143
|
+
);
|
|
144
|
+
expect(script?.getAttribute("data-text")).toBe("Feedback");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("does not set data-text when not provided", () => {
|
|
148
|
+
init({ project: "prj_abc" });
|
|
149
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
150
|
+
"script[data-beeport-script]"
|
|
151
|
+
);
|
|
152
|
+
expect(script?.hasAttribute("data-text")).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("does not set window.__beeport_loaded (CDN script sets it after actual init)", () => {
|
|
156
|
+
init({ project: "prj_abc" });
|
|
157
|
+
// The NPM wrapper no longer sets __beeport_loaded — the CDN widget.js
|
|
158
|
+
// sets it after creating the button. This avoids a race condition where
|
|
159
|
+
// the flag blocks the CDN script's autoInit.
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
|
+
expect((window as any).__beeport_loaded).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("does not inject a second script if already loaded", () => {
|
|
165
|
+
init({ project: "prj_abc" });
|
|
166
|
+
init({ project: "prj_abc" });
|
|
167
|
+
const scripts = document.body.querySelectorAll("script[data-beeport-script]");
|
|
168
|
+
expect(scripts).toHaveLength(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns early if __beeport_loaded is pre-set", () => {
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
(window as any).__beeport_loaded = true;
|
|
174
|
+
init({ project: "prj_abc" });
|
|
175
|
+
const scripts = document.body.querySelectorAll("script[data-beeport-script]");
|
|
176
|
+
expect(scripts).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("accepts all optional config fields without throwing", () => {
|
|
180
|
+
const config: BeePortConfig = {
|
|
181
|
+
project: "prj_abc",
|
|
182
|
+
theme: "auto",
|
|
183
|
+
position: "bottom-right",
|
|
184
|
+
accent: "#333333",
|
|
185
|
+
text: "Send feedback",
|
|
186
|
+
};
|
|
187
|
+
expect(() => init(config)).not.toThrow();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// destroy()
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
describe("destroy()", () => {
|
|
196
|
+
it("removes the injected script tag", () => {
|
|
197
|
+
init({ project: "prj_abc" });
|
|
198
|
+
destroy();
|
|
199
|
+
const script = document.body.querySelector("script[data-beeport-script]");
|
|
200
|
+
expect(script).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("resets the __beeport_loaded flag", () => {
|
|
204
|
+
init({ project: "prj_abc" });
|
|
205
|
+
destroy();
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
207
|
+
expect((window as any).__beeport_loaded).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("removes the floating button element if present", () => {
|
|
211
|
+
init({ project: "prj_abc" });
|
|
212
|
+
// Simulate widget creating a button
|
|
213
|
+
const btn = document.createElement("button");
|
|
214
|
+
btn.setAttribute("data-beeport-trigger", "true");
|
|
215
|
+
document.body.appendChild(btn);
|
|
216
|
+
destroy();
|
|
217
|
+
const remaining = document.body.querySelector("[data-beeport-trigger]");
|
|
218
|
+
expect(remaining).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("removes any iframe card if open", () => {
|
|
222
|
+
init({ project: "prj_abc" });
|
|
223
|
+
// Simulate an open card
|
|
224
|
+
const card = document.createElement("div");
|
|
225
|
+
card.setAttribute("data-beeport-card", "true");
|
|
226
|
+
document.body.appendChild(card);
|
|
227
|
+
destroy();
|
|
228
|
+
const remaining = document.body.querySelector("[data-beeport-card]");
|
|
229
|
+
expect(remaining).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("removes any mobile sheet if open", () => {
|
|
233
|
+
init({ project: "prj_abc" });
|
|
234
|
+
const sheet = document.createElement("div");
|
|
235
|
+
sheet.setAttribute("data-beeport-sheet", "true");
|
|
236
|
+
document.body.appendChild(sheet);
|
|
237
|
+
destroy();
|
|
238
|
+
const remaining = document.body.querySelector("[data-beeport-sheet]");
|
|
239
|
+
expect(remaining).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("does not throw when called before init", () => {
|
|
243
|
+
expect(() => destroy()).not.toThrow();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("does not throw when window is undefined", () => {
|
|
247
|
+
const original = global.window;
|
|
248
|
+
// @ts-expect-error — intentionally removing window for SSR test
|
|
249
|
+
delete global.window;
|
|
250
|
+
expect(() => destroy()).not.toThrow();
|
|
251
|
+
global.window = original;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("allows re-initialisation after destroy", () => {
|
|
255
|
+
init({ project: "prj_abc" });
|
|
256
|
+
destroy();
|
|
257
|
+
init({ project: "prj_abc" });
|
|
258
|
+
const script = document.body.querySelector("script[data-beeport-script]");
|
|
259
|
+
expect(script).not.toBeNull();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// open()
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
describe("open()", () => {
|
|
268
|
+
it("dispatches a beeport:open custom event on window", () => {
|
|
269
|
+
const events: Event[] = [];
|
|
270
|
+
window.addEventListener("beeport:open", (e) => events.push(e));
|
|
271
|
+
|
|
272
|
+
open();
|
|
273
|
+
|
|
274
|
+
expect(events).toHaveLength(1);
|
|
275
|
+
window.removeEventListener("beeport:open", (e) => events.push(e));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("does not throw when window is undefined", () => {
|
|
279
|
+
const original = global.window;
|
|
280
|
+
// @ts-expect-error — intentionally removing window
|
|
281
|
+
delete global.window;
|
|
282
|
+
expect(() => open()).not.toThrow();
|
|
283
|
+
global.window = original;
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// close()
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
describe("close()", () => {
|
|
292
|
+
it("dispatches a beeport:close custom event on window", () => {
|
|
293
|
+
const events: Event[] = [];
|
|
294
|
+
window.addEventListener("beeport:close", (e) => events.push(e));
|
|
295
|
+
|
|
296
|
+
close();
|
|
297
|
+
|
|
298
|
+
expect(events).toHaveLength(1);
|
|
299
|
+
window.removeEventListener("beeport:close", (e) => events.push(e));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("does not throw when window is undefined", () => {
|
|
303
|
+
const original = global.window;
|
|
304
|
+
// @ts-expect-error — intentionally removing window
|
|
305
|
+
delete global.window;
|
|
306
|
+
expect(() => close()).not.toThrow();
|
|
307
|
+
global.window = original;
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// BeeportWidget namespace export (default export)
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
describe("BeeportWidget namespace (default export)", () => {
|
|
316
|
+
it("exports a BeeportWidget object with init method", () => {
|
|
317
|
+
expect(typeof BeeportWidget).toBe("object");
|
|
318
|
+
expect(typeof BeeportWidget.init).toBe("function");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("BeeportWidget.init() injects a <script> tag into document.body", () => {
|
|
322
|
+
BeeportWidget.init({ project: "prj_ns_test" });
|
|
323
|
+
const script = document.body.querySelector<HTMLScriptElement>(
|
|
324
|
+
"script[data-beeport-script]"
|
|
325
|
+
);
|
|
326
|
+
expect(script).not.toBeNull();
|
|
327
|
+
expect(script?.getAttribute("data-project")).toBe("prj_ns_test");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("BeeportWidget.destroy() removes widget elements", () => {
|
|
331
|
+
BeeportWidget.init({ project: "prj_ns_test" });
|
|
332
|
+
BeeportWidget.destroy();
|
|
333
|
+
const script = document.body.querySelector("script[data-beeport-script]");
|
|
334
|
+
expect(script).toBeNull();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("BeeportWidget.open() dispatches beeport:open event", () => {
|
|
338
|
+
const events: Event[] = [];
|
|
339
|
+
window.addEventListener("beeport:open", (e) => events.push(e));
|
|
340
|
+
BeeportWidget.open();
|
|
341
|
+
expect(events).toHaveLength(1);
|
|
342
|
+
window.removeEventListener("beeport:open", (e) => events.push(e));
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("BeeportWidget.close() dispatches beeport:close event", () => {
|
|
346
|
+
const events: Event[] = [];
|
|
347
|
+
window.addEventListener("beeport:close", (e) => events.push(e));
|
|
348
|
+
BeeportWidget.close();
|
|
349
|
+
expect(events).toHaveLength(1);
|
|
350
|
+
window.removeEventListener("beeport:close", (e) => events.push(e));
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("BeeportWidget.init() is identical to named init()", () => {
|
|
354
|
+
// Both should be the same function reference
|
|
355
|
+
expect(BeeportWidget.init).toBe(init);
|
|
356
|
+
expect(BeeportWidget.destroy).toBe(destroy);
|
|
357
|
+
expect(BeeportWidget.open).toBe(open);
|
|
358
|
+
expect(BeeportWidget.close).toBe(close);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// TypeScript type validation (compile-time only — these just need to compile)
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
describe("TypeScript types", () => {
|
|
367
|
+
it("BeePortConfig requires project field", () => {
|
|
368
|
+
// This is a type-level test — just verifying config shape at runtime
|
|
369
|
+
const config: BeePortConfig = { project: "prj_required" };
|
|
370
|
+
expect(config.project).toBe("prj_required");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("BeePortConfig theme accepts light | dark | auto", () => {
|
|
374
|
+
const light: BeePortConfig = { project: "p", theme: "light" };
|
|
375
|
+
const dark: BeePortConfig = { project: "p", theme: "dark" };
|
|
376
|
+
const auto: BeePortConfig = { project: "p", theme: "auto" };
|
|
377
|
+
expect(light.theme).toBe("light");
|
|
378
|
+
expect(dark.theme).toBe("dark");
|
|
379
|
+
expect(auto.theme).toBe("auto");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("BeePortConfig position accepts bottom-right | bottom-left", () => {
|
|
383
|
+
const right: BeePortConfig = { project: "p", position: "bottom-right" };
|
|
384
|
+
const left: BeePortConfig = { project: "p", position: "bottom-left" };
|
|
385
|
+
expect(right.position).toBe("bottom-right");
|
|
386
|
+
expect(left.position).toBe("bottom-left");
|
|
387
|
+
});
|
|
388
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"noImplicitOverride": true,
|
|
10
|
+
"exactOptionalPropertyTypes": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"outDir": "dist",
|
|
14
|
+
"rootDir": ".",
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationDir": "dist",
|
|
17
|
+
"types": ["node"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*", "test/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist"]
|
|
21
|
+
}
|
package/vitest.config.ts
ADDED