@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.
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
@@ -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 declare const EXPERIMENT_WINDOWS_KEY = "dotExperiment";
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 declare const EXPERIMENT_DEFAULT_VARIANT_NAME = "DEFAULT";
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 declare const EXPERIMENT_QUERY_PARAM_KEY = "variantName";
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 declare const EXPERIMENT_ALREADY_CHECKED_KEY = "experimentAlreadyCheck";
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 declare const EXPERIMENT_FETCH_EXPIRE_TIME_KEY = "experimentFetchExpireTime";
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 declare const LOCAL_STORAGE_TIME_DURATION_MILLISECONDS: number;
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 declare const EXPERIMENT_SCRIPT_FILE_NAME = "dot-experiments.min.iife.js";
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 declare const EXPERIMENT_SCRIPT_DATA_PREFIX = "data-experiment-";
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 declare const EXPERIMENT_ALLOWED_DATA_ATTRIBUTES: string[];
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 declare const API_EXPERIMENTS_URL = "api/v1/experiments/isUserIncluded";
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 declare const EXPERIMENT_DB_STORE_NAME = "dotExperimentStore";
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 declare const EXPERIMENT_DB_KEY_PATH = "running_experiment";
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 declare enum DEBUG_LEVELS {
90
- NONE = "NONE",
91
- DEBUG = "DEBUG",
92
- WARN = "WARN",
93
- ERROR = "ERROR"
105
+ export enum DEBUG_LEVELS {
106
+ NONE = 'NONE',
107
+ DEBUG = 'DEBUG',
108
+ WARN = 'WARN',
109
+ ERROR = 'ERROR'
94
110
  }
95
- export declare const PAGE_VIEW_EVENT_NAME = "pageview";
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
+ };