@jam.dev/recording-links 0.1.3 → 0.2.0

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.
@@ -0,0 +1,79 @@
1
+ import { vi } from "vitest";
2
+ /**
3
+ * Shared browser mocking utilities for consistent test environment setup.
4
+ * This consolidates duplicate browser mock implementations across test files.
5
+ */
6
+ export interface MockLocalStorage {
7
+ getItem: ReturnType<typeof vi.fn>;
8
+ setItem: ReturnType<typeof vi.fn>;
9
+ removeItem: ReturnType<typeof vi.fn>;
10
+ clear: ReturnType<typeof vi.fn>;
11
+ length: number;
12
+ key: ReturnType<typeof vi.fn>;
13
+ _store: Record<string, string>;
14
+ }
15
+ export declare const createMockLocalStorage: () => MockLocalStorage;
16
+ export interface MockHistory {
17
+ pushState: ReturnType<typeof vi.fn>;
18
+ replaceState: ReturnType<typeof vi.fn>;
19
+ }
20
+ export declare const createMockHistory: () => MockHistory;
21
+ export interface MockWindow {
22
+ localStorage: MockLocalStorage;
23
+ location: {
24
+ href: string;
25
+ };
26
+ addEventListener: ReturnType<typeof vi.fn>;
27
+ removeEventListener: ReturnType<typeof vi.fn>;
28
+ history: MockHistory;
29
+ }
30
+ export declare const createMockWindow: (initialUrl?: string) => MockWindow;
31
+ export declare class MockPopStateEvent {
32
+ type: string;
33
+ constructor(type: string, options?: PopStateEventInit);
34
+ state: any;
35
+ }
36
+ export declare class MockCustomEvent {
37
+ type: string;
38
+ options: {
39
+ detail?: any;
40
+ };
41
+ constructor(type: string, options?: {
42
+ detail?: any;
43
+ });
44
+ detail: any;
45
+ }
46
+ export declare class MockEventTarget {
47
+ private listeners;
48
+ addEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions): void;
49
+ removeEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions): void;
50
+ dispatchEvent(event: CustomEvent): boolean;
51
+ }
52
+ /**
53
+ * Complete browser environment mock setup
54
+ */
55
+ export interface BrowserMocks {
56
+ localStorage: MockLocalStorage;
57
+ window: MockWindow;
58
+ history: MockHistory;
59
+ resetMocks: () => void;
60
+ }
61
+ export declare const createBrowserMocks: (initialUrl?: string) => BrowserMocks;
62
+ /**
63
+ * Helper to set test URL with parameters
64
+ */
65
+ export declare const setTestUrl: (params?: Record<string, string>, baseUrl?: string) => string;
66
+ /**
67
+ * Helper to create mock RefCounter for tests
68
+ */
69
+ export declare const createMockRefCounter: (count?: number) => {
70
+ count: number;
71
+ update: import("vitest").Mock<(...args: any[]) => any>;
72
+ addEventListener: import("vitest").Mock<(...args: any[]) => any>;
73
+ removeEventListener: import("vitest").Mock<(...args: any[]) => any>;
74
+ };
75
+ /**
76
+ * Browser environment setup helper that combines environment reset with mock creation
77
+ */
78
+ export declare const setupBrowserEnvironment: (initialUrl?: string) => MockHistory;
79
+ //# sourceMappingURL=browser-mocks.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=setup.d.ts.map
package/lib/sdk.d.ts CHANGED
@@ -10,7 +10,7 @@ type Events = {
10
10
  name: "recorder";
11
11
  Recorder: RecorderSingleton;
12
12
  } | {
13
- name: Exclude<ScriptName, "recorder">;
13
+ name: "capture";
14
14
  });
15
15
  };
16
16
  /**
@@ -64,6 +64,8 @@ type InitializeOptions = {
64
64
  * Setting a custom `parseJamData` disables this default.
65
65
  */
66
66
  applyJamData?(data: SerializableJamData): string;
67
+ /** Selectors to blur on host pages while the Jam Recorder is recording. */
68
+ blurSelectors?: string | string[] | (() => string | string[]);
67
69
  };
68
70
  /**
69
71
  * Data structure representing Jam recording metadata that can be serialized to/from URLs.
@@ -74,6 +76,9 @@ export type SerializableJamData = {
74
76
  /** The human-readable title of the recording, or null if not present */
75
77
  jamTitle: string | null;
76
78
  };
79
+ declare function resetForTesting(): void;
80
+ /** @internal - Reset SDK state for testing (only available in dev builds) */
81
+ export declare const _resetForTesting: typeof resetForTesting | undefined;
77
82
  /**
78
83
  * Add an event listener for SDK events.
79
84
  * @param type - The event type to listen for
@@ -123,7 +128,7 @@ export declare let Recorder: RecorderSingleton | null;
123
128
  * });
124
129
  * ```
125
130
  */
126
- export declare function initialize({ recorderRefCounter, ...config }?: InitializeOptions): void;
131
+ export declare function initialize({ recorderRefCounter, _loadRemoteScript, ...config }?: InitializeOptions): void;
127
132
  /**
128
133
  * Loads the Jam recorder module and returns the recorder singleton.
129
134
  *
package/lib/sdk.js CHANGED
@@ -1,5 +1,5 @@
1
- function e(){const e=new EventTarget;return{addEventListener(t,r,n){e.addEventListener(t,r,n)},removeEventListener(t,r,n){e.removeEventListener(t,r,n)},dispatch:(t,r)=>e.dispatchEvent(new CustomEvent(t,{detail:r}))}}const t=t=>{const r=`jam:${t}`,n=e();let i=Number.parseInt(localStorage.getItem(r)??"0",10)||0;return window.addEventListener("storage",e=>{if(e.storageArea===localStorage&&e.key===r){const t=Number.parseInt(e.newValue??"",10);Number.isNaN(t)||t===i||(i=t,n.dispatch("update",i))}}),{count:i,addEventListener:n.addEventListener.bind(n),removeEventListener:n.removeEventListener.bind(n),update(e){const t=i;switch(e){case"increment":i+=1;break;case"decrement":i-=1;break;default:i=e}return i<0&&(i=0),i!==t&&(localStorage.setItem(r,`${i}`),n.dispatch("update",i)),i}}},r=e=>{try{const t=new URL(e);return{recordingId:t.searchParams.get("jam-recording"),jamTitle:t.searchParams.get("jam-title")}}catch(e){}return null},n={isInitialized:!1},i=e(),a=i.addEventListener.bind(i),o=i.removeEventListener.bind(i),d=()=>n.isInitialized;let c=null;function s({recorderRefCounter:e=t("numRecorders"),...i}={}){if(n.isInitialized)throw new Error("SDK already initialized.");Object.assign(n,{isInitialized:!0,recorderRefCounter:e,config:i}),e.count>0?p("capture"):e.addEventListener("update",()=>p("capture"),{once:!0}),window.addEventListener("popstate",o);const a={apply(e,t,r){const n=Reflect.apply(e,t,r);return o(),n}};function o(){if(c)return;const e=n.config?.parseJamData??r,t="string"==typeof n.config?.openImmediately?n.config.openImmediately:e(window.location.href)?.recordingId;t&&l({openImmediately:!1!==n.config?.openImmediately&&t})}history.pushState=new Proxy(history.pushState,a),history.replaceState=new Proxy(history.replaceState,a),o()}async function l({...e}={}){if(c)return c;if(!n.isInitialized)throw new Error("SDK not initialized. Call initialize() first.");if(({Recorder:c}=await p("recorder")),!c)throw new Error("Failed to load recorder script.");const{openImmediately:t,...r}={...n.config,...e},i="string"==typeof t?t:null;return c.initialize({...r,openImmediately:!i&&(t??!0)}),n.recorderRefCounter.update("increment"),window.addEventListener("pagehide",()=>{n.recorderRefCounter.update("decrement")}),i&&c.open(i,r),c}async function p(e){const t=`https://js.jam.dev/${e}.js`,r=await import(
1
+ function e(){const e=new EventTarget;return{addEventListener(t,n,r){e.addEventListener(t,n,r)},removeEventListener(t,n,r){e.removeEventListener(t,n,r)},dispatch:(t,n)=>e.dispatchEvent(new CustomEvent(t,{detail:n}))}}const t=(t,n=localStorage)=>{const r=`jam:${t}`,i=e();let o=null;try{o=n.getItem(r)}catch(e){}const a={count:Number.parseInt(o??"0",10)||0,addEventListener:i.addEventListener.bind(i),removeEventListener:i.removeEventListener.bind(i),update(e){const t=a.count;switch(e){case"increment":a.count+=1;break;case"decrement":a.count-=1;break;default:a.count=e}if(a.count<0&&(a.count=0),a.count!==t){try{n.setItem(r,`${a.count}`)}catch(e){}Object.assign(a,{count:a.count}),i.dispatch("update",a.count)}return a.count}};return window.addEventListener("storage",e=>{if(e.storageArea===n&&e.key===r){const t=Number.parseInt(e.newValue??"",10);Number.isNaN(t)||t===a.count||(a.count=t,i.dispatch("update",a.count))}}),a},n=e=>{try{const t=new URL(e);return{recordingId:t.searchParams.get("jam-recording"),jamTitle:t.searchParams.get("jam-title")}}catch(e){}return null},r={isInitialized:!1},i=void 0,o=e(),a=o.addEventListener.bind(o),c=o.removeEventListener.bind(o),d=()=>r.isInitialized;let s=null;function u({recorderRefCounter:e=t("numRecorders"),_loadRemoteScript:i=m,...o}={}){if(r.isInitialized)throw new Error("SDK already initialized.");Object.assign(r,{isInitialized:!0,recorderRefCounter:e,config:o,_loadRemoteScript:i}),e.count>0?p():e.addEventListener("update",p,{once:!0}),window.addEventListener("popstate",c);const a={apply(e,t,n){const r=Reflect.apply(e,t,n);return c(),r}};function c(){if(s)return;const e=r.config?.parseJamData??n,t="string"==typeof r.config?.openImmediately?r.config.openImmediately:e(window.location.href)?.recordingId;t&&l({openImmediately:!1!==r.config?.openImmediately&&t})}history.pushState=new Proxy(history.pushState,a),history.replaceState=new Proxy(history.replaceState,a),c()}async function l({...e}={}){if(s)return s;if(!r.isInitialized)throw new Error("SDK not initialized. Call initialize() first.");if(({Recorder:s}=await r._loadRemoteScript("recorder")),!s)throw new Error("Failed to load recorder script.");const{openImmediately:t,...n}={...r.config,...e},i="string"==typeof t?t:null;return s.initialize({...n,openImmediately:!i&&(t??!0)}),r.recorderRefCounter.update("increment"),window.addEventListener("pagehide",()=>{r.recorderRefCounter.update("decrement")}),i&&s.open(i,n),s}async function p(){if(!r.isInitialized||!r._loadRemoteScript)throw new Error("SDK not initialized. Call initialize() first.");const{Capture:e}=await r._loadRemoteScript("capture")??{};await(e?.initialize(r.config))}async function m(e){const t=`https://js.jam.dev/${e}.js`,n=await import(
2
2
  /* webpackIgnore: true */
3
3
  /* @vite-ignore */
4
4
  /* @rollup/plugin-dynamic-import-vars ignore */
5
- t);if("recorder"===e){if(!r.Recorder)throw new Error("Loaded recorder script, but Recorder not found.");i.dispatch("loaded",{type:"script",name:e,Recorder:r.Recorder})}else i.dispatch("loaded",{type:"script",name:e});return r}export{c as Recorder,a as addEventListener,s as initialize,d as isInitialized,l as loadRecorder,o as removeEventListener};//# sourceMappingURL=sdk.js.map
5
+ t);if("recorder"===e){if(!n.Recorder)throw new Error("Loaded recorder script, but Recorder not found.");o.dispatch("loaded",{type:"script",name:e,Recorder:n.Recorder})}else o.dispatch("loaded",{type:"script",name:e});return n}export{s as Recorder,i as _resetForTesting,a as addEventListener,u as initialize,d as isInitialized,l as loadRecorder,c as removeEventListener};//# sourceMappingURL=sdk.js.map
@@ -5,5 +5,5 @@ export type RefCounter = Omit<events.Emitter<{
5
5
  count: number;
6
6
  update(value: "increment" | "decrement" | number): number;
7
7
  };
8
- export declare const createLocalStorageRefCounter: <K extends string>(name: K) => RefCounter;
8
+ export declare const createStorageRefCounter: <K extends string>(name: K, storage?: Storage) => RefCounter;
9
9
  //# sourceMappingURL=refs.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jam.dev/recording-links",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Capture bug reports from your users with the Jam recording links SDK",
5
5
  "keywords": [
6
6
  "jam",
@@ -43,11 +43,11 @@
43
43
  "dev:remote": "rollup -c --watch --environment BUILD:dev,JAM_JS_ORIGIN:prod",
44
44
  "dev:staging": "rollup -c --watch --environment BUILD:dev,JAM_JS_ORIGIN:staging",
45
45
  "build": "rollup -c --environment BUILD:prod",
46
- "prepublishOnly": "bun run scripts/validate-build.ts",
47
46
  "build:dev": "rollup -c --environment BUILD:dev",
47
+ "prepublishOnly": "bun run scripts/validate-build.ts",
48
48
  "lint": "biome lint './src' --diagnostic-level=error",
49
49
  "lint:fix": "biome lint './src' --diagnostic-level=error --fix",
50
- "test": "vitest --passWithNoTests",
50
+ "test": "vitest --passWithNoTests src/",
51
51
  "type-check": "tsc --noEmit"
52
52
  },
53
53
  "dependencies": {},
@@ -58,6 +58,7 @@
58
58
  "@rollup/plugin-typescript": "^11.1.6",
59
59
  "@types/node": "^20.0.0",
60
60
  "biome": "^0.3.3",
61
+ "jsdom": "^27.0.0",
61
62
  "rollup": "^4.12.0",
62
63
  "tslib": "^2.8.1",
63
64
  "typescript": "^5.8.2",