tracker: introduce singleton approach for tracker
This commit is contained in:
parent
2ee535f213
commit
1ab7d0ad7f
6 changed files with 476 additions and 33 deletions
|
|
@ -15,13 +15,18 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
|
"require": "./dist/cjs/entry.js",
|
||||||
|
"import": "./dist/lib/entry.js",
|
||||||
|
"types": "./dist/lib/main/entry.d.ts"
|
||||||
|
},
|
||||||
|
"./class": {
|
||||||
"require": "./dist/cjs/index.js",
|
"require": "./dist/cjs/index.js",
|
||||||
"import": "./dist/lib/index.js",
|
"import": "./dist/lib/index.js",
|
||||||
"types": "./dist/lib/main/index.d.ts"
|
"types": "./dist/lib/main/index.d.ts"
|
||||||
},
|
},
|
||||||
"./cjs": {
|
"./cjs": {
|
||||||
"require": "./dist/cjs/index.js",
|
"require": "./dist/cjs/entry.js",
|
||||||
"types": "./dist/cjs/main/index.d.ts"
|
"types": "./dist/cjs/main/entry.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
@ -29,9 +34,9 @@
|
||||||
"dist/cjs/**/*",
|
"dist/cjs/**/*",
|
||||||
"dist/types/**/*"
|
"dist/types/**/*"
|
||||||
],
|
],
|
||||||
"main": "./dist/cjs/index.js",
|
"main": "./dist/cjs/entry.js",
|
||||||
"module": "./dist/lib/index.js",
|
"module": "./dist/lib/entry.js",
|
||||||
"types": "./dist/lib/main/index.d.ts",
|
"types": "./dist/lib/main/entry.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint src --ext .ts,.js --fix --quiet",
|
"lint": "eslint src --ext .ts,.js --fix --quiet",
|
||||||
"clean": "rm -Rf build && rm -Rf dist",
|
"clean": "rm -Rf build && rm -Rf dist",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import typescript from '@rollup/plugin-typescript'
|
||||||
import terser from '@rollup/plugin-terser'
|
import terser from '@rollup/plugin-terser'
|
||||||
import replace from '@rollup/plugin-replace'
|
import replace from '@rollup/plugin-replace'
|
||||||
import { rollup } from 'rollup'
|
import { rollup } from 'rollup'
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs'
|
||||||
import { createRequire } from 'module'
|
import { createRequire } from 'module'
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
const packageConfig = require('./package.json')
|
const packageConfig = require('./package.json')
|
||||||
|
|
@ -21,33 +21,32 @@ export default async () => {
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
return [
|
|
||||||
{
|
const entryPoints = ['build/main/index.js', 'build/main/entry.js']
|
||||||
input: 'build/main/index.js',
|
|
||||||
output: {
|
const esmBuilds = entryPoints.map((input) => ({
|
||||||
dir: 'dist/lib',
|
input,
|
||||||
format: 'es',
|
output: {
|
||||||
sourcemap: true,
|
dir: 'dist/lib',
|
||||||
entryFileNames: '[name].js',
|
format: 'es',
|
||||||
},
|
sourcemap: true,
|
||||||
plugins: [
|
entryFileNames: '[name].js',
|
||||||
...commonPlugins,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
plugins: [...commonPlugins],
|
||||||
input: 'build/main/index.js',
|
}))
|
||||||
output: {
|
|
||||||
dir: 'dist/cjs',
|
const cjsBuilds = entryPoints.map((input) => ({
|
||||||
format: 'cjs',
|
input,
|
||||||
sourcemap: true,
|
output: {
|
||||||
entryFileNames: '[name].js',
|
dir: 'dist/cjs',
|
||||||
},
|
format: 'cjs',
|
||||||
plugins: [
|
sourcemap: true,
|
||||||
...commonPlugins,
|
entryFileNames: '[name].js',
|
||||||
commonjs(),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
plugins: [...commonPlugins, commonjs()],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return [...esmBuilds, ...cjsBuilds]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildWebWorker() {
|
async function buildWebWorker() {
|
||||||
|
|
|
||||||
5
tracker/tracker/src/main/entry.ts
Normal file
5
tracker/tracker/src/main/entry.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import TrackerClass from './index.js'
|
||||||
|
|
||||||
|
export { SanitizeLevel, Messages, Options } from './index.js'
|
||||||
|
export { default as tracker } from './singleton.js'
|
||||||
|
export default TrackerClass
|
||||||
|
|
@ -109,7 +109,7 @@ export default class API {
|
||||||
private readonly app: App | null = null
|
private readonly app: App | null = null
|
||||||
private readonly crossdomainMode: boolean = false
|
private readonly crossdomainMode: boolean = false
|
||||||
|
|
||||||
constructor(private readonly options: Options) {
|
constructor(public readonly options: Partial<Options>) {
|
||||||
this.crossdomainMode = Boolean(inIframe() && options.crossdomain?.enabled)
|
this.crossdomainMode = Boolean(inIframe() && options.crossdomain?.enabled)
|
||||||
if (!IN_BROWSER || !processOptions(options)) {
|
if (!IN_BROWSER || !processOptions(options)) {
|
||||||
return
|
return
|
||||||
|
|
@ -287,7 +287,7 @@ export default class API {
|
||||||
this.app.restartCanvasTracking()
|
this.app.restartCanvasTracking()
|
||||||
}
|
}
|
||||||
|
|
||||||
use<T>(fn: (app: App | null, options?: Options) => T): T {
|
use<T>(fn: (app: App | null, options?: Partial<Options>) => T): T {
|
||||||
return fn(this.app, this.options)
|
return fn(this.app, this.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
352
tracker/tracker/src/main/singleton.ts
Normal file
352
tracker/tracker/src/main/singleton.ts
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
import Tracker, { App, Options } from './index.js'
|
||||||
|
import { IN_BROWSER } from './utils.js'
|
||||||
|
import type { StartOptions, StartPromiseReturn } from './app/index.js'
|
||||||
|
|
||||||
|
class TrackerSingleton {
|
||||||
|
private instance: Tracker | null = null
|
||||||
|
private isConfigured = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this method once to create tracker configuration
|
||||||
|
* @param options {Object} Check available options:
|
||||||
|
* https://docs.openreplay.com/en/sdk/constructor/#initialization-options
|
||||||
|
*/
|
||||||
|
configure(options: Partial<Options>): void {
|
||||||
|
if (!IN_BROWSER) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isConfigured) {
|
||||||
|
console.warn(
|
||||||
|
'OpenReplay: Tracker is already configured. You should only call configure once.',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.projectKey) {
|
||||||
|
console.error('OpenReplay: Missing required projectKey option')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance = new Tracker(options)
|
||||||
|
this.isConfigured = true
|
||||||
|
}
|
||||||
|
|
||||||
|
get options(): Partial<Options> | null {
|
||||||
|
return this.instance?.options || null
|
||||||
|
}
|
||||||
|
|
||||||
|
start(startOpts?: Partial<StartOptions>): Promise<StartPromiseReturn> {
|
||||||
|
if (!IN_BROWSER) {
|
||||||
|
return Promise.resolve({ success: false, reason: 'Not in browser environment' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.ensureConfigured()) {
|
||||||
|
return Promise.resolve({ success: false, reason: 'Tracker not configured' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.instance?.start(startOpts) ||
|
||||||
|
Promise.resolve({ success: false, reason: 'Tracker not initialized' })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the session and return sessionHash
|
||||||
|
* (which can be used to stitch sessions together)
|
||||||
|
* */
|
||||||
|
stop(): string | undefined {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserID(id: string): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.setUserID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set metadata for the current session
|
||||||
|
*
|
||||||
|
* Make sure that its configured in project settings first
|
||||||
|
*
|
||||||
|
* Read more: https://docs.openreplay.com/en/installation/metadata/
|
||||||
|
*/
|
||||||
|
setMetadata(key: string, value: string): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.setMetadata(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns full URL for the current session
|
||||||
|
*/
|
||||||
|
getSessionURL(options?: { withCurrentTime?: boolean }): string | undefined {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.getSessionURL(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionID(): string | null | undefined {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.getSessionID()
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionToken(): string | null | undefined {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.getSessionToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
event(key: string, payload: any = null, issue = false): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.event(key, payload, issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
issue(key: string, payload: any = null): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.issue(key, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(
|
||||||
|
e: Error | ErrorEvent | PromiseRejectionEvent,
|
||||||
|
metadata: Record<string, any> = {},
|
||||||
|
): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.handleError(e, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
isFlagEnabled(flagName: string): boolean {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.isFlagEnabled(flagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
onFlagsLoad(...args: Parameters<Tracker['onFlagsLoad']>): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.onFlagsLoad(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPersistFlags(): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.clearPersistFlags()
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadFlags(): Promise<void> | undefined {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.reloadFlags()
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeatureFlag(flagName: string) {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.getFeatureFlag(flagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllFeatureFlags() {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.getAllFeatureFlags()
|
||||||
|
}
|
||||||
|
|
||||||
|
restartCanvasTracking(): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.restartCanvasTracking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the anonymous user ID
|
||||||
|
*/
|
||||||
|
setUserAnonymousID(id: string): void {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.instance.setUserAnonymousID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the tracker is active
|
||||||
|
*/
|
||||||
|
isActive(): boolean {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.isActive()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying Tracker instance
|
||||||
|
*
|
||||||
|
* Use when you need access to methods not exposed by the singleton
|
||||||
|
*/
|
||||||
|
getInstance(): Tracker | null {
|
||||||
|
if (!this.ensureConfigured() || !IN_BROWSER) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start buffering messages without starting the actual session, which gives user 30 seconds to "activate" and record
|
||||||
|
* session by calling start() on conditional trigger and we will then send buffered batch, so it won't get lost
|
||||||
|
* */
|
||||||
|
coldStart(startOpts?: Partial<StartOptions>, conditional?: boolean) {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.coldStart(startOpts, conditional)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a named hook that expects event name, data string and msg direction (up/down),
|
||||||
|
* it will skip any message bigger than 5 mb or event name bigger than 255 symbols
|
||||||
|
* msg direction is "down" (incoming) by default
|
||||||
|
*
|
||||||
|
* @returns {(msgType: string, data: string, dir: 'up' | 'down') => void}
|
||||||
|
* */
|
||||||
|
trackWs(
|
||||||
|
channelName: string,
|
||||||
|
): ((msgType: string, data: string, dir: 'up' | 'down') => void) | undefined {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return () => {} // Return no-op function
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.trackWs(channelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureConfigured() {
|
||||||
|
if (!this.isConfigured && IN_BROWSER) {
|
||||||
|
console.warn(
|
||||||
|
'OpenReplay: Tracker must be configured before use. Call tracker.configure({projectKey: "your-project-key"}) first.',
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
use<T>(fn: (app: App | null, options?: Partial<Options>) => T): T {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return fn(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.use(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts offline session recording. Keep in mind that only user device time will be used for timestamps.
|
||||||
|
* (no backend delay sync)
|
||||||
|
*
|
||||||
|
* @param {Object} startOpts - options for session start, same as .start()
|
||||||
|
* @param {Function} onSessionSent - callback that will be called once session is fully sent
|
||||||
|
* @returns methods to manipulate buffer:
|
||||||
|
*
|
||||||
|
* saveBuffer - to save it in localStorage
|
||||||
|
*
|
||||||
|
* getBuffer - returns current buffer
|
||||||
|
*
|
||||||
|
* setBuffer - replaces current buffer with given
|
||||||
|
* */
|
||||||
|
startOfflineRecording(...args: Parameters<Tracker['startOfflineRecording']>) {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.startOfflineRecording(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the stored session buffer to backend
|
||||||
|
* @returns promise that resolves once messages are loaded, it has to be awaited
|
||||||
|
* so the session can be uploaded properly
|
||||||
|
* @resolve - if messages were loaded into service worker successfully
|
||||||
|
* @reject {string} - error message
|
||||||
|
* */
|
||||||
|
uploadOfflineRecording() {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.uploadOfflineRecording()
|
||||||
|
}
|
||||||
|
|
||||||
|
forceFlushBatch() {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.forceFlushBatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionInfo() {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.getSessionInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
getTabId() {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.getTabId()
|
||||||
|
}
|
||||||
|
|
||||||
|
getUxId() {
|
||||||
|
if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.getUxId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracker = new TrackerSingleton()
|
||||||
|
|
||||||
|
export default tracker
|
||||||
82
tracker/tracker/src/tests/singleton.test.ts
Normal file
82
tracker/tracker/src/tests/singleton.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { describe, expect, test, jest, beforeAll, afterAll } from '@jest/globals'
|
||||||
|
import singleton from "../main/singleton";
|
||||||
|
|
||||||
|
jest.mock('@medv/finder', () => ({ default: jest.fn(() => 'mocked network-proxy content') }));
|
||||||
|
jest.mock('@openreplay/network-proxy', () => ({ default: jest.fn(() => 'mocked network-proxy content') }));
|
||||||
|
|
||||||
|
const methods = [
|
||||||
|
'onFlagsLoad',
|
||||||
|
'isFlagEnabled',
|
||||||
|
'clearPersistFlags',
|
||||||
|
'reloadFlags',
|
||||||
|
'getFeatureFlag',
|
||||||
|
'getAllFeatureFlags',
|
||||||
|
'restartCanvasTracking',
|
||||||
|
'use',
|
||||||
|
'isActive',
|
||||||
|
'trackWs',
|
||||||
|
'start',
|
||||||
|
'coldStart',
|
||||||
|
'startOfflineRecording',
|
||||||
|
'uploadOfflineRecording',
|
||||||
|
'stop',
|
||||||
|
'forceFlushBatch',
|
||||||
|
'getSessionToken',
|
||||||
|
'getSessionInfo',
|
||||||
|
'getSessionID',
|
||||||
|
'getTabId',
|
||||||
|
'getUxId',
|
||||||
|
'getSessionURL',
|
||||||
|
'setUserID',
|
||||||
|
'setUserAnonymousID',
|
||||||
|
'setMetadata',
|
||||||
|
'event',
|
||||||
|
'issue',
|
||||||
|
'handleError',
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('Singleton Testing', () => {
|
||||||
|
const options = {
|
||||||
|
projectKey: 'test-project-key',
|
||||||
|
ingestPoint: 'test-ingest-point',
|
||||||
|
respectDoNotTrack: false,
|
||||||
|
__DISABLE_SECURE_MODE: true
|
||||||
|
};
|
||||||
|
beforeAll(() => {
|
||||||
|
// Mock the performance object and its timing property
|
||||||
|
Object.defineProperty(window, 'performance', {
|
||||||
|
value: {
|
||||||
|
timing: {},
|
||||||
|
now: jest.fn(() => 1000), // Mock performance.now() if needed
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(window, 'Worker', {
|
||||||
|
value: jest.fn(() => 'mocked worker content')
|
||||||
|
})
|
||||||
|
global.IntersectionObserver = jest.fn(() => ({
|
||||||
|
observe: jest.fn(),
|
||||||
|
unobserve: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
root: null,
|
||||||
|
rootMargin: '0px',
|
||||||
|
thresholds: [0],
|
||||||
|
takeRecords: jest.fn(() => []),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// Clean up the mock after tests if needed
|
||||||
|
delete window.performance;
|
||||||
|
delete window.Worker;
|
||||||
|
delete global.IntersectionObserver;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Singleton methods are compatible with Class', () => {
|
||||||
|
singleton.configure(options);
|
||||||
|
|
||||||
|
methods.forEach(method => {
|
||||||
|
console.log(method);
|
||||||
|
expect(singleton[method]).toBeDefined();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Reference in a new issue