@dotcms/experiments 0.0.1-alpha.37 → 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.
- package/.babelrc +12 -0
- package/.eslintrc.json +26 -0
- package/jest.config.ts +11 -0
- package/package.json +4 -8
- package/project.json +55 -0
- package/src/lib/components/{DotExperimentHandlingComponent.d.ts → DotExperimentHandlingComponent.tsx} +20 -3
- package/src/lib/components/DotExperimentsProvider.spec.tsx +62 -0
- package/src/lib/components/{DotExperimentsProvider.d.ts → DotExperimentsProvider.tsx} +41 -3
- package/src/lib/components/withExperiments.tsx +52 -0
- package/src/lib/contexts/DotExperimentsContext.spec.tsx +42 -0
- package/src/lib/contexts/{DotExperimentsContext.d.ts → DotExperimentsContext.tsx} +5 -2
- package/src/lib/dot-experiments.spec.ts +285 -0
- package/src/lib/dot-experiments.ts +716 -0
- package/src/lib/hooks/useExperimentVariant.spec.tsx +111 -0
- package/src/lib/hooks/useExperimentVariant.ts +55 -0
- package/src/lib/hooks/useExperiments.ts +90 -0
- package/src/lib/shared/{constants.d.ts → constants.ts} +35 -18
- package/src/lib/shared/mocks/mock.ts +209 -0
- package/src/lib/shared/{models.d.ts → models.ts} +35 -2
- package/src/lib/shared/parser/parse.spec.ts +187 -0
- package/src/lib/shared/parser/parser.ts +171 -0
- package/src/lib/shared/persistence/index-db-database-handler.spec.ts +100 -0
- package/src/lib/shared/persistence/index-db-database-handler.ts +218 -0
- package/src/lib/shared/utils/DotLogger.ts +57 -0
- package/src/lib/shared/utils/memoize.spec.ts +49 -0
- package/src/lib/shared/utils/memoize.ts +49 -0
- package/src/lib/shared/utils/utils.spec.ts +142 -0
- package/src/lib/shared/utils/utils.ts +203 -0
- package/src/lib/standalone.spec.ts +36 -0
- package/src/lib/standalone.ts +28 -0
- package/tsconfig.json +20 -0
- package/tsconfig.lib.json +20 -0
- package/tsconfig.spec.json +9 -0
- package/vite.config.ts +41 -0
- package/index.esm.d.ts +0 -1
- package/index.esm.js +0 -7174
- package/src/lib/components/withExperiments.d.ts +0 -20
- package/src/lib/dot-experiments.d.ts +0 -289
- package/src/lib/hooks/useExperimentVariant.d.ts +0 -21
- package/src/lib/hooks/useExperiments.d.ts +0 -14
- package/src/lib/shared/mocks/mock.d.ts +0 -43
- package/src/lib/shared/parser/parser.d.ts +0 -54
- package/src/lib/shared/persistence/index-db-database-handler.d.ts +0 -87
- package/src/lib/shared/utils/DotLogger.d.ts +0 -15
- package/src/lib/shared/utils/memoize.d.ts +0 -7
- package/src/lib/shared/utils/utils.d.ts +0 -73
- package/src/lib/standalone.d.ts +0 -7
- /package/src/{index.d.ts → index.ts} +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import * as dotcmsClient from '@dotcms/client';
|
|
5
|
+
|
|
6
|
+
import { useExperimentVariant } from './useExperimentVariant';
|
|
7
|
+
|
|
8
|
+
import MockDotExperimentsContext from '../contexts/DotExperimentsContext';
|
|
9
|
+
import { EXPERIMENT_DEFAULT_VARIANT_NAME } from '../shared/constants';
|
|
10
|
+
import { LocationMock } from '../shared/mocks/mock';
|
|
11
|
+
|
|
12
|
+
interface WrapperProps {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const createMockDotExperimentsContext = (variantResponse: unknown) => {
|
|
17
|
+
const mockGetVariantFromHref = jest.fn().mockImplementation(() => variantResponse);
|
|
18
|
+
|
|
19
|
+
return React.createContext({
|
|
20
|
+
getVariantFromHref: mockGetVariantFromHref
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
jest.mock('../contexts/DotExperimentsContext', () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
default: createMockDotExperimentsContext({ name: 'variant-1' })
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const wrapper = ({ children }: WrapperProps) => (
|
|
30
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
31
|
+
<MockDotExperimentsContext.Provider value={React.useContext(MockDotExperimentsContext)}>
|
|
32
|
+
{children}
|
|
33
|
+
</MockDotExperimentsContext.Provider>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
describe('useExperimentVariant', () => {
|
|
37
|
+
describe('shouldWaitForVariant `false`', () => {
|
|
38
|
+
it('if is insideEditor is `false`', () => {
|
|
39
|
+
const mockData = {
|
|
40
|
+
runningExperimentId: '1',
|
|
41
|
+
viewAs: { variantId: '1' }
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
jest.spyOn(dotcmsClient, 'isInsideEditor').mockReturnValue(true);
|
|
45
|
+
|
|
46
|
+
const { result } = renderHook(() => useExperimentVariant(mockData));
|
|
47
|
+
|
|
48
|
+
const { shouldWaitForVariant } = result.current;
|
|
49
|
+
|
|
50
|
+
expect(shouldWaitForVariant).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it(' if `runningExperimentId` is undefined', () => {
|
|
54
|
+
const mockData = {
|
|
55
|
+
viewAs: { variantId: EXPERIMENT_DEFAULT_VARIANT_NAME }
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
jest.spyOn(dotcmsClient, 'isInsideEditor').mockReturnValue(false);
|
|
59
|
+
|
|
60
|
+
const { result } = renderHook(() => useExperimentVariant(mockData));
|
|
61
|
+
|
|
62
|
+
const { shouldWaitForVariant } = result.current;
|
|
63
|
+
|
|
64
|
+
expect(shouldWaitForVariant).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it(' if VariantId get from `PageApi` is same of VariantAssigned', () => {
|
|
68
|
+
const locationSpy = jest.spyOn(window, 'location', 'get');
|
|
69
|
+
|
|
70
|
+
const locationMock = { ...LocationMock, pathname: '/blog' };
|
|
71
|
+
|
|
72
|
+
locationSpy.mockReturnValue(locationMock);
|
|
73
|
+
|
|
74
|
+
jest.spyOn(dotcmsClient, 'isInsideEditor').mockReturnValue(false);
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(
|
|
77
|
+
() =>
|
|
78
|
+
useExperimentVariant({
|
|
79
|
+
runningExperimentId: 'exp-id',
|
|
80
|
+
viewAs: { variantId: 'variant-1' }
|
|
81
|
+
}),
|
|
82
|
+
{ wrapper }
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(result.current.shouldWaitForVariant).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('shouldWaitForVariant `true`', () => {
|
|
89
|
+
it(' if VariantId get from `PageApi` is different of VariantAssigned', () => {
|
|
90
|
+
const locationSpy = jest.spyOn(window, 'location', 'get');
|
|
91
|
+
|
|
92
|
+
const locationMock = { ...LocationMock, pathname: '/blog' };
|
|
93
|
+
|
|
94
|
+
locationSpy.mockReturnValue(locationMock);
|
|
95
|
+
|
|
96
|
+
jest.spyOn(dotcmsClient, 'isInsideEditor').mockReturnValue(false);
|
|
97
|
+
|
|
98
|
+
const { result } = renderHook(
|
|
99
|
+
() =>
|
|
100
|
+
useExperimentVariant({
|
|
101
|
+
runningExperimentId: 'exp-id',
|
|
102
|
+
viewAs: { variantId: EXPERIMENT_DEFAULT_VARIANT_NAME }
|
|
103
|
+
}),
|
|
104
|
+
{ wrapper }
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(result.current.shouldWaitForVariant).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { isInsideEditor } from '@dotcms/client';
|
|
4
|
+
|
|
5
|
+
import DotExperimentsContext from '../contexts/DotExperimentsContext';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A React Hook that determines whether to wait for the correct variant in an A/B testing scenario.
|
|
9
|
+
* This is used to avoid flickering - showing the original content before redirecting to the assigned variant.
|
|
10
|
+
*
|
|
11
|
+
* The hook uses the running experiment id and viewAs (containing variantId) from the provided data.
|
|
12
|
+
* It then works with the DotExperimentsContext to synchronize between the assigned variant and the one requested.
|
|
13
|
+
* If the hook is executed inside an editor or if no running experiment id is provided, it immediately signals not to wait for the variant.
|
|
14
|
+
* Similarly, if the assigned variant matches the requested one, it signals not to wait for the variant.
|
|
15
|
+
* By default, the hook signals to wait for the variant.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} data - An object containing the runningExperimentId and viewAs (containing variantId).
|
|
18
|
+
* @returns {Object} An object with a function `shouldWaitForVariant` that, when called, returns `true` if it should wait for the correct variant, `false` otherwise.
|
|
19
|
+
*/
|
|
20
|
+
export const useExperimentVariant = (data: {
|
|
21
|
+
runningExperimentId?: string;
|
|
22
|
+
viewAs: { variantId: string };
|
|
23
|
+
}): { shouldWaitForVariant: boolean } => {
|
|
24
|
+
const dotExperimentInstance = useContext(DotExperimentsContext);
|
|
25
|
+
|
|
26
|
+
const { runningExperimentId, viewAs } = data;
|
|
27
|
+
|
|
28
|
+
const { variantId } = viewAs;
|
|
29
|
+
|
|
30
|
+
// By default, wait for the variant
|
|
31
|
+
const [shouldWaitForVariant, setShouldWaitForVariant] = useState<boolean>(true);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (isInsideEditor() || !runningExperimentId) {
|
|
35
|
+
setShouldWaitForVariant(false);
|
|
36
|
+
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const location = typeof window !== 'undefined' ? window.location : undefined;
|
|
41
|
+
|
|
42
|
+
if (location && dotExperimentInstance) {
|
|
43
|
+
const variantAssigned = dotExperimentInstance.getVariantFromHref(location.pathname);
|
|
44
|
+
|
|
45
|
+
if (variantAssigned && variantId === variantAssigned.name) {
|
|
46
|
+
// the data requested and the variant assigned is the correct no need to wait
|
|
47
|
+
setShouldWaitForVariant(false);
|
|
48
|
+
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, [dotExperimentInstance, data]);
|
|
53
|
+
|
|
54
|
+
return { shouldWaitForVariant };
|
|
55
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { isInsideEditor } from '@dotcms/client';
|
|
4
|
+
|
|
5
|
+
import { DotExperiments } from '../dot-experiments';
|
|
6
|
+
import { EXPERIMENT_DEFAULT_VARIANT_NAME, EXPERIMENT_QUERY_PARAM_KEY } from '../shared/constants';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom hook `useExperiments`.
|
|
10
|
+
*
|
|
11
|
+
* This hook is designed to handle changes in the location of the current DotExperiments
|
|
12
|
+
* instance and set a global click handler that redirects the application when an element with an
|
|
13
|
+
* assigned variant is clicked.
|
|
14
|
+
*
|
|
15
|
+
* It also manages adding or removing the experimentation query parameter from the URL as
|
|
16
|
+
* appropriate when a click occurs.
|
|
17
|
+
*
|
|
18
|
+
* @returns {void}
|
|
19
|
+
*/
|
|
20
|
+
export const useExperiments = (instance: DotExperiments | null): void => {
|
|
21
|
+
/**
|
|
22
|
+
* This `useEffect` hook is responsible for tracking location changes when not inside an editor environment, and invoking the
|
|
23
|
+
* `locationChanged` method from `experimentContext` with current location and custom redirection function.
|
|
24
|
+
*
|
|
25
|
+
*/
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!instance || typeof document === 'undefined') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const insideEditor = isInsideEditor();
|
|
32
|
+
|
|
33
|
+
if (!insideEditor) {
|
|
34
|
+
const location = typeof window !== 'undefined' ? window.location : undefined;
|
|
35
|
+
|
|
36
|
+
if (instance && location) {
|
|
37
|
+
instance.locationChanged(location, instance.customRedirectFn);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, [instance]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* This effect sets a click handler on the document.
|
|
44
|
+
* It captures click events and redirects to a new URL if the clicked anchor has a variant assigned,
|
|
45
|
+
* additional removing the experiment query param from the URL.
|
|
46
|
+
*/
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!instance) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const customClickHandler = (event: MouseEvent) => {
|
|
53
|
+
const target = (event.target as HTMLElement).closest('a');
|
|
54
|
+
|
|
55
|
+
if (!target) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const clickedHref = target.getAttribute('href');
|
|
60
|
+
|
|
61
|
+
if (clickedHref) {
|
|
62
|
+
const modifiedUrl = new URL(clickedHref, instance.location.href);
|
|
63
|
+
|
|
64
|
+
// Remove the experiment query param from the URL
|
|
65
|
+
modifiedUrl.searchParams.delete(EXPERIMENT_QUERY_PARAM_KEY);
|
|
66
|
+
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
|
|
69
|
+
// Get the variant from the href of the clicked anchor
|
|
70
|
+
const variant = instance.getVariantFromHref(clickedHref);
|
|
71
|
+
|
|
72
|
+
if (variant && variant.name !== EXPERIMENT_DEFAULT_VARIANT_NAME) {
|
|
73
|
+
// Set the experiment query param in the URL if the variant is not the default one
|
|
74
|
+
modifiedUrl.searchParams.set(EXPERIMENT_QUERY_PARAM_KEY, variant.name);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Redirect to the new URL using the custom redirect function
|
|
78
|
+
instance.customRedirectFn(modifiedUrl.toString());
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Register the click handler to all elements in the document
|
|
83
|
+
document.addEventListener('click', customClickHandler);
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
// Remove the click handler when the component is unmounted
|
|
87
|
+
document.removeEventListener('click', customClickHandler);
|
|
88
|
+
};
|
|
89
|
+
}, [instance]);
|
|
90
|
+
};
|
|
@@ -3,34 +3,39 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @constant {string}
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
6
|
+
export const EXPERIMENT_WINDOWS_KEY = 'dotExperiment';
|
|
7
|
+
|
|
7
8
|
/**
|
|
8
9
|
* The default variant name for an experiment.
|
|
9
10
|
*
|
|
10
11
|
* @type {string}
|
|
11
12
|
* @constant
|
|
12
13
|
*/
|
|
13
|
-
export
|
|
14
|
+
export const EXPERIMENT_DEFAULT_VARIANT_NAME = 'DEFAULT';
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
17
|
* The key used to store or retrieve the information in the SessionStore
|
|
16
18
|
*
|
|
17
19
|
* @constant {string}
|
|
18
20
|
*/
|
|
19
|
-
export
|
|
21
|
+
export const EXPERIMENT_QUERY_PARAM_KEY = 'variantName';
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* The key used to store or retrieve the information in the SessionStore
|
|
22
25
|
* indicating whether an experiment has already been checked.
|
|
23
26
|
*
|
|
24
27
|
* @constant {string}
|
|
25
28
|
*/
|
|
26
|
-
export
|
|
29
|
+
export const EXPERIMENT_ALREADY_CHECKED_KEY = 'experimentAlreadyCheck';
|
|
30
|
+
|
|
27
31
|
/**
|
|
28
32
|
* EXPERIMENT_FETCH_EXPIRE_TIME is a constant that represents the name of the variable used to store
|
|
29
33
|
* the expire time for experiment fetching. It is a string value 'experimentFetchExpireTime'.
|
|
30
34
|
*
|
|
31
35
|
* @constant {string}
|
|
32
36
|
*/
|
|
33
|
-
export
|
|
37
|
+
export const EXPERIMENT_FETCH_EXPIRE_TIME_KEY = 'experimentFetchExpireTime';
|
|
38
|
+
|
|
34
39
|
/**
|
|
35
40
|
* The duration in milliseconds for which data should be stored in the local storage.
|
|
36
41
|
*
|
|
@@ -39,26 +44,34 @@ export declare const EXPERIMENT_FETCH_EXPIRE_TIME_KEY = "experimentFetchExpireTi
|
|
|
39
44
|
* @default 86400000 (A day)
|
|
40
45
|
*
|
|
41
46
|
*/
|
|
42
|
-
export
|
|
47
|
+
export const LOCAL_STORAGE_TIME_DURATION_MILLISECONDS = 86400 * 1000;
|
|
48
|
+
|
|
43
49
|
/**
|
|
44
50
|
* The name of the experiment script file.
|
|
45
51
|
*
|
|
46
52
|
* @constant {string}
|
|
47
53
|
*/
|
|
48
|
-
export
|
|
54
|
+
export const EXPERIMENT_SCRIPT_FILE_NAME = 'dot-experiments.min.iife.js';
|
|
55
|
+
|
|
49
56
|
/**
|
|
50
57
|
* The prefix used for the experiment script data attributes.
|
|
51
58
|
*
|
|
52
59
|
* @constant {string}
|
|
53
60
|
*/
|
|
54
|
-
export
|
|
61
|
+
export const EXPERIMENT_SCRIPT_DATA_PREFIX = 'data-experiment-';
|
|
62
|
+
|
|
55
63
|
/**
|
|
56
64
|
* Array containing the allowed data attributes for an experiment.
|
|
57
65
|
*
|
|
58
66
|
* @type {Array.<string>}
|
|
59
67
|
* @constant
|
|
60
68
|
*/
|
|
61
|
-
export
|
|
69
|
+
export const EXPERIMENT_ALLOWED_DATA_ATTRIBUTES = [
|
|
70
|
+
EXPERIMENT_SCRIPT_DATA_PREFIX + 'api-key',
|
|
71
|
+
EXPERIMENT_SCRIPT_DATA_PREFIX + 'server',
|
|
72
|
+
EXPERIMENT_SCRIPT_DATA_PREFIX + 'debug'
|
|
73
|
+
];
|
|
74
|
+
|
|
62
75
|
/**
|
|
63
76
|
* API_EXPERIMENTS_URL
|
|
64
77
|
*
|
|
@@ -67,29 +80,33 @@ export declare const EXPERIMENT_ALLOWED_DATA_ATTRIBUTES: string[];
|
|
|
67
80
|
* @type {string}
|
|
68
81
|
* @constant
|
|
69
82
|
*/
|
|
70
|
-
export
|
|
83
|
+
export const API_EXPERIMENTS_URL = 'api/v1/experiments/isUserIncluded';
|
|
84
|
+
|
|
71
85
|
/**
|
|
72
86
|
* The name of the experiment database store in indexDB.
|
|
73
87
|
*
|
|
74
88
|
* @type {string}
|
|
75
89
|
* @constant
|
|
76
90
|
*/
|
|
77
|
-
export
|
|
91
|
+
export const EXPERIMENT_DB_STORE_NAME = 'dotExperimentStore';
|
|
92
|
+
|
|
78
93
|
/**
|
|
79
94
|
* The path to the key in the database IndexDB representing the running experiment data.
|
|
80
95
|
* @type {string}
|
|
81
96
|
*/
|
|
82
|
-
export
|
|
97
|
+
export const EXPERIMENT_DB_KEY_PATH = 'running_experiment';
|
|
98
|
+
|
|
83
99
|
/**
|
|
84
100
|
* Enumeration of debug levels.
|
|
85
101
|
*
|
|
86
102
|
* @enum {string}
|
|
87
103
|
* @readonly
|
|
88
104
|
*/
|
|
89
|
-
export
|
|
90
|
-
NONE =
|
|
91
|
-
DEBUG =
|
|
92
|
-
WARN =
|
|
93
|
-
ERROR =
|
|
105
|
+
export enum DEBUG_LEVELS {
|
|
106
|
+
NONE = 'NONE',
|
|
107
|
+
DEBUG = 'DEBUG',
|
|
108
|
+
WARN = 'WARN',
|
|
109
|
+
ERROR = 'ERROR'
|
|
94
110
|
}
|
|
95
|
-
|
|
111
|
+
|
|
112
|
+
export const PAGE_VIEW_EVENT_NAME = 'pageview';
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Experiment, ExperimentEvent, IsUserIncludedApiResponse } from '../models';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents the response object for the IsUserIncluded API.
|
|
5
|
+
* @typedef {Object} IsUserIncludedResponse
|
|
6
|
+
* @property {IsUserIncludedApiResponse} entity - The response entity.
|
|
7
|
+
* @property {string[]} errors - Array of error messages, if any.
|
|
8
|
+
* @property {Object} i18nMessagesMap - Map of internationalization messages.
|
|
9
|
+
* @property {string[]} messages - Array of additional messages, if any.
|
|
10
|
+
*/
|
|
11
|
+
export const IsUserIncludedResponse: IsUserIncludedApiResponse = {
|
|
12
|
+
entity: {
|
|
13
|
+
excludedExperimentIds: [],
|
|
14
|
+
excludedExperimentIdsEnded: [],
|
|
15
|
+
experiments: [
|
|
16
|
+
{
|
|
17
|
+
id: '11111-11111-11111-11111-11111',
|
|
18
|
+
lookBackWindow: {
|
|
19
|
+
expireMillis: 1209600000,
|
|
20
|
+
value: 'AAAAAAAAAA'
|
|
21
|
+
},
|
|
22
|
+
name: 'Exp1',
|
|
23
|
+
pageUrl: '/blog/index',
|
|
24
|
+
regexs: {
|
|
25
|
+
isExperimentPage:
|
|
26
|
+
'^(http|https):\\/\\/(localhost|127.0.0.1|\\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,})(:\\d{1,5})?\\/blog(\\/index|\\/)?(\\/?\\?.*)?$',
|
|
27
|
+
isTargetPage: '.*destinations.*'
|
|
28
|
+
},
|
|
29
|
+
runningId: '1111111-22222222',
|
|
30
|
+
variant: {
|
|
31
|
+
name: 'variant-1',
|
|
32
|
+
url: '/blog/index?variantName=variant-1'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
includedExperimentIds: ['11111-11111-11111-11111-11111']
|
|
37
|
+
},
|
|
38
|
+
errors: [],
|
|
39
|
+
i18nMessagesMap: {},
|
|
40
|
+
messages: []
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const NewIsUserIncludedResponse: IsUserIncludedApiResponse = {
|
|
44
|
+
entity: {
|
|
45
|
+
excludedExperimentIds: ['11111-11111-11111-11111-11111'],
|
|
46
|
+
excludedExperimentIdsEnded: ['11111-11111-11111-11111-11111'],
|
|
47
|
+
experiments: [
|
|
48
|
+
{
|
|
49
|
+
id: '222222-222222-222222-222222-222222',
|
|
50
|
+
lookBackWindow: {
|
|
51
|
+
expireMillis: 1209600000,
|
|
52
|
+
value: 'BBBBBBBBBBBBBB'
|
|
53
|
+
},
|
|
54
|
+
name: 'Exp2',
|
|
55
|
+
pageUrl: '/destinations/index',
|
|
56
|
+
regexs: {
|
|
57
|
+
isExperimentPage:
|
|
58
|
+
'^(http|https):\\/\\/(localhost|127.0.0.1|\\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,})(:\\d{1,5})?\\/destinations(\\/index|\\/)?(\\/?\\?.*)?$',
|
|
59
|
+
isTargetPage: null
|
|
60
|
+
},
|
|
61
|
+
runningId: '33333333-3333333333',
|
|
62
|
+
variant: {
|
|
63
|
+
name: 'variant-1',
|
|
64
|
+
url: '/blog/index?variantName=variant-1'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
includedExperimentIds: ['222222-222222-222222-222222-222222']
|
|
69
|
+
},
|
|
70
|
+
errors: [],
|
|
71
|
+
i18nMessagesMap: {},
|
|
72
|
+
messages: []
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const After15DaysIsUserIncludedResponse: IsUserIncludedApiResponse = {
|
|
76
|
+
entity: {
|
|
77
|
+
excludedExperimentIds: ['222222-222222-222222-222222-222222'],
|
|
78
|
+
excludedExperimentIdsEnded: [],
|
|
79
|
+
experiments: [],
|
|
80
|
+
includedExperimentIds: []
|
|
81
|
+
},
|
|
82
|
+
errors: [],
|
|
83
|
+
i18nMessagesMap: {},
|
|
84
|
+
messages: []
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const NoExperimentsIsUserIncludedResponse: IsUserIncludedApiResponse = {
|
|
88
|
+
entity: {
|
|
89
|
+
excludedExperimentIds: [],
|
|
90
|
+
excludedExperimentIdsEnded: [],
|
|
91
|
+
experiments: [],
|
|
92
|
+
includedExperimentIds: []
|
|
93
|
+
},
|
|
94
|
+
errors: [],
|
|
95
|
+
i18nMessagesMap: {},
|
|
96
|
+
messages: []
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const MOCK_CURRENT_TIMESTAMP = 1704096000000;
|
|
100
|
+
|
|
101
|
+
export const TIME_15_DAYS_MILLISECONDS = 1296000 * 1000;
|
|
102
|
+
|
|
103
|
+
export const TIME_5_DAYS_MILLISECONDS = 432000 * 1000;
|
|
104
|
+
|
|
105
|
+
export const MockDataStoredIndexDB: Experiment[] = [
|
|
106
|
+
{
|
|
107
|
+
...IsUserIncludedResponse.entity.experiments[0],
|
|
108
|
+
lookBackWindow: {
|
|
109
|
+
...IsUserIncludedResponse.entity.experiments[0].lookBackWindow,
|
|
110
|
+
// Added expireTime to the lookBackWindow
|
|
111
|
+
expireTime:
|
|
112
|
+
MOCK_CURRENT_TIMESTAMP +
|
|
113
|
+
IsUserIncludedResponse.entity.experiments[0].lookBackWindow.expireMillis
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
export const MockDataStoredIndexDBNew: Experiment[] = [
|
|
119
|
+
{
|
|
120
|
+
...NewIsUserIncludedResponse.entity.experiments[0],
|
|
121
|
+
lookBackWindow: {
|
|
122
|
+
...NewIsUserIncludedResponse.entity.experiments[0].lookBackWindow,
|
|
123
|
+
// Added expireTime to the lookBackWindow
|
|
124
|
+
expireTime:
|
|
125
|
+
// 2nd request, 5 days later, so expireTime is 5 days from MOCK_CURRENT_TIMESTAMP
|
|
126
|
+
MOCK_CURRENT_TIMESTAMP +
|
|
127
|
+
TIME_5_DAYS_MILLISECONDS +
|
|
128
|
+
NewIsUserIncludedResponse.entity.experiments[0].lookBackWindow.expireMillis
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// Final data to be stored in IndexedDB only the last, the first will be removed by was ended from `excludedExperimentIdsEnded`
|
|
134
|
+
export const MockDataStoredIndexDBWithNew: Experiment[] = [...MockDataStoredIndexDBNew];
|
|
135
|
+
|
|
136
|
+
// Mock Store after 15 days
|
|
137
|
+
export const MockDataStoredIndexDBWithNew15DaysLater: Experiment[] = [...MockDataStoredIndexDBNew];
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Represents an event that indicates the expected experiments parsed from a response to send to Analytics.
|
|
141
|
+
*
|
|
142
|
+
* @typedef {Object} ExpectedExperimentsParsedEvent
|
|
143
|
+
* @property {ExperimentEvent[]} experiments - An array of experiment events.
|
|
144
|
+
*/
|
|
145
|
+
export const ExpectedExperimentsParsedEvent: ExperimentEvent[] = [
|
|
146
|
+
{
|
|
147
|
+
experiment: IsUserIncludedResponse.entity.experiments[0].id,
|
|
148
|
+
runningId: IsUserIncludedResponse.entity.experiments[0].runningId,
|
|
149
|
+
variant: IsUserIncludedResponse.entity.experiments[0].variant.name,
|
|
150
|
+
lookBackWindow: IsUserIncludedResponse.entity.experiments[0].lookBackWindow.value,
|
|
151
|
+
isExperimentPage: false,
|
|
152
|
+
isTargetPage: false
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Represents a mock location object.
|
|
158
|
+
*
|
|
159
|
+
* @typedef {Object} LocationMock
|
|
160
|
+
* @property {string} href - The complete URL.
|
|
161
|
+
*
|
|
162
|
+
*/
|
|
163
|
+
export const LocationMock: Location = {
|
|
164
|
+
hash: '',
|
|
165
|
+
host: '',
|
|
166
|
+
hostname: '',
|
|
167
|
+
href: 'http://localhost/blog',
|
|
168
|
+
origin: '',
|
|
169
|
+
pathname: '',
|
|
170
|
+
port: '',
|
|
171
|
+
protocol: '',
|
|
172
|
+
search: '',
|
|
173
|
+
assign: () => {
|
|
174
|
+
//
|
|
175
|
+
},
|
|
176
|
+
replace: () => {
|
|
177
|
+
//
|
|
178
|
+
},
|
|
179
|
+
reload: () => {
|
|
180
|
+
//
|
|
181
|
+
},
|
|
182
|
+
toString: () => {
|
|
183
|
+
return '';
|
|
184
|
+
},
|
|
185
|
+
ancestorOrigins: {} as DOMStringList
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const store: { [key: string]: string } = {};
|
|
189
|
+
|
|
190
|
+
export const sessionStorageMock = {
|
|
191
|
+
getItem: function (key: string): string | null {
|
|
192
|
+
return store[key] || null;
|
|
193
|
+
},
|
|
194
|
+
setItem: function (key: string, value: string) {
|
|
195
|
+
store[key] = value;
|
|
196
|
+
},
|
|
197
|
+
removeItem: function (key: string) {
|
|
198
|
+
delete store[key];
|
|
199
|
+
},
|
|
200
|
+
clear: function () {
|
|
201
|
+
Object.keys(store).forEach((key) => delete store[key]);
|
|
202
|
+
},
|
|
203
|
+
key: function (index: number) {
|
|
204
|
+
return Object.keys(store)[index] || null;
|
|
205
|
+
},
|
|
206
|
+
get length() {
|
|
207
|
+
return Object.keys(store).length;
|
|
208
|
+
}
|
|
209
|
+
};
|