@dotcms/experiments 0.0.1-alpha.38 → 0.0.1-alpha.39

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 (48) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +26 -0
  3. package/jest.config.ts +11 -0
  4. package/package.json +4 -8
  5. package/project.json +55 -0
  6. package/src/lib/components/{DotExperimentHandlingComponent.d.ts → DotExperimentHandlingComponent.tsx} +20 -3
  7. package/src/lib/components/DotExperimentsProvider.spec.tsx +62 -0
  8. package/src/lib/components/{DotExperimentsProvider.d.ts → DotExperimentsProvider.tsx} +41 -3
  9. package/src/lib/components/withExperiments.tsx +52 -0
  10. package/src/lib/contexts/DotExperimentsContext.spec.tsx +42 -0
  11. package/src/lib/contexts/{DotExperimentsContext.d.ts → DotExperimentsContext.tsx} +5 -2
  12. package/src/lib/dot-experiments.spec.ts +285 -0
  13. package/src/lib/dot-experiments.ts +716 -0
  14. package/src/lib/hooks/useExperimentVariant.spec.tsx +111 -0
  15. package/src/lib/hooks/useExperimentVariant.ts +55 -0
  16. package/src/lib/hooks/useExperiments.ts +90 -0
  17. package/src/lib/shared/{constants.d.ts → constants.ts} +35 -18
  18. package/src/lib/shared/mocks/mock.ts +209 -0
  19. package/src/lib/shared/{models.d.ts → models.ts} +35 -2
  20. package/src/lib/shared/parser/parse.spec.ts +187 -0
  21. package/src/lib/shared/parser/parser.ts +171 -0
  22. package/src/lib/shared/persistence/index-db-database-handler.spec.ts +100 -0
  23. package/src/lib/shared/persistence/index-db-database-handler.ts +218 -0
  24. package/src/lib/shared/utils/DotLogger.ts +57 -0
  25. package/src/lib/shared/utils/memoize.spec.ts +49 -0
  26. package/src/lib/shared/utils/memoize.ts +49 -0
  27. package/src/lib/shared/utils/utils.spec.ts +142 -0
  28. package/src/lib/shared/utils/utils.ts +203 -0
  29. package/src/lib/standalone.spec.ts +36 -0
  30. package/src/lib/standalone.ts +28 -0
  31. package/tsconfig.json +20 -0
  32. package/tsconfig.lib.json +20 -0
  33. package/tsconfig.spec.json +9 -0
  34. package/vite.config.ts +41 -0
  35. package/index.esm.d.ts +0 -1
  36. package/index.esm.js +0 -7174
  37. package/src/lib/components/withExperiments.d.ts +0 -20
  38. package/src/lib/dot-experiments.d.ts +0 -289
  39. package/src/lib/hooks/useExperimentVariant.d.ts +0 -21
  40. package/src/lib/hooks/useExperiments.d.ts +0 -14
  41. package/src/lib/shared/mocks/mock.d.ts +0 -43
  42. package/src/lib/shared/parser/parser.d.ts +0 -54
  43. package/src/lib/shared/persistence/index-db-database-handler.d.ts +0 -87
  44. package/src/lib/shared/utils/DotLogger.d.ts +0 -15
  45. package/src/lib/shared/utils/memoize.d.ts +0 -7
  46. package/src/lib/shared/utils/utils.d.ts +0 -73
  47. package/src/lib/standalone.d.ts +0 -7
  48. /package/src/{index.d.ts → index.ts} +0 -0
package/.babelrc ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "presets": [
3
+ [
4
+ "@nrwl/react/babel",
5
+ {
6
+ "runtime": "automatic",
7
+ "useBuiltIns": "usage"
8
+ }
9
+ ]
10
+ ],
11
+ "plugins": []
12
+ }
package/.eslintrc.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.base.json"],
3
+ "ignorePatterns": ["!**/*"],
4
+ "overrides": [
5
+ {
6
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7
+ "rules": {
8
+ "padding-line-between-statements": [
9
+ "error",
10
+ { "blankLine": "always", "prev": "const", "next": "*" },
11
+ { "blankLine": "always", "prev": "*", "next": "if" },
12
+ { "blankLine": "always", "prev": "if", "next": "*" },
13
+ { "blankLine": "always", "prev": "*", "next": "const" }
14
+ ]
15
+ }
16
+ },
17
+ {
18
+ "files": ["*.ts", "*.tsx"],
19
+ "rules": {}
20
+ },
21
+ {
22
+ "files": ["*.js", "*.jsx"],
23
+ "rules": {}
24
+ }
25
+ ]
26
+ }
package/jest.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ /* eslint-disable */
2
+ export default {
3
+ displayName: 'sdk-experiments',
4
+ preset: '../../../jest.preset.js',
5
+ transform: {
6
+ '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
7
+ '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/react/babel'] }]
8
+ },
9
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
10
+ coverageDirectory: '../../../coverage/libs/sdk/experiments'
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotcms/experiments",
3
- "version": "0.0.1-alpha.38",
3
+ "version": "0.0.1-alpha.39",
4
4
  "description": "Official JavaScript library to use Experiments with DotCMS.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,10 +25,6 @@
25
25
  "peerDependencies": {
26
26
  "react": ">=18",
27
27
  "react-dom": ">=18",
28
- "@dotcms/client": "0.0.1-alpha.38"
29
- },
30
- "module": "./index.esm.js",
31
- "type": "module",
32
- "main": "./index.esm.js",
33
- "types": "./index.esm.d.ts"
34
- }
28
+ "@dotcms/client": "0.0.1-alpha.39"
29
+ }
30
+ }
package/project.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "sdk-experiments",
3
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/sdk/experiments/src",
5
+ "projectType": "library",
6
+ "targets": {
7
+ "build": {
8
+ "executor": "@nrwl/rollup:rollup",
9
+ "outputs": ["{options.outputPath}"],
10
+ "options": {
11
+ "outputPath": "dist/libs/sdk/experiments",
12
+ "tsConfig": "libs/sdk/experiments/tsconfig.lib.json",
13
+ "project": "libs/sdk/experiments/package.json",
14
+ "entryFile": "libs/sdk/experiments/src/index.ts",
15
+ "external": ["react/jsx-runtime"],
16
+ "rollupConfig": "@nrwl/react/plugins/bundle-rollup",
17
+ "compiler": "babel",
18
+ "assets": [
19
+ {
20
+ "glob": "libs/sdk/experiments/README.md",
21
+ "input": ".",
22
+ "output": "."
23
+ }
24
+ ]
25
+ }
26
+ },
27
+ "build:iife": {
28
+ "executor": "@nx/vite:build",
29
+ "outputs": ["{options.outputPath}"],
30
+ "options": {
31
+ "outputPath": "dist/libs/sdk/experiments/iife",
32
+ "main": "libs/sdk/experiments/src/lib/standalone.ts",
33
+ "tsConfig": "libs/sdk/experiments/tsconfig.lib.json"
34
+ }
35
+ },
36
+ "publish": {
37
+ "command": "node tools/scripts/publish.mjs sdk-experiments {args.ver} {args.tag}",
38
+ "outputPath": "dist/libs/sdk/experiments",
39
+ "dependsOn": ["build", "test"]
40
+ },
41
+ "nx-release-publish": {
42
+ "options": {
43
+ "packageRoot": "dist/libs/sdk/experiments"
44
+ }
45
+ },
46
+ "test": {
47
+ "executor": "@nx/jest:jest",
48
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
49
+ "options": {
50
+ "jestConfig": "libs/sdk/experiments/jest.config.ts"
51
+ }
52
+ }
53
+ },
54
+ "tags": []
55
+ }
@@ -1,8 +1,11 @@
1
- /// <reference types="react" />
2
1
  import { DotcmsPageProps } from '@dotcms/react';
2
+
3
+ import { useExperimentVariant } from '../hooks/useExperimentVariant';
4
+
3
5
  interface ExperimentHandlingProps extends DotcmsPageProps {
4
6
  WrappedComponent: React.ComponentType<DotcmsPageProps>;
5
7
  }
8
+
6
9
  /**
7
10
  * A React functional component that conditionally renders a WrappedComponent based on the
8
11
  * experiment variant state. It uses the `useExperimentVariant` hook to determine if there's a
@@ -17,5 +20,19 @@ interface ExperimentHandlingProps extends DotcmsPageProps {
17
20
  * @returns {React.ReactElement} A React element that either renders the WrappedComponent hidden or visible
18
21
  * based on the experiment variant.
19
22
  */
20
- export declare const DotExperimentHandlingComponent: React.FC<ExperimentHandlingProps>;
21
- export {};
23
+ export const DotExperimentHandlingComponent: React.FC<ExperimentHandlingProps> = ({
24
+ WrappedComponent,
25
+ ...props
26
+ }) => {
27
+ const { shouldWaitForVariant } = useExperimentVariant(props.pageContext.pageAsset);
28
+
29
+ if (shouldWaitForVariant) {
30
+ return (
31
+ <div style={{ visibility: 'hidden' }}>
32
+ <WrappedComponent {...props} />
33
+ </div>
34
+ );
35
+ }
36
+
37
+ return <WrappedComponent {...props} />;
38
+ };
@@ -0,0 +1,62 @@
1
+ import { render, waitFor } from '@testing-library/react';
2
+
3
+ import * as dotcmsClient from '@dotcms/client';
4
+
5
+ import { DotExperimentsProvider } from './DotExperimentsProvider';
6
+
7
+ import { DotExperiments } from '../dot-experiments';
8
+
9
+ jest.mock('../dot-experiments');
10
+ jest.mock('@dotcms/client');
11
+
12
+ const mockDotExperimentsInstance = {
13
+ getInstance: jest.fn().mockResolvedValue(true),
14
+ ready: jest.fn().mockResolvedValue(true),
15
+ locationChanged: jest.fn().mockResolvedValue(true)
16
+ };
17
+
18
+ describe('DotExperimentsProvider', () => {
19
+ beforeEach(() => {
20
+ DotExperiments.getInstance = jest.fn().mockReturnValue(mockDotExperimentsInstance);
21
+ });
22
+
23
+ it('initializes DotExperiments instance when not inside the editor', async () => {
24
+ const config = { apiKey: 'key', server: 'server', debug: true };
25
+
26
+ jest.spyOn(dotcmsClient, 'isInsideEditor').mockReturnValue(true);
27
+
28
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
29
+
30
+ render(
31
+ <DotExperimentsProvider config={config}>
32
+ <div>Test</div>
33
+ </DotExperimentsProvider>
34
+ );
35
+
36
+ await waitFor(() => expect(DotExperiments.getInstance).not.toHaveBeenCalled());
37
+
38
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
39
+ 'DotExperimentsProvider: DotExperiments instance not initialized because it is inside the editor.'
40
+ );
41
+
42
+ consoleWarnSpy.mockRestore();
43
+ });
44
+
45
+ it('initializes DotExperiments instance when is inside the editor', async () => {
46
+ const config = { apiKey: 'key', server: 'server', debug: true };
47
+
48
+ jest.spyOn(dotcmsClient, 'isInsideEditor').mockReturnValue(false);
49
+
50
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
51
+
52
+ render(
53
+ <DotExperimentsProvider config={config}>
54
+ <div>Test</div>
55
+ </DotExperimentsProvider>
56
+ );
57
+
58
+ await waitFor(() => expect(DotExperiments.getInstance).toHaveBeenCalled());
59
+
60
+ consoleWarnSpy.mockRestore();
61
+ });
62
+ });
@@ -1,9 +1,17 @@
1
- import { ReactElement, ReactNode } from 'react';
1
+ import { ReactElement, ReactNode, useEffect, useState } from 'react';
2
+
3
+ import { isInsideEditor } from '@dotcms/client';
4
+
5
+ import DotExperimentsContext from '../contexts/DotExperimentsContext';
6
+ import { DotExperiments } from '../dot-experiments';
7
+ import { useExperiments } from '../hooks/useExperiments';
2
8
  import { DotExperimentConfig } from '../shared/models';
9
+
3
10
  interface DotExperimentsProviderProps {
4
11
  children?: ReactNode;
5
12
  config: DotExperimentConfig;
6
13
  }
14
+
7
15
  /**
8
16
  * `DotExperimentsProvider` is a component that uses React's Context API to provide
9
17
  * an instance of `DotExperiments` to all of its descendants.
@@ -43,5 +51,35 @@ interface DotExperimentsProviderProps {
43
51
  * @returns {ReactElement} The provider component, which should wrap the components
44
52
  * that need access to the `DotExperiments` instance.
45
53
  */
46
- export declare const DotExperimentsProvider: ({ children, config }: DotExperimentsProviderProps) => ReactElement;
47
- export {};
54
+ export const DotExperimentsProvider = ({
55
+ children,
56
+ config
57
+ }: DotExperimentsProviderProps): ReactElement => {
58
+ const [instance, setInstance] = useState<DotExperiments | null>(null);
59
+
60
+ // Run Experiments detection
61
+ useExperiments(instance);
62
+
63
+ // Initialize the DotExperiments instance
64
+ useEffect(() => {
65
+ const insideEditor = isInsideEditor();
66
+
67
+ if (!insideEditor) {
68
+ const dotExperimentsInstance = DotExperiments.getInstance(config);
69
+
70
+ dotExperimentsInstance.ready().then(() => {
71
+ setInstance(dotExperimentsInstance);
72
+ });
73
+ } else {
74
+ if (config.debug) {
75
+ console.warn(
76
+ 'DotExperimentsProvider: DotExperiments instance not initialized because it is inside the editor.'
77
+ );
78
+ }
79
+ }
80
+ }, [config]);
81
+
82
+ return (
83
+ <DotExperimentsContext.Provider value={instance}>{children}</DotExperimentsContext.Provider>
84
+ );
85
+ };
@@ -0,0 +1,52 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import React, { ReactNode, useCallback } from 'react';
3
+
4
+ import { DotcmsPageProps } from '@dotcms/react';
5
+
6
+ import { DotExperimentHandlingComponent } from './DotExperimentHandlingComponent';
7
+ import { DotExperimentsProvider } from './DotExperimentsProvider';
8
+
9
+ import { DotExperimentConfig } from '../shared/models';
10
+ import { useMemoizedObject } from '../shared/utils/memoize';
11
+
12
+ export interface PageProviderProps {
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ readonly entity: any;
15
+ readonly children: ReactNode;
16
+ }
17
+
18
+ /**
19
+ * Wraps a given component with experiment handling capabilities using the 'useExperimentVariant' hook.
20
+ * This HOC checks if the entity's assigned experiment variant differs from the currently displayed variant.
21
+ * If they differ, the content is hidden until the correct variant is displayed. Once the assigned variant
22
+ * matches the displayed variant, the content of the WrappedComponent is shown.
23
+ *
24
+ * @param {React.ComponentType<DotcmsPageProps>} WrappedComponent - The component to be enhanced.
25
+ * @param {DotExperimentConfig} config - Configuration for experiment handling, including any necessary
26
+ * redirection functions or other settings.
27
+ * @returns {React.FunctionComponent<DotcmsPageProps>} A component that wraps the original component,
28
+ * adding experiment handling based on the specified configuration.
29
+ */
30
+ export const withExperiments = (
31
+ WrappedComponent: React.ComponentType<DotcmsPageProps>,
32
+ config: DotExperimentConfig
33
+ ) => {
34
+ // We need to use a custom memoization hook
35
+ // because the useMemo or React.memo lose the reference of the object
36
+ // in each render, causing the experiment handling to be reinitialized.
37
+ const memoizedConfig = useMemoizedObject(config);
38
+
39
+ return useCallback(
40
+ (props: DotcmsPageProps) => {
41
+ return (
42
+ <DotExperimentsProvider config={memoizedConfig}>
43
+ <DotExperimentHandlingComponent
44
+ {...props}
45
+ WrappedComponent={WrappedComponent}
46
+ />
47
+ </DotExperimentsProvider>
48
+ );
49
+ },
50
+ [WrappedComponent, memoizedConfig]
51
+ );
52
+ };
@@ -0,0 +1,42 @@
1
+ import { renderHook } from '@testing-library/react-hooks';
2
+ import { ReactNode, useContext } from 'react';
3
+
4
+ import DotExperimentsContext from './DotExperimentsContext';
5
+
6
+ import { DotExperiments } from '../dot-experiments';
7
+
8
+ jest.mock('../dot-experiments', () => {
9
+ return jest.fn().mockImplementation(() => {
10
+ return {};
11
+ });
12
+ });
13
+
14
+ describe('useDotExperimentsContext', () => {
15
+ it('returns the context value null', () => {
16
+ const mockContextValue = null;
17
+
18
+ const { result } = renderHook(() => useContext(DotExperimentsContext), {
19
+ wrapper: ({ children }: { children: ReactNode }) => (
20
+ <DotExperimentsContext.Provider value={mockContextValue}>
21
+ {children}
22
+ </DotExperimentsContext.Provider>
23
+ )
24
+ });
25
+
26
+ expect(result.current).toEqual(mockContextValue);
27
+ });
28
+
29
+ it('returns the context value DotExperiment', () => {
30
+ const mockContextValue = {} as DotExperiments;
31
+
32
+ const { result } = renderHook(() => useContext(DotExperimentsContext), {
33
+ wrapper: ({ children }: { children: ReactNode }) => (
34
+ <DotExperimentsContext.Provider value={mockContextValue}>
35
+ {children}
36
+ </DotExperimentsContext.Provider>
37
+ )
38
+ });
39
+
40
+ expect(result.current).toEqual(mockContextValue);
41
+ });
42
+ });
@@ -1,5 +1,7 @@
1
- /// <reference types="react" />
1
+ import { createContext } from 'react';
2
+
2
3
  import { DotExperiments } from '../dot-experiments';
4
+
3
5
  /**
4
6
  * `DotExperimentsContext` is a React context that is designed to provide an instance of
5
7
  * `DotExperiments` to all of the components within its tree that are Consumers of this context.
@@ -9,5 +11,6 @@ import { DotExperiments } from '../dot-experiments';
9
11
  *
10
12
  * @see {@link https://reactjs.org/docs/context.html|React Context}
11
13
  */
12
- declare const DotExperimentsContext: import("react").Context<DotExperiments | null>;
14
+ const DotExperimentsContext = createContext<DotExperiments | null>(null);
15
+
13
16
  export default DotExperimentsContext;
@@ -0,0 +1,285 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import fakeIndexedDB from 'fake-indexeddb';
3
+ import fetchMock from 'fetch-mock';
4
+
5
+ import { DotExperiments } from './dot-experiments';
6
+ import { API_EXPERIMENTS_URL, EXPERIMENT_QUERY_PARAM_KEY } from './shared/constants';
7
+ import {
8
+ After15DaysIsUserIncludedResponse,
9
+ IsUserIncludedResponse,
10
+ LocationMock,
11
+ MOCK_CURRENT_TIMESTAMP,
12
+ MockDataStoredIndexDB,
13
+ MockDataStoredIndexDBWithNew,
14
+ MockDataStoredIndexDBWithNew15DaysLater,
15
+ NewIsUserIncludedResponse,
16
+ NoExperimentsIsUserIncludedResponse,
17
+ sessionStorageMock,
18
+ TIME_15_DAYS_MILLISECONDS,
19
+ TIME_5_DAYS_MILLISECONDS
20
+ } from './shared/mocks/mock';
21
+ import { DotExperimentConfig } from './shared/models';
22
+
23
+ jest.spyOn(Date, 'now').mockImplementation(() => MOCK_CURRENT_TIMESTAMP);
24
+
25
+ // Jitsu SDK Mock
26
+ jest.mock('@jitsu/sdk-js', () => ({
27
+ jitsuClient: jest.fn(() => ({
28
+ set: jest.fn(),
29
+ track: jest.fn().mockResolvedValue(true)
30
+ }))
31
+ }));
32
+
33
+ // SessionStorage mock
34
+ global.sessionStorage = sessionStorageMock;
35
+
36
+ // IndexDB Mock
37
+ Object.defineProperty(window, 'indexedDB', {
38
+ writable: true,
39
+ value: fakeIndexedDB
40
+ });
41
+
42
+ if (!globalThis.structuredClone) {
43
+ globalThis.structuredClone = function (obj) {
44
+ return JSON.parse(JSON.stringify(obj));
45
+ };
46
+ }
47
+
48
+ // Windows Mock
49
+ global.window = Object.create(window);
50
+ Object.defineProperty(window, 'location', {
51
+ value: {
52
+ href: 'http://localhost:8080/',
53
+ origin: 'http://localhost:8080'
54
+ }
55
+ });
56
+
57
+ describe('DotExperiments', () => {
58
+ const configMock: DotExperimentConfig = {
59
+ apiKey: 'yourApiKey',
60
+ server: 'http://localhost:8080/',
61
+ debug: false,
62
+ trackPageView: true
63
+ };
64
+
65
+ describe('DotExperiments Instance and Initialization', () => {
66
+ beforeEach(() => {
67
+ // destroy the instance of the singleton
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ (DotExperiments as any).instance = null;
70
+ });
71
+ it('should throw an error if config is not provided', () => {
72
+ expect(() => DotExperiments.getInstance()).toThrow(
73
+ 'Configuration is required to create a new instance.'
74
+ );
75
+ });
76
+
77
+ it('should instantiate the class when getInstance is called with config', () => {
78
+ const dotExperimentsInstance = DotExperiments.getInstance(configMock);
79
+
80
+ expect(dotExperimentsInstance).toBeInstanceOf(DotExperiments);
81
+ });
82
+
83
+ it('should throw an error if server is not provided in config', () => {
84
+ expect(() =>
85
+ // @ts-ignore
86
+ DotExperiments.getInstance({ 'api-key': 'api-key-test', debug: true })
87
+ ).toThrow('`server` must be provided and should not be empty!');
88
+ });
89
+
90
+ it('should throw an error if api-key is not provided in config', () => {
91
+ expect(() =>
92
+ // @ts-ignore
93
+ DotExperiments.getInstance({ server: 'http://server-test.com', debug: true })
94
+ ).toThrow('`apiKey` must be provided and should not be empty!');
95
+ });
96
+
97
+ it('should return false if the debug inactive', async () => {
98
+ const instance = DotExperiments.getInstance(configMock);
99
+
100
+ expect(instance.getIsDebugActive()).toBe(false);
101
+ });
102
+
103
+ it('should not call to trackPageView if you send the flag', async () => {
104
+ const config: DotExperimentConfig = { ...configMock, trackPageView: false };
105
+
106
+ fetchMock.post(`${configMock.server}/${API_EXPERIMENTS_URL}`, {
107
+ status: 200,
108
+ body: IsUserIncludedResponse,
109
+ headers: {
110
+ Accept: 'application/json',
111
+ 'Content-Type': 'application/json'
112
+ }
113
+ });
114
+
115
+ const instance = DotExperiments.getInstance(config);
116
+
117
+ const spyTrackPageView = jest.spyOn(instance, 'trackPageView');
118
+
119
+ expect(spyTrackPageView).not.toHaveBeenCalled();
120
+
121
+ await instance.locationChanged(LocationMock).then(() => {
122
+ expect(spyTrackPageView).not.toHaveBeenCalled();
123
+ });
124
+ });
125
+
126
+ it('should not call to trackPageView if you dont have experiment to track', async () => {
127
+ const config: DotExperimentConfig = { ...configMock, trackPageView: false };
128
+
129
+ fetchMock.post(
130
+ `${configMock.server}/${API_EXPERIMENTS_URL}`,
131
+ {
132
+ status: 200,
133
+ body: NoExperimentsIsUserIncludedResponse,
134
+ headers: {
135
+ Accept: 'application/json',
136
+ 'Content-Type': 'application/json'
137
+ }
138
+ },
139
+ { overwriteRoutes: true }
140
+ );
141
+
142
+ const instance = DotExperiments.getInstance(config);
143
+
144
+ const spyTrackPageView = jest.spyOn(instance, 'trackPageView');
145
+
146
+ expect(spyTrackPageView).not.toHaveBeenCalled();
147
+
148
+ await instance.locationChanged(LocationMock).then(() => {
149
+ expect(spyTrackPageView).not.toHaveBeenCalled();
150
+ });
151
+ });
152
+
153
+ it('should return a a string with the query params of variant by the url given', async () => {
154
+ const config: DotExperimentConfig = { ...configMock, trackPageView: false };
155
+
156
+ fetchMock.post(
157
+ `${configMock.server}/${API_EXPERIMENTS_URL}`,
158
+ {
159
+ status: 200,
160
+ body: IsUserIncludedResponse,
161
+ headers: {
162
+ Accept: 'application/json',
163
+ 'Content-Type': 'application/json'
164
+ }
165
+ },
166
+ { overwriteRoutes: true }
167
+ );
168
+
169
+ const instance = DotExperiments.getInstance(config);
170
+
171
+ await instance.ready();
172
+
173
+ const EMPTY_URL = '';
174
+
175
+ const expected1 = new URLSearchParams({});
176
+
177
+ expect(instance.getVariantAsQueryParam(EMPTY_URL)).toStrictEqual(expected1);
178
+
179
+ const URL_WITH_EXPERIMENT = '/blog';
180
+
181
+ const expected2 = new URLSearchParams({
182
+ [EXPERIMENT_QUERY_PARAM_KEY]:
183
+ IsUserIncludedResponse.entity.experiments[0].variant.name
184
+ });
185
+
186
+ expect(instance.getVariantAsQueryParam(URL_WITH_EXPERIMENT)).toStrictEqual(expected2);
187
+
188
+ const URL_NO_EXPERIMENT = '/destinations';
189
+
190
+ const expected3 = new URLSearchParams({});
191
+
192
+ expect(instance.getVariantAsQueryParam(URL_NO_EXPERIMENT)).toStrictEqual(expected3);
193
+ });
194
+ });
195
+
196
+ describe('Class interactions', () => {
197
+ beforeEach(() => {
198
+ fetchMock.restore();
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
200
+ (DotExperiments as any).instance = null;
201
+ });
202
+
203
+ it('should simulate the changes of the data in first run, after 5 days and 15 days.', async () => {
204
+ // First time the user enter to the page
205
+
206
+ fetchMock.post(`${configMock.server}/${API_EXPERIMENTS_URL}`, {
207
+ status: 200,
208
+ body: IsUserIncludedResponse,
209
+ headers: {
210
+ Accept: 'application/json',
211
+ 'Content-Type': 'application/json'
212
+ }
213
+ });
214
+
215
+ const instance = DotExperiments.getInstance(configMock);
216
+
217
+ const spyTrackPageView = jest.spyOn(instance, 'trackPageView');
218
+
219
+ await instance.ready().then(() => {
220
+ const experiments = instance.experiments;
221
+
222
+ expect(experiments.length).toBe(1);
223
+ expect(experiments).toEqual(MockDataStoredIndexDB);
224
+
225
+ expect(spyTrackPageView).toBeCalledTimes(1);
226
+ });
227
+
228
+ // Second time the user enter to the page
229
+ // change the time 5 days later
230
+ jest.spyOn(Date, 'now').mockImplementation(
231
+ () => MOCK_CURRENT_TIMESTAMP + TIME_5_DAYS_MILLISECONDS
232
+ );
233
+
234
+ fetchMock.post(
235
+ `${configMock.server}/${API_EXPERIMENTS_URL}`,
236
+ {
237
+ status: 200,
238
+ body: NewIsUserIncludedResponse,
239
+ headers: {
240
+ Accept: 'application/json',
241
+ 'Content-Type': 'application/json'
242
+ }
243
+ },
244
+ { overwriteRoutes: true }
245
+ );
246
+
247
+ await instance.locationChanged(LocationMock).then(() => {
248
+ // get the experiments stored in the indexDB
249
+ const experiments = instance.experiments;
250
+
251
+ expect(experiments.length).toBe(1);
252
+ expect(experiments).toEqual(MockDataStoredIndexDBWithNew);
253
+ expect(spyTrackPageView).toBeCalledTimes(2);
254
+ });
255
+
256
+ fetchMock.post(
257
+ `${configMock.server}/${API_EXPERIMENTS_URL}`,
258
+ {
259
+ status: 200,
260
+ body: After15DaysIsUserIncludedResponse,
261
+ headers: {
262
+ Accept: 'application/json',
263
+ 'Content-Type': 'application/json'
264
+ }
265
+ },
266
+ { overwriteRoutes: true }
267
+ );
268
+
269
+ // Third try, after 15 days
270
+ const location = { ...LocationMock, href: 'http://localhost/destinations' };
271
+
272
+ jest.spyOn(Date, 'now').mockImplementation(
273
+ () => MOCK_CURRENT_TIMESTAMP + TIME_15_DAYS_MILLISECONDS
274
+ );
275
+ await instance.locationChanged(location).then(() => {
276
+ // get the experiments stored in the indexDB
277
+ const experiments = instance.experiments;
278
+
279
+ expect(experiments.length).toBe(1);
280
+ expect(experiments).toEqual(MockDataStoredIndexDBWithNew15DaysLater);
281
+ expect(spyTrackPageView).toBeCalledTimes(3);
282
+ });
283
+ });
284
+ });
285
+ });