@dotcms/experiments 0.0.1-alpha.39 → 0.0.1-alpha.41
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/index.esm.d.ts +1 -0
- package/index.esm.js +7174 -0
- package/package.json +10 -6
- package/src/lib/components/{DotExperimentHandlingComponent.tsx → DotExperimentHandlingComponent.d.ts} +3 -20
- package/src/lib/components/{DotExperimentsProvider.tsx → DotExperimentsProvider.d.ts} +3 -41
- package/src/lib/components/withExperiments.d.ts +20 -0
- package/src/lib/contexts/{DotExperimentsContext.tsx → DotExperimentsContext.d.ts} +2 -5
- package/src/lib/dot-experiments.d.ts +289 -0
- package/src/lib/hooks/useExperimentVariant.d.ts +21 -0
- package/src/lib/hooks/useExperiments.d.ts +14 -0
- package/src/lib/shared/{constants.ts → constants.d.ts} +18 -35
- package/src/lib/shared/mocks/mock.d.ts +43 -0
- package/src/lib/shared/{models.ts → models.d.ts} +2 -35
- package/src/lib/shared/parser/parser.d.ts +54 -0
- package/src/lib/shared/persistence/index-db-database-handler.d.ts +87 -0
- package/src/lib/shared/utils/DotLogger.d.ts +15 -0
- package/src/lib/shared/utils/memoize.d.ts +7 -0
- package/src/lib/shared/utils/utils.d.ts +73 -0
- package/src/lib/standalone.d.ts +7 -0
- package/.babelrc +0 -12
- package/.eslintrc.json +0 -26
- package/jest.config.ts +0 -11
- package/project.json +0 -55
- package/src/lib/components/DotExperimentsProvider.spec.tsx +0 -62
- package/src/lib/components/withExperiments.tsx +0 -52
- package/src/lib/contexts/DotExperimentsContext.spec.tsx +0 -42
- package/src/lib/dot-experiments.spec.ts +0 -285
- package/src/lib/dot-experiments.ts +0 -716
- package/src/lib/hooks/useExperimentVariant.spec.tsx +0 -111
- package/src/lib/hooks/useExperimentVariant.ts +0 -55
- package/src/lib/hooks/useExperiments.ts +0 -90
- package/src/lib/shared/mocks/mock.ts +0 -209
- package/src/lib/shared/parser/parse.spec.ts +0 -187
- package/src/lib/shared/parser/parser.ts +0 -171
- package/src/lib/shared/persistence/index-db-database-handler.spec.ts +0 -100
- package/src/lib/shared/persistence/index-db-database-handler.ts +0 -218
- package/src/lib/shared/utils/DotLogger.ts +0 -57
- package/src/lib/shared/utils/memoize.spec.ts +0 -49
- package/src/lib/shared/utils/memoize.ts +0 -49
- package/src/lib/shared/utils/utils.spec.ts +0 -142
- package/src/lib/shared/utils/utils.ts +0 -203
- package/src/lib/standalone.spec.ts +0 -36
- package/src/lib/standalone.ts +0 -28
- package/tsconfig.json +0 -20
- package/tsconfig.lib.json +0 -20
- package/tsconfig.spec.json +0 -9
- package/vite.config.ts +0 -41
- /package/src/{index.ts → index.d.ts} +0 -0
|
@@ -1,111 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,90 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,209 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { parseData, parseDataForAnalytics } from './parser';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
IsUserIncludedResponse,
|
|
5
|
-
LocationMock,
|
|
6
|
-
MOCK_CURRENT_TIMESTAMP,
|
|
7
|
-
MockDataStoredIndexDB,
|
|
8
|
-
MockDataStoredIndexDBWithNew,
|
|
9
|
-
MockDataStoredIndexDBWithNew15DaysLater,
|
|
10
|
-
NewIsUserIncludedResponse,
|
|
11
|
-
TIME_15_DAYS_MILLISECONDS,
|
|
12
|
-
TIME_5_DAYS_MILLISECONDS
|
|
13
|
-
} from '../mocks/mock';
|
|
14
|
-
import { Experiment, ExperimentParsed, FetchExperiments } from '../models';
|
|
15
|
-
|
|
16
|
-
const assignedExperiments: Experiment[] = IsUserIncludedResponse.entity.experiments;
|
|
17
|
-
|
|
18
|
-
const experimentMock = IsUserIncludedResponse.entity.experiments[0];
|
|
19
|
-
|
|
20
|
-
describe('Parsers', () => {
|
|
21
|
-
describe('parseData For Analytics', () => {
|
|
22
|
-
it('returns `isExperimentPage` true and `isTargetPage` false when location is /blog', () => {
|
|
23
|
-
const expectedURL = 'http://localhost/blog';
|
|
24
|
-
|
|
25
|
-
const expectedExperimentsParsed: ExperimentParsed = {
|
|
26
|
-
href: expectedURL,
|
|
27
|
-
experiments: [
|
|
28
|
-
{
|
|
29
|
-
experiment: experimentMock.id,
|
|
30
|
-
runningId: experimentMock.runningId,
|
|
31
|
-
variant: experimentMock.variant.name,
|
|
32
|
-
lookBackWindow: experimentMock.lookBackWindow.value,
|
|
33
|
-
isExperimentPage: true,
|
|
34
|
-
isTargetPage: false
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const location: Location = { ...LocationMock, href: expectedURL };
|
|
40
|
-
|
|
41
|
-
const parsedData: ExperimentParsed = parseDataForAnalytics(
|
|
42
|
-
assignedExperiments,
|
|
43
|
-
location
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
expect(parsedData).toStrictEqual(expectedExperimentsParsed);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('returns `isExperimentPage` false and `isTargetPage` true when location is /destinations', () => {
|
|
50
|
-
const expectedURL = 'http://localhost/destinations';
|
|
51
|
-
|
|
52
|
-
const expectedExperimentsParsed: ExperimentParsed = {
|
|
53
|
-
href: expectedURL,
|
|
54
|
-
experiments: [
|
|
55
|
-
{
|
|
56
|
-
experiment: experimentMock.id,
|
|
57
|
-
runningId: experimentMock.runningId,
|
|
58
|
-
variant: experimentMock.variant.name,
|
|
59
|
-
lookBackWindow: experimentMock.lookBackWindow.value,
|
|
60
|
-
isExperimentPage: false,
|
|
61
|
-
isTargetPage: true
|
|
62
|
-
}
|
|
63
|
-
]
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const location: Location = { ...LocationMock, href: expectedURL };
|
|
67
|
-
|
|
68
|
-
const parsedData: ExperimentParsed = parseDataForAnalytics(
|
|
69
|
-
assignedExperiments,
|
|
70
|
-
location
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
expect(parsedData).toStrictEqual(expectedExperimentsParsed);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('returns `isExperimentPage` false and `isTargetPage` false when location is /other-url', () => {
|
|
77
|
-
const expectedURL = 'http://localhost/other-url';
|
|
78
|
-
|
|
79
|
-
const expectedExperimentsParsed: ExperimentParsed = {
|
|
80
|
-
href: expectedURL,
|
|
81
|
-
experiments: [
|
|
82
|
-
{
|
|
83
|
-
experiment: experimentMock.id,
|
|
84
|
-
runningId: experimentMock.runningId,
|
|
85
|
-
variant: experimentMock.variant.name,
|
|
86
|
-
lookBackWindow: experimentMock.lookBackWindow.value,
|
|
87
|
-
isExperimentPage: false,
|
|
88
|
-
isTargetPage: false
|
|
89
|
-
}
|
|
90
|
-
]
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const location: Location = { ...LocationMock, href: expectedURL };
|
|
94
|
-
|
|
95
|
-
const parsedData: ExperimentParsed = parseDataForAnalytics(
|
|
96
|
-
assignedExperiments,
|
|
97
|
-
location
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
expect(parsedData).toStrictEqual(expectedExperimentsParsed);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe('parseData For Store', () => {
|
|
105
|
-
const mockNow = jest.spyOn(Date, 'now');
|
|
106
|
-
|
|
107
|
-
mockNow.mockImplementation(() => MOCK_CURRENT_TIMESTAMP);
|
|
108
|
-
|
|
109
|
-
beforeEach(() => {
|
|
110
|
-
jest.clearAllMocks();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should handle case where only NEW data is available', () => {
|
|
114
|
-
// First request, expire in now + experiment.lookBackWindow.expireMillis
|
|
115
|
-
const newData: FetchExperiments = {
|
|
116
|
-
experiments: IsUserIncludedResponse.entity.experiments,
|
|
117
|
-
excludedExperimentIdsEnded: []
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const dataFromIndexDB: Experiment[] | undefined = undefined;
|
|
121
|
-
|
|
122
|
-
const parsedData = parseData(newData, dataFromIndexDB);
|
|
123
|
-
|
|
124
|
-
expect(parsedData).toStrictEqual(MockDataStoredIndexDB);
|
|
125
|
-
expect(newData.experiments.length).toBe(parsedData.length);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should handle case where only OLD data is available', () => {
|
|
129
|
-
//No new request, not touch anything if not expired
|
|
130
|
-
|
|
131
|
-
const newData: FetchExperiments = {
|
|
132
|
-
experiments: [],
|
|
133
|
-
excludedExperimentIdsEnded: []
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const dataFromIndexDB: Experiment[] | undefined = MockDataStoredIndexDB;
|
|
137
|
-
|
|
138
|
-
const parsedData = parseData(newData, dataFromIndexDB);
|
|
139
|
-
|
|
140
|
-
expect(parsedData).toStrictEqual(MockDataStoredIndexDB);
|
|
141
|
-
expect(MockDataStoredIndexDB.length).toBe(parsedData.length);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('should handle case where both OLD and NEW data are available', () => {
|
|
145
|
-
//new request, stored data + new data. No delete anything only 5 days passed, 2 to store
|
|
146
|
-
|
|
147
|
-
const nowPlus5Days = MOCK_CURRENT_TIMESTAMP + TIME_5_DAYS_MILLISECONDS;
|
|
148
|
-
|
|
149
|
-
mockNow.mockImplementation(() => nowPlus5Days);
|
|
150
|
-
|
|
151
|
-
const newData: FetchExperiments = {
|
|
152
|
-
experiments: NewIsUserIncludedResponse.entity.experiments,
|
|
153
|
-
excludedExperimentIdsEnded: [
|
|
154
|
-
...NewIsUserIncludedResponse.entity.excludedExperimentIdsEnded
|
|
155
|
-
]
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const dataFromIndexDB: Experiment[] | undefined = MockDataStoredIndexDB;
|
|
159
|
-
|
|
160
|
-
const parsedData = parseData(newData, dataFromIndexDB);
|
|
161
|
-
|
|
162
|
-
expect(parsedData).toStrictEqual(MockDataStoredIndexDBWithNew);
|
|
163
|
-
|
|
164
|
-
expect(parsedData.length).toBe(MockDataStoredIndexDBWithNew.length);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should remove from stored experiment the experiments expired', () => {
|
|
168
|
-
// no new request, 15 days later, so expireTime is 15 days from MOCK_CURRENT_TIMESTAMP
|
|
169
|
-
// 1st experiment expired, so only 1 to store
|
|
170
|
-
const now15Days = MOCK_CURRENT_TIMESTAMP + TIME_15_DAYS_MILLISECONDS;
|
|
171
|
-
|
|
172
|
-
mockNow.mockImplementation(() => now15Days);
|
|
173
|
-
|
|
174
|
-
const newData: FetchExperiments = {
|
|
175
|
-
experiments: [],
|
|
176
|
-
excludedExperimentIdsEnded: []
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const dataFromIndexDB: Experiment[] | undefined = MockDataStoredIndexDBWithNew;
|
|
180
|
-
|
|
181
|
-
const parsedData = parseData(newData, dataFromIndexDB);
|
|
182
|
-
|
|
183
|
-
expect(parsedData.length).toBe(MockDataStoredIndexDBWithNew15DaysLater.length);
|
|
184
|
-
expect(parsedData).toStrictEqual(MockDataStoredIndexDBWithNew15DaysLater);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
});
|