@playpilot/tpi 5.5.1 → 5.6.0
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/dist/link-injections.js +8 -8
- package/events.md +2 -2
- package/package.json +1 -1
- package/src/lib/enums/SplitTest.ts +10 -0
- package/src/lib/linkInjection.ts +36 -2
- package/src/lib/splitTest.ts +19 -14
- package/src/routes/+layout.svelte +4 -0
- package/src/routes/+page.svelte +2 -1
- package/src/routes/components/DragHandle.svelte +105 -0
- package/src/routes/components/Modal.svelte +29 -6
- package/src/routes/components/TitleModal.svelte +0 -3
- package/src/tests/lib/splitTest.test.js +97 -18
package/events.md
CHANGED
|
@@ -64,5 +64,5 @@ Event | Action | Info | Payload
|
|
|
64
64
|
### Split Testing
|
|
65
65
|
Event | Action | Info | Payload
|
|
66
66
|
--- | --- | --- | ---
|
|
67
|
-
`ali_split_test_view` | _Should be fired for any active split test_ | | `key`, `variant` (
|
|
68
|
-
`ali_split_test_action` | _Should be fired for any assertion in split tests_ | | `key` (matches the key of `ali_split_test_view`), `variant` (
|
|
67
|
+
`ali_split_test_view` | _Should be fired for any active split test_ | | `key`, `variant` (a whole number starting at 0)
|
|
68
|
+
`ali_split_test_action` | _Should be fired for any assertion in split tests_ | | `key` (matches the key of `ali_split_test_view`), `variant` (a whole number starting at 0), `action`
|
package/package.json
CHANGED
package/src/lib/linkInjection.ts
CHANGED
|
@@ -219,7 +219,9 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
219
219
|
if (replacementIndex === -1) continue
|
|
220
220
|
if (hasBeenReplaced) continue
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
if (!replaceIfSafeInjection(element.innerHTML, injection.title, element, injectionElement, replacementIndex)) {
|
|
223
|
+
failedMessages[injection.key] = 'Injection would have lead to broken HTML.'
|
|
224
|
+
}
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
addLinkInjectionEventListeners(validInjections)
|
|
@@ -267,6 +269,31 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
267
269
|
return { injectionElement, linkElement }
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
/**
|
|
273
|
+
* In some coses injections lead to broken HTML. The reason for this varies and is hard to figure out. In any case, we
|
|
274
|
+
* should never serve broken HTML.
|
|
275
|
+
* Before replacing the HTML, we apply the the replacement to a dummy element. From here we check if the number of links
|
|
276
|
+
* in the HTML increased by one. If it didn't, something went wrong. What exactly is hard to say, we just know something
|
|
277
|
+
* didn't go as intended and we should not inject.
|
|
278
|
+
*
|
|
279
|
+
* No tests exists for this function because writing tests would require finding a scenario in which things break. If we
|
|
280
|
+
* knew when things break we'd fix it instead.
|
|
281
|
+
*/
|
|
282
|
+
function replaceIfSafeInjection(originalHtml: string, phrase: string, sentenceElement: HTMLElement, injectionElement: HTMLElement, replacementIndex: number): boolean {
|
|
283
|
+
const dummyElement = document.createElement('div')
|
|
284
|
+
dummyElement.innerHTML = originalHtml
|
|
285
|
+
|
|
286
|
+
const startingNumberOfLinks = Array.from(dummyElement.querySelectorAll<HTMLAnchorElement>('a')).filter(a => a.innerText).length
|
|
287
|
+
dummyElement.innerHTML = replaceStartingFrom(originalHtml, phrase, injectionElement.outerHTML, replacementIndex)
|
|
288
|
+
const finalNumberOfLinks = Array.from(dummyElement.querySelectorAll<HTMLAnchorElement>('a')).filter(a => a.innerText).length
|
|
289
|
+
|
|
290
|
+
if (finalNumberOfLinks != startingNumberOfLinks + 1) return false
|
|
291
|
+
|
|
292
|
+
sentenceElement.innerHTML = dummyElement.innerHTML
|
|
293
|
+
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
|
|
270
297
|
/**
|
|
271
298
|
* Add all used CSS variables to a data attribute. This data attribute is then used for selectors that for each
|
|
272
299
|
* individual CSS variable. This is done this way so that CSS variables are only set when they are used.
|
|
@@ -399,7 +426,14 @@ function openLinkPopover(event: MouseEvent, injection: LinkInjection): void {
|
|
|
399
426
|
* Unmount the popover, removing it from the dom
|
|
400
427
|
*/
|
|
401
428
|
function destroyLinkPopover(outro: boolean = true) {
|
|
402
|
-
if (!activePopoverInsertedComponent)
|
|
429
|
+
if (!activePopoverInsertedComponent) {
|
|
430
|
+
// In some cases a popover lingers even if it should have been removed. This happens sometimes during
|
|
431
|
+
// HMR during development. In that case we remove the element straight from the dom.
|
|
432
|
+
// Doing this will prevent the outro animation from playing, but this being a fallback, that's ok.
|
|
433
|
+
document.querySelectorAll<HTMLElement>('[data-playpilot-title-popover]').forEach(element => element.remove())
|
|
434
|
+
|
|
435
|
+
return
|
|
436
|
+
}
|
|
403
437
|
|
|
404
438
|
unmount(activePopoverInsertedComponent, { outro })
|
|
405
439
|
|
package/src/lib/splitTest.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { TrackingEvent } from "./enums/TrackingEvent"
|
|
2
2
|
import { track } from "./tracking"
|
|
3
3
|
|
|
4
|
+
type SplitTest = {
|
|
5
|
+
key: string
|
|
6
|
+
numberOfVariants: number
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
/**
|
|
5
10
|
* Each split test uses a different split test identifier so a user can be part of one, multiple, or none, tests.
|
|
6
11
|
* The identifier is saved on the window object so that it is consistent across this session.
|
|
7
12
|
*/
|
|
8
|
-
export function getSplitTestIdentifier(key:
|
|
13
|
+
export function getSplitTestIdentifier({ key }: SplitTest): number {
|
|
9
14
|
const windowIdentifier = window.PlayPilotLinkInjections?.split_test_identifiers?.[key]
|
|
10
15
|
if (windowIdentifier) return windowIdentifier
|
|
11
16
|
|
|
@@ -17,24 +22,24 @@ export function getSplitTestIdentifier(key: string): number {
|
|
|
17
22
|
return randomIdentifier
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const isInSplitTest = identifier > 0.5
|
|
25
|
+
export function getSplitTestVariantIndex(test: SplitTest): number {
|
|
26
|
+
const identifier = getSplitTestIdentifier(test)
|
|
27
|
+
|
|
28
|
+
return Math.floor(identifier * test.numberOfVariants)
|
|
29
|
+
}
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
export function isInSplitTestVariant(test: SplitTest, variant = 1): boolean {
|
|
32
|
+
return getSplitTestVariantIndex(test) === variant
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export function trackSplitTestView(
|
|
31
|
-
const variant =
|
|
35
|
+
export function trackSplitTestView(test: SplitTest): void {
|
|
36
|
+
const variant = getSplitTestVariantIndex(test)
|
|
32
37
|
|
|
33
|
-
track(TrackingEvent.SplitTestView, null, { key, variant })
|
|
38
|
+
track(TrackingEvent.SplitTestView, null, { key: test.key, variant })
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
export function trackSplitTestAction(
|
|
37
|
-
const variant =
|
|
41
|
+
export function trackSplitTestAction(test: SplitTest, action: string): void {
|
|
42
|
+
const variant = getSplitTestVariantIndex(test) ? 1 : 0
|
|
38
43
|
|
|
39
|
-
track(TrackingEvent.SplitTestAction, null, { key, variant, action })
|
|
44
|
+
track(TrackingEvent.SplitTestAction, null, { key: test.key, variant, action })
|
|
40
45
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { browser } from '$app/environment'
|
|
3
3
|
import { page } from '$app/state'
|
|
4
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
5
|
+
import { getSplitTestVariantIndex } from '$lib/splitTest'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* !! NOTE: This layout file is for development purposes only and will not be compiled with the final script.
|
|
@@ -75,6 +77,8 @@
|
|
|
75
77
|
|
|
76
78
|
{#if browser}
|
|
77
79
|
{@render children()}
|
|
80
|
+
|
|
81
|
+
Viewing with split test index: {getSplitTestVariantIndex(SplitTest.MultipleVariants)}
|
|
78
82
|
{/if}
|
|
79
83
|
{/key}
|
|
80
84
|
{/if}
|
package/src/routes/+page.svelte
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
9
9
|
import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/auth'
|
|
10
10
|
import { trackSplitTestView } from '$lib/splitTest'
|
|
11
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
11
12
|
import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
|
|
12
13
|
import Editor from './components/Editorial/Editor.svelte'
|
|
13
14
|
import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
await initialize()
|
|
43
44
|
track(TrackingEvent.ArticlePageView)
|
|
44
45
|
|
|
45
|
-
trackSplitTestView(
|
|
46
|
+
trackSplitTestView(SplitTest.MultipleVariants)
|
|
46
47
|
})()
|
|
47
48
|
|
|
48
49
|
return () => clearLinkInjections()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
target?: HTMLElement
|
|
4
|
+
threshold?: number
|
|
5
|
+
isDragging?: boolean
|
|
6
|
+
passedThreshold?: boolean
|
|
7
|
+
onpassed: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
target,
|
|
12
|
+
threshold = 100,
|
|
13
|
+
isDragging = $bindable(false),
|
|
14
|
+
passedThreshold = $bindable(false),
|
|
15
|
+
onpassed = () => null,
|
|
16
|
+
}: Props = $props()
|
|
17
|
+
|
|
18
|
+
let movementStartY = $state(0)
|
|
19
|
+
let movementCurrentY = $state(0)
|
|
20
|
+
let handleElement: HTMLElement | null = $state(null)
|
|
21
|
+
let hasBeenDragged = $state(false) // This is used to prevent the element from transitioning into view on mount
|
|
22
|
+
|
|
23
|
+
const difference = $derived(Math.max(movementCurrentY - movementStartY, 0))
|
|
24
|
+
|
|
25
|
+
$effect(() => {
|
|
26
|
+
passedThreshold = difference > threshold
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export function start(event: TouchEvent): void {
|
|
30
|
+
if (!event) return
|
|
31
|
+
if (!target) return
|
|
32
|
+
|
|
33
|
+
isDragging = true
|
|
34
|
+
hasBeenDragged = true
|
|
35
|
+
|
|
36
|
+
movementStartY = event.touches[0].pageY
|
|
37
|
+
target.style.transitionDuration = '0ms'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function move(event: TouchEvent): void {
|
|
41
|
+
if (!event) return
|
|
42
|
+
if (!target) return
|
|
43
|
+
if (!isDragging) return
|
|
44
|
+
|
|
45
|
+
movementCurrentY = event.touches[0].pageY
|
|
46
|
+
|
|
47
|
+
target.style.transform = `translateY(${difference}px)`
|
|
48
|
+
handleElement!.style.transform = `translateY(${difference}px)`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function end(): void {
|
|
52
|
+
if (!target) return
|
|
53
|
+
if (!isDragging) return
|
|
54
|
+
|
|
55
|
+
isDragging = false
|
|
56
|
+
|
|
57
|
+
if (passedThreshold) {
|
|
58
|
+
onpassed()
|
|
59
|
+
} else {
|
|
60
|
+
target.removeAttribute('style')
|
|
61
|
+
handleElement!.removeAttribute('style')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
movementStartY = 0
|
|
65
|
+
movementCurrentY = 0
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<svelte:window ontouchmove={move} ontouchend={end} />
|
|
70
|
+
|
|
71
|
+
<div class="handle" class:dragging={isDragging} class:dragged={hasBeenDragged} bind:this={handleElement} ontouchstart={start}></div>
|
|
72
|
+
|
|
73
|
+
<style lang="scss">
|
|
74
|
+
.handle {
|
|
75
|
+
position: relative;
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: var(--playpilot-drag-handle-height, margin(3));
|
|
78
|
+
|
|
79
|
+
&::before {
|
|
80
|
+
content: '';
|
|
81
|
+
display: block;
|
|
82
|
+
position: absolute;
|
|
83
|
+
top: calc(50% - (var(--playpilot-drag-handle-bar-height, margin(0.25)) / 2));
|
|
84
|
+
width: 100%;
|
|
85
|
+
height: var(--playpilot-drag-handle-bar-height, margin(0.25));
|
|
86
|
+
border-radius: margin(1);
|
|
87
|
+
background: var(--playpilot-drag-handle-color, currentColor);
|
|
88
|
+
opacity: 0.65;
|
|
89
|
+
transition: opacity 100ms, scale 100ms;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&.dragged {
|
|
93
|
+
transition: transform 200ms;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&.dragging {
|
|
97
|
+
transition: none;
|
|
98
|
+
|
|
99
|
+
&::before {
|
|
100
|
+
scale: 1.5 1;
|
|
101
|
+
opacity: 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { fade, fly, scale, type TransitionConfig } from 'svelte/transition'
|
|
3
3
|
import IconClose from './Icons/IconClose.svelte'
|
|
4
4
|
import RoundButton from './RoundButton.svelte'
|
|
5
|
+
import DragHandle from './DragHandle.svelte'
|
|
5
6
|
import { onMount, setContext, type Snippet } from 'svelte'
|
|
6
7
|
import { prefersReducedMotion } from 'svelte/motion'
|
|
7
8
|
|
|
@@ -13,6 +14,15 @@
|
|
|
13
14
|
|
|
14
15
|
const { children, onclose = () => null, onscroll = () => null }: Props = $props()
|
|
15
16
|
|
|
17
|
+
let windowWidth = $state(0)
|
|
18
|
+
let dialogElement: HTMLElement | null = $state(null)
|
|
19
|
+
let dragHandleOffset: number = $state(0)
|
|
20
|
+
|
|
21
|
+
const isMobile = $derived(windowWidth < 600)
|
|
22
|
+
|
|
23
|
+
$effect(() => { if (windowWidth) dragHandleOffset = dialogElement?.offsetTop || 0 })
|
|
24
|
+
$effect(() => { setTimeout(() => dragHandleOffset = dialogElement?.offsetTop || 0) }) // Set after the dialog has shown to get the proper height
|
|
25
|
+
|
|
16
26
|
setContext('scope', 'modal')
|
|
17
27
|
|
|
18
28
|
onMount(() => {
|
|
@@ -25,17 +35,21 @@
|
|
|
25
35
|
function scaleOrFly(node: Element): TransitionConfig {
|
|
26
36
|
if (prefersReducedMotion.current) return fade(node, { duration: 0 })
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (shouldFly) return fly(node, { duration: 250, y: window.innerHeight })
|
|
38
|
+
if (isMobile) return fly(node, { duration: 250, y: window.innerHeight })
|
|
31
39
|
return scale(node, { duration: 150, start: 0.85 })
|
|
32
40
|
}
|
|
33
41
|
</script>
|
|
34
42
|
|
|
35
|
-
<svelte:window
|
|
43
|
+
<svelte:window onkeydown={({ key }) => { if (key === 'Escape') onclose() }} bind:innerWidth={windowWidth} />
|
|
36
44
|
|
|
37
45
|
<div class="modal" transition:fade|global={{ duration: 150 }}>
|
|
38
|
-
|
|
46
|
+
{#if isMobile}
|
|
47
|
+
<div class="drag-handle" style:top="{dragHandleOffset}px" transition:scaleOrFly|global>
|
|
48
|
+
<DragHandle target={dialogElement!} onpassed={() => onclose()} />
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
51
|
+
|
|
52
|
+
<div class="dialog" {onscroll} bind:this={dialogElement} role="dialog" aria-labelledby="title" transition:scaleOrFly|global data-view-transition-new>
|
|
39
53
|
<div class="close">
|
|
40
54
|
<RoundButton onclick={() => onclose()}>
|
|
41
55
|
<IconClose />
|
|
@@ -80,6 +94,7 @@
|
|
|
80
94
|
margin-top: auto;
|
|
81
95
|
border-radius: var(--playpilot-detail-border-radius, margin(1) margin(1) 0 0);
|
|
82
96
|
background: var(--playpilot-detail-background, var(--playpilot-light));
|
|
97
|
+
transition: transform 200ms;
|
|
83
98
|
|
|
84
99
|
@media (min-width: 600px) {
|
|
85
100
|
margin-top: 0;
|
|
@@ -88,7 +103,6 @@
|
|
|
88
103
|
}
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
|
|
92
106
|
.backdrop {
|
|
93
107
|
z-index: 0;
|
|
94
108
|
position: absolute;
|
|
@@ -98,6 +112,15 @@
|
|
|
98
112
|
left: 0;
|
|
99
113
|
}
|
|
100
114
|
|
|
115
|
+
.drag-handle {
|
|
116
|
+
z-index: 5;
|
|
117
|
+
position: absolute;
|
|
118
|
+
left: 50%;
|
|
119
|
+
transform: translateY(margin(-0.5)) translateX(-50%);
|
|
120
|
+
width: margin(7);
|
|
121
|
+
color: var(--playpilot-text-color);
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
.close {
|
|
102
125
|
--playpilot-button-background: var(--playpilot-modal-close-button-background, var(--playpilot-content));
|
|
103
126
|
--playpilot-button-shadow: var(--playpilot-modal-close-button-shadow, var(--playpilot-shadow));
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
3
|
import { track } from '$lib/tracking'
|
|
4
4
|
import type { TitleData } from '$lib/types/title'
|
|
5
|
-
import { trackSplitTestAction } from '$lib/splitTest'
|
|
6
5
|
import { onMount } from 'svelte'
|
|
7
6
|
import Modal from './Modal.svelte'
|
|
8
7
|
import Title from './Title.svelte'
|
|
@@ -21,8 +20,6 @@
|
|
|
21
20
|
onMount(() => {
|
|
22
21
|
const openTimestamp = Date.now()
|
|
23
22
|
|
|
24
|
-
trackSplitTestAction('initial_test', 'modal')
|
|
25
|
-
|
|
26
23
|
return () => track(TrackingEvent.TitleModalClose, title, { time_spent: Date.now() - openTimestamp })
|
|
27
24
|
})
|
|
28
25
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
-
import { getSplitTestIdentifier,
|
|
2
|
+
import { getSplitTestIdentifier, getSplitTestVariantIndex, isInSplitTestVariant, trackSplitTestAction, trackSplitTestView } from '$lib/splitTest'
|
|
3
3
|
import { track } from '$lib/tracking'
|
|
4
4
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
5
5
|
|
|
@@ -8,6 +8,11 @@ vi.mock('$lib/tracking', () => ({
|
|
|
8
8
|
}))
|
|
9
9
|
|
|
10
10
|
describe('$lib/splitTest', () => {
|
|
11
|
+
const splitTest = {
|
|
12
|
+
key: 'Some key',
|
|
13
|
+
numberOfVariants: 2,
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
describe('getSplitTestIdentifier', () => {
|
|
12
17
|
afterEach(() => {
|
|
13
18
|
// @ts-ignore
|
|
@@ -20,11 +25,11 @@ describe('$lib/splitTest', () => {
|
|
|
20
25
|
// @ts-ignore
|
|
21
26
|
window.PlayPilotLinkInjections = {}
|
|
22
27
|
|
|
23
|
-
const value = getSplitTestIdentifier(
|
|
28
|
+
const value = getSplitTestIdentifier(splitTest)
|
|
24
29
|
|
|
25
|
-
expect(window.PlayPilotLinkInjections.split_test_identifiers).toEqual({
|
|
30
|
+
expect(window.PlayPilotLinkInjections.split_test_identifiers).toEqual({ [splitTest.key]: value })
|
|
26
31
|
// @ts-ignore
|
|
27
|
-
expect(value).toBe(window.PlayPilotLinkInjections.split_test_identifiers[
|
|
32
|
+
expect(value).toBe(window.PlayPilotLinkInjections.split_test_identifiers[splitTest.key])
|
|
28
33
|
})
|
|
29
34
|
|
|
30
35
|
it('Should return value as stored in the window object', () => {
|
|
@@ -32,46 +37,69 @@ describe('$lib/splitTest', () => {
|
|
|
32
37
|
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
33
38
|
window.PlayPilotLinkInjections.split_test_identifiers['Some key'] = 0.5
|
|
34
39
|
|
|
35
|
-
expect(getSplitTestIdentifier(
|
|
40
|
+
expect(getSplitTestIdentifier(splitTest)).toBe(0.5)
|
|
36
41
|
})
|
|
37
42
|
|
|
38
43
|
it('Should set separate value for different keys', () => {
|
|
39
44
|
// @ts-ignore
|
|
40
45
|
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
41
46
|
|
|
42
|
-
getSplitTestIdentifier(
|
|
43
|
-
getSplitTestIdentifier('Some other key')
|
|
47
|
+
getSplitTestIdentifier(splitTest)
|
|
48
|
+
getSplitTestIdentifier({ key: 'Some other key', numberOfVariants: 2 })
|
|
44
49
|
|
|
45
50
|
expect(window.PlayPilotLinkInjections.split_test_identifiers).toEqual({
|
|
46
|
-
|
|
51
|
+
[splitTest.key]: expect.any(Number),
|
|
47
52
|
'Some other key': expect.any(Number),
|
|
48
53
|
})
|
|
49
54
|
})
|
|
50
55
|
})
|
|
51
56
|
|
|
52
|
-
describe('
|
|
57
|
+
describe('isInSplitTestVariant', () => {
|
|
53
58
|
it('Should return true if stored value is higher than 0.5', () => {
|
|
54
59
|
// @ts-ignore
|
|
55
60
|
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
56
|
-
window.PlayPilotLinkInjections.split_test_identifiers[
|
|
61
|
+
window.PlayPilotLinkInjections.split_test_identifiers[splitTest.key] = 0.75
|
|
57
62
|
|
|
58
|
-
expect(
|
|
63
|
+
expect(isInSplitTestVariant(splitTest)).toBe(true)
|
|
59
64
|
})
|
|
60
65
|
|
|
61
66
|
it('Should return false if stored value is lower than 0.5', () => {
|
|
62
67
|
// @ts-ignore
|
|
63
68
|
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
64
|
-
window.PlayPilotLinkInjections.split_test_identifiers[
|
|
69
|
+
window.PlayPilotLinkInjections.split_test_identifiers[splitTest.key] = 0.25
|
|
70
|
+
|
|
71
|
+
expect(isInSplitTestVariant(splitTest)).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('Should return correct values when numberOfVariants is given based on the stored value', () => {
|
|
75
|
+
const multiple = { key: 'multiple', numberOfVariants: 3 }
|
|
76
|
+
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
79
|
+
window.PlayPilotLinkInjections.split_test_identifiers[multiple.key] = 0.25
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
expect(isInSplitTestVariant(multiple, 0)).toBe(true)
|
|
83
|
+
expect(isInSplitTestVariant(multiple, 1)).toBe(false)
|
|
84
|
+
expect(isInSplitTestVariant(multiple, 2)).toBe(false)
|
|
65
85
|
|
|
66
|
-
|
|
86
|
+
window.PlayPilotLinkInjections.split_test_identifiers['multiple'] = 0.5
|
|
87
|
+
expect(isInSplitTestVariant(multiple, 0)).toBe(false)
|
|
88
|
+
expect(isInSplitTestVariant(multiple, 1)).toBe(true)
|
|
89
|
+
expect(isInSplitTestVariant(multiple, 2)).toBe(false)
|
|
90
|
+
|
|
91
|
+
window.PlayPilotLinkInjections.split_test_identifiers['multiple'] = 0.75
|
|
92
|
+
expect(isInSplitTestVariant(multiple, 0)).toBe(false)
|
|
93
|
+
expect(isInSplitTestVariant(multiple, 1)).toBe(false)
|
|
94
|
+
expect(isInSplitTestVariant(multiple, 2)).toBe(true)
|
|
67
95
|
})
|
|
68
96
|
})
|
|
69
97
|
|
|
70
98
|
describe('trackSplitTestView', () => {
|
|
71
99
|
it('Should track view event with the given key', () => {
|
|
72
|
-
trackSplitTestView(
|
|
100
|
+
trackSplitTestView(splitTest)
|
|
73
101
|
|
|
74
|
-
const variant =
|
|
102
|
+
const variant = isInSplitTestVariant(splitTest) ? 1 : 0
|
|
75
103
|
|
|
76
104
|
expect(track).toHaveBeenCalledWith(TrackingEvent.SplitTestView, null, { key: 'Some key', variant })
|
|
77
105
|
})
|
|
@@ -79,10 +107,61 @@ describe('$lib/splitTest', () => {
|
|
|
79
107
|
|
|
80
108
|
describe('trackSplitTestAction', () => {
|
|
81
109
|
it('Should track action event with the given key and action', () => {
|
|
82
|
-
trackSplitTestAction(
|
|
83
|
-
const variant =
|
|
110
|
+
trackSplitTestAction(splitTest, 'Some action')
|
|
111
|
+
const variant = isInSplitTestVariant(splitTest) ? 1 : 0
|
|
112
|
+
|
|
113
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.SplitTestAction, null, { key: splitTest.key, variant, action: 'Some action' })
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('getSplitTestVariantIndex', () => {
|
|
118
|
+
it('Should return the expected index split test consists of 2 variants', () => {
|
|
119
|
+
// @ts-ignore
|
|
120
|
+
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
121
|
+
|
|
122
|
+
window.PlayPilotLinkInjections.split_test_identifiers[splitTest.key] = 0.25
|
|
123
|
+
expect(getSplitTestVariantIndex(splitTest)).toBe(0)
|
|
124
|
+
|
|
125
|
+
window.PlayPilotLinkInjections.split_test_identifiers[splitTest.key] = 0.75
|
|
126
|
+
expect(getSplitTestVariantIndex(splitTest)).toBe(1)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('Should return the expected index when split test includes more than 2 variants', () => {
|
|
130
|
+
const multiple = { key: 'multiple', numberOfVariants: 3 }
|
|
131
|
+
|
|
132
|
+
// @ts-ignore
|
|
133
|
+
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
134
|
+
|
|
135
|
+
window.PlayPilotLinkInjections.split_test_identifiers[multiple.key] = 0.25
|
|
136
|
+
expect(getSplitTestVariantIndex(multiple)).toBe(0)
|
|
137
|
+
|
|
138
|
+
window.PlayPilotLinkInjections.split_test_identifiers[multiple.key] = 0.5
|
|
139
|
+
expect(getSplitTestVariantIndex(multiple)).toBe(1)
|
|
140
|
+
|
|
141
|
+
window.PlayPilotLinkInjections.split_test_identifiers[multiple.key] = 0.75
|
|
142
|
+
expect(getSplitTestVariantIndex(multiple)).toBe(2)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('Should return the expected index when split test includes a large amount of variants', () => {
|
|
146
|
+
const many = { key: 'many', numberOfVariants: 10 }
|
|
147
|
+
|
|
148
|
+
// @ts-ignore
|
|
149
|
+
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
150
|
+
|
|
151
|
+
window.PlayPilotLinkInjections.split_test_identifiers[many.key] = 0.05
|
|
152
|
+
expect(getSplitTestVariantIndex(many)).toBe(0)
|
|
153
|
+
|
|
154
|
+
window.PlayPilotLinkInjections.split_test_identifiers[many.key] = 0.35
|
|
155
|
+
expect(getSplitTestVariantIndex(many)).toBe(3)
|
|
156
|
+
|
|
157
|
+
window.PlayPilotLinkInjections.split_test_identifiers[many.key] = 0.65
|
|
158
|
+
expect(getSplitTestVariantIndex(many)).toBe(6)
|
|
159
|
+
|
|
160
|
+
window.PlayPilotLinkInjections.split_test_identifiers[many.key] = 0.78
|
|
161
|
+
expect(getSplitTestVariantIndex(many)).toBe(7)
|
|
84
162
|
|
|
85
|
-
|
|
163
|
+
window.PlayPilotLinkInjections.split_test_identifiers[many.key] = 0.98
|
|
164
|
+
expect(getSplitTestVariantIndex(many)).toBe(9)
|
|
86
165
|
})
|
|
87
166
|
})
|
|
88
167
|
})
|