@playpilot/tpi 5.31.3 → 5.32.0-beta.1
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 +11 -11
- package/package.json +1 -1
- package/src/lib/data/translations.ts +5 -0
- package/src/lib/enums/SplitTest.ts +5 -0
- package/src/lib/injection.ts +35 -0
- package/src/lib/scss/global.scss +39 -0
- package/src/routes/+page.svelte +13 -2
- package/src/routes/components/Debugger.svelte +8 -2
- package/src/routes/components/HighlightedInjection.svelte +222 -0
- package/src/routes/components/RoundButton.svelte +5 -4
- package/src/tests/routes/+page.test.js +2 -0
- package/src/tests/routes/components/HighlightedInjection.test.js +98 -0
package/package.json
CHANGED
|
@@ -61,6 +61,11 @@ export const translations = {
|
|
|
61
61
|
[Language.Swedish]: 'minuter',
|
|
62
62
|
[Language.Danish]: 'minutter',
|
|
63
63
|
},
|
|
64
|
+
'Minutes Short': {
|
|
65
|
+
[Language.English]: 'min.',
|
|
66
|
+
[Language.Swedish]: 'min.',
|
|
67
|
+
[Language.Danish]: 'min.',
|
|
68
|
+
},
|
|
64
69
|
'Type: movie': {
|
|
65
70
|
[Language.English]: 'Movie',
|
|
66
71
|
[Language.Swedish]: 'Film',
|
|
@@ -9,4 +9,9 @@ export const SplitTest = {
|
|
|
9
9
|
numberOfVariants: 2,
|
|
10
10
|
variantNames: ['Image', 'Label'] as string[],
|
|
11
11
|
},
|
|
12
|
+
InTextEngagement: {
|
|
13
|
+
key: 'in-text-engagement',
|
|
14
|
+
numberOfVariants: 4,
|
|
15
|
+
variantNames: ['Default', '(i) Icon', 'Providers', 'Highlight'] as string[]
|
|
16
|
+
}
|
|
12
17
|
} as const
|
package/src/lib/injection.ts
CHANGED
|
@@ -13,6 +13,9 @@ import { destroyAllModals, openModal } from './modal'
|
|
|
13
13
|
import { track } from './tracking'
|
|
14
14
|
import { TrackingEvent } from './enums/TrackingEvent'
|
|
15
15
|
import InTextDisclaimer from '../routes/components/InTextDisclaimer.svelte'
|
|
16
|
+
import { getSplitTestVariantName, trackSplitTestAction } from './splitTest'
|
|
17
|
+
import { SplitTest } from './enums/SplitTest'
|
|
18
|
+
import { mergePlaylinks } from './playlink'
|
|
16
19
|
|
|
17
20
|
const keyDataAttribute = 'data-playpilot-injection-key'
|
|
18
21
|
const keySelector = `[${keyDataAttribute}]`
|
|
@@ -307,6 +310,33 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
307
310
|
linkElement.target = '_blank'
|
|
308
311
|
linkElement.rel = 'noopener nofollow noreferrer'
|
|
309
312
|
|
|
313
|
+
// Part of a split test, insert an absolute positioned (i) icon in the top right of links.
|
|
314
|
+
if (getSplitTestVariantName(SplitTest.InTextEngagement) === '(i) Icon') {
|
|
315
|
+
const iconElement = document.createElement('span')
|
|
316
|
+
iconElement.classList.add('playpilot-injection-info-icon')
|
|
317
|
+
iconElement.dataset.playpilotElement = 'true'
|
|
318
|
+
|
|
319
|
+
linkElement.insertAdjacentElement('beforeend', iconElement)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Part of a split test, insert a list of max 3 provider icons after the link, these are inline and cause page shift.
|
|
323
|
+
if (getSplitTestVariantName(SplitTest.InTextEngagement) === 'Providers') {
|
|
324
|
+
const size = parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.875 // 0.875 rem, 14 pixels if 1rem is 16 pixels
|
|
325
|
+
|
|
326
|
+
for (const provider of mergePlaylinks(injection.title_details!.providers).slice(0, 3)) {
|
|
327
|
+
const imageElement = document.createElement('img')
|
|
328
|
+
imageElement.loading = 'lazy'
|
|
329
|
+
imageElement.src = provider.logo_url
|
|
330
|
+
imageElement.alt = '' // Intentionally ignore the image or TTS would make a mess of the sentence
|
|
331
|
+
imageElement.width = size
|
|
332
|
+
imageElement.height = size
|
|
333
|
+
imageElement.dataset.playpilotElement = 'true'
|
|
334
|
+
imageElement.classList.add('playpilot-injection-provider-icon')
|
|
335
|
+
|
|
336
|
+
linkElement.insertAdjacentElement('beforeend', imageElement)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
310
340
|
injectionElement.insertAdjacentElement('beforeend', linkElement)
|
|
311
341
|
|
|
312
342
|
return { injectionElement, linkElement }
|
|
@@ -422,6 +452,8 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
422
452
|
|
|
423
453
|
event.preventDefault()
|
|
424
454
|
|
|
455
|
+
trackSplitTestAction(SplitTest.InTextEngagement, 'click')
|
|
456
|
+
|
|
425
457
|
playFallbackViewTransition(() => {
|
|
426
458
|
destroyLinkPopover(false)
|
|
427
459
|
openModal({ event, injection, data: injection.title_details })
|
|
@@ -602,6 +634,9 @@ export function clearLinkInjection(key: string): void {
|
|
|
602
634
|
const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
|
|
603
635
|
if (!element) return
|
|
604
636
|
|
|
637
|
+
const playpilotElements = element.querySelectorAll('[data-playpilot-element]')
|
|
638
|
+
playpilotElements.forEach(element => element.remove())
|
|
639
|
+
|
|
605
640
|
const linkContent = element.querySelector('a')?.innerHTML
|
|
606
641
|
element.outerHTML = linkContent || ''
|
|
607
642
|
}
|
package/src/lib/scss/global.scss
CHANGED
|
@@ -11,6 +11,45 @@
|
|
|
11
11
|
position: relative;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.playpilot-injection-info-icon {
|
|
15
|
+
position: relative;
|
|
16
|
+
display: inline-block;
|
|
17
|
+
height: 1em;
|
|
18
|
+
|
|
19
|
+
&::before {
|
|
20
|
+
content: "i";
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
align-items: center;
|
|
24
|
+
position: absolute;
|
|
25
|
+
top: -0.25em;
|
|
26
|
+
right: -1em;
|
|
27
|
+
width: 0.75em;
|
|
28
|
+
height: 0.75em;
|
|
29
|
+
border-radius: 50%;
|
|
30
|
+
box-shadow: 0 0 0 1px currentColor;
|
|
31
|
+
line-height: 1;
|
|
32
|
+
font-size: 0.65em;
|
|
33
|
+
text-align: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
h1 &, h2 &, h3 &, h4 &, h5 &, h6 & {
|
|
37
|
+
display: none
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.playpilot-injection-provider-icon {
|
|
42
|
+
display: inline-block;
|
|
43
|
+
margin-left: margin(0.25);
|
|
44
|
+
border-radius: 2px;
|
|
45
|
+
box-shadow: 0 0 1px rgba(0, 0, 0, 0.25);
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
|
|
48
|
+
h1 &, h2 &, h3 &, h4 &, h5 &, h6 & {
|
|
49
|
+
display: none
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
14
53
|
.playpilot-injection-highlight {
|
|
15
54
|
outline: margin(0.25) solid var(--playpilot-primary) !important;
|
|
16
55
|
outline-offset: margin(0.5) !important;
|
package/src/routes/+page.svelte
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { fetchAds } from '$lib/api/ads'
|
|
10
10
|
import { fetchConfig } from '$lib/api/config'
|
|
11
11
|
import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/api/auth'
|
|
12
|
+
import { getSplitTestVariantName, trackSplitTestView } from '$lib/splitTest'
|
|
13
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
12
14
|
import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
|
|
13
15
|
import Editor from './components/Editorial/Editor.svelte'
|
|
14
16
|
import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
|
|
@@ -16,6 +18,7 @@
|
|
|
16
18
|
import TrackingPixels from './components/TrackingPixels.svelte'
|
|
17
19
|
import Consent from './components/Consent.svelte'
|
|
18
20
|
import Debugger from './components/Debugger.svelte'
|
|
21
|
+
import HighlightedInjection from './components/HighlightedInjection.svelte'
|
|
19
22
|
|
|
20
23
|
let parentElement: HTMLElement | null = $state(null)
|
|
21
24
|
let elements: HTMLElement[] = $state([])
|
|
@@ -54,7 +57,11 @@
|
|
|
54
57
|
fireQueuedTrackingEvents()
|
|
55
58
|
track(TrackingEvent.ArticlePageView)
|
|
56
59
|
|
|
57
|
-
if (aiInjections.length
|
|
60
|
+
if (!aiInjections.length && !manualInjections.length) return
|
|
61
|
+
|
|
62
|
+
window.PlayPilotLinkInjections.ads = await fetchAds()
|
|
63
|
+
|
|
64
|
+
trackSplitTestView(SplitTest.InTextEngagement)
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
async function initialize(): Promise<void> {
|
|
@@ -225,7 +232,11 @@
|
|
|
225
232
|
</svelte:boundary>
|
|
226
233
|
{/if}
|
|
227
234
|
|
|
228
|
-
|
|
235
|
+
{#if linkInjections.length && getSplitTestVariantName(SplitTest.InTextEngagement) === 'Highlight'}
|
|
236
|
+
<HighlightedInjection {linkInjections} />
|
|
237
|
+
{/if}
|
|
238
|
+
|
|
239
|
+
<Debugger onrerender={rerender} />
|
|
229
240
|
</div>
|
|
230
241
|
|
|
231
242
|
{#if response?.pixels?.length}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import { getFullUrlPath } from '$lib/url'
|
|
4
4
|
import { onDestroy } from 'svelte'
|
|
5
5
|
|
|
6
|
+
interface Props {
|
|
7
|
+
onrerender: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { onrerender }: Props = $props()
|
|
11
|
+
|
|
6
12
|
const secrets = ['tpidebug', 'debugtpi']
|
|
7
13
|
const lastInputs: string[] = []
|
|
8
14
|
const isUsingBetaScript = !!document.querySelector('script[src*="scripts.playpilot.com/link-injection@next"]')
|
|
@@ -92,8 +98,6 @@
|
|
|
92
98
|
newScriptTag.setAttribute(attribute.name, attribute.value)
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
console.log(newScriptTag, newScriptTag.getAttribute('onload'))
|
|
96
|
-
|
|
97
101
|
// If no onload attribute is given we add our own and load it straight up. This can happen when the third party loads their
|
|
98
102
|
// script through other means. That's fine, this beta script is meant to be an approximation and how they load the script
|
|
99
103
|
// exactly does not matter in this case.
|
|
@@ -153,6 +157,8 @@
|
|
|
153
157
|
|
|
154
158
|
<hr />
|
|
155
159
|
|
|
160
|
+
<button onclick={onrerender}>Re-inject</button>
|
|
161
|
+
|
|
156
162
|
{#if isUsingBetaScript}
|
|
157
163
|
<small>You are using the beta version of TPI</small>
|
|
158
164
|
{:else}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { filterRemovedAndInactiveInjections, sortInjections } from '$lib/injection'
|
|
3
|
+
import { t } from '$lib/localization'
|
|
4
|
+
import { titleUrl } from '$lib/routes'
|
|
5
|
+
import { cleanPhrase } from '$lib/text'
|
|
6
|
+
import type { LinkInjection } from '$lib/types/injection'
|
|
7
|
+
import { openModal } from '$lib/modal'
|
|
8
|
+
import { trackSplitTestAction } from '$lib/splitTest'
|
|
9
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
10
|
+
import { scale } from 'svelte/transition'
|
|
11
|
+
import IconIMDb from './Icons/IconIMDb.svelte'
|
|
12
|
+
import TitlePoster from './TitlePoster.svelte'
|
|
13
|
+
import RoundButton from './RoundButton.svelte'
|
|
14
|
+
import IconClose from './Icons/IconClose.svelte'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
linkInjections: LinkInjection[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { linkInjections }: Props = $props()
|
|
21
|
+
|
|
22
|
+
const scrollThreshold = Math.min(window.innerHeight, 800)
|
|
23
|
+
|
|
24
|
+
const filteredInjections = $derived(filterRemovedAndInactiveInjections(linkInjections))
|
|
25
|
+
const sortedInjections: LinkInjection[] = $derived(sortInjections(filteredInjections))
|
|
26
|
+
|
|
27
|
+
// Get title injection that might relate the most the overall article.
|
|
28
|
+
// If any is found it is used as the primary display, highlighting that injection over others.
|
|
29
|
+
// If none are found we simply use the first active injection and hope for the best.
|
|
30
|
+
const primaryInjection = $derived.by(() => {
|
|
31
|
+
const pageTitle = cleanPhrase(document.querySelector('h1')?.innerText || document.title || '')
|
|
32
|
+
return sortedInjections.find(injection => pageTitle.includes(cleanPhrase(injection.title))) || filteredInjections[0]
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
let shown = $state(false)
|
|
36
|
+
let closed = $state(false)
|
|
37
|
+
let offsetBottom = $state(0)
|
|
38
|
+
|
|
39
|
+
// Only show the element once the user has scroll past a threshold. From here we check the offset from the bottom of
|
|
40
|
+
// the page to make sure we don't overlay the element on top of other elements on the page, such as ads or menus
|
|
41
|
+
function onscroll(): void {
|
|
42
|
+
if (shown || closed) return
|
|
43
|
+
if (window.scrollY < scrollThreshold) return
|
|
44
|
+
|
|
45
|
+
const bottomFixedElementOffsets = getBottomFixedElementOffsets()
|
|
46
|
+
const largestOffset = Math.max(...bottomFixedElementOffsets, 0)
|
|
47
|
+
|
|
48
|
+
offsetBottom = largestOffset + parseFloat(getComputedStyle(document.documentElement).fontSize) // 1 rem
|
|
49
|
+
shown = true
|
|
50
|
+
|
|
51
|
+
trackSplitTestAction(SplitTest.InTextEngagement, 'shown')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get all elements that are seemingly fixed to the bottom. This is used to determine the position of the element to try
|
|
55
|
+
// and make sure it does not overlap ads or other elements at the bottom of the screen.
|
|
56
|
+
// This is determined by them being fixed (duh) and being close to the bottom of the screen. This might turn out to be unreliable.
|
|
57
|
+
// It does not account for elements that are added after this function is called, which is often the case for ads.
|
|
58
|
+
function getBottomFixedElementOffsets(): number[] {
|
|
59
|
+
const fixedElements = Array.from(document.querySelectorAll('*')).filter((element) => {
|
|
60
|
+
const style = getComputedStyle(element)
|
|
61
|
+
return style.position === 'fixed' && window.innerHeight - element.getBoundingClientRect().bottom < 50
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return fixedElements.map((element) => element.clientHeight + (window.innerHeight - element.getBoundingClientRect().bottom))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onclick(event: MouseEvent): void {
|
|
68
|
+
event.preventDefault()
|
|
69
|
+
|
|
70
|
+
closed = true
|
|
71
|
+
|
|
72
|
+
openModal({ event, injection: primaryInjection, data: primaryInjection.title_details })
|
|
73
|
+
trackSplitTestAction(SplitTest.InTextEngagement, 'click-highlight')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function close(event: MouseEvent): void {
|
|
77
|
+
event.preventDefault()
|
|
78
|
+
|
|
79
|
+
closed = true
|
|
80
|
+
|
|
81
|
+
trackSplitTestAction(SplitTest.InTextEngagement, 'close')
|
|
82
|
+
}
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<svelte:window {onscroll} />
|
|
86
|
+
|
|
87
|
+
{#if primaryInjection && shown && !closed}
|
|
88
|
+
{@const title = primaryInjection.title_details!}
|
|
89
|
+
|
|
90
|
+
<div class="highlighted-injection" style:bottom="{offsetBottom}px" in:scale={{ start: 0.85, duration: 100 }}>
|
|
91
|
+
<a {onclick} class="link" href={titleUrl(title)} target="_blank" aria-label="{title.title} (opens in a new tab)">
|
|
92
|
+
<div class="poster">
|
|
93
|
+
<TitlePoster {title} width={30} height={43} />
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="content">
|
|
97
|
+
<div class="details">
|
|
98
|
+
<div class="heading">{title.title}</div>
|
|
99
|
+
|
|
100
|
+
<div class="meta">
|
|
101
|
+
<div class="imdb">
|
|
102
|
+
<IconIMDb />
|
|
103
|
+
{title.imdb_score || '-'}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div>{title.year}</div>
|
|
107
|
+
|
|
108
|
+
<div class="capitalize">{t(`Type: ${title.type}`)}</div>
|
|
109
|
+
|
|
110
|
+
{#if title.length}
|
|
111
|
+
<div>{title.length} {t('Minutes Short')}</div>
|
|
112
|
+
{/if}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="action">
|
|
117
|
+
{t('Watch')}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</a>
|
|
121
|
+
|
|
122
|
+
<div class="close">
|
|
123
|
+
<RoundButton onclick={close} aria-label="Close">
|
|
124
|
+
<IconClose />
|
|
125
|
+
</RoundButton>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
{/if}
|
|
129
|
+
|
|
130
|
+
<style lang="scss">
|
|
131
|
+
.highlighted-injection {
|
|
132
|
+
position: fixed;
|
|
133
|
+
bottom: margin(1);
|
|
134
|
+
left: margin(1);
|
|
135
|
+
width: calc(100% - margin(2));
|
|
136
|
+
|
|
137
|
+
@include desktop {
|
|
138
|
+
max-width: 400px;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.link {
|
|
143
|
+
display: flex;
|
|
144
|
+
gap: margin(1);
|
|
145
|
+
align-items: flex-start;
|
|
146
|
+
border-radius: theme(border-radius);
|
|
147
|
+
padding: margin(0.5) margin(1) margin(0.5) margin(0.5);
|
|
148
|
+
background: theme(dark);
|
|
149
|
+
box-shadow: theme(shadow-large);
|
|
150
|
+
text-decoration: none;
|
|
151
|
+
|
|
152
|
+
&:hover,
|
|
153
|
+
&:active {
|
|
154
|
+
transform: scale(1.025);
|
|
155
|
+
transition: transform 50ms;
|
|
156
|
+
filter: brightness(theme(hover-filter-brightness));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
&:active {
|
|
160
|
+
transform: scale(0.975);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.poster {
|
|
165
|
+
flex: 0 0 margin(2);
|
|
166
|
+
width: margin(2);
|
|
167
|
+
box-shadow: 0 0 2px 1px theme(content);
|
|
168
|
+
border-radius: theme(border-radius-small);
|
|
169
|
+
background: theme(content);
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.content {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: margin(0.5);
|
|
177
|
+
margin: auto 0;
|
|
178
|
+
color: theme(text-color-alt);
|
|
179
|
+
font-family: theme(font-family);
|
|
180
|
+
font-size: 12px;
|
|
181
|
+
line-height: 1.5;
|
|
182
|
+
font-style: normal;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.heading {
|
|
186
|
+
margin-bottom: margin(0.25);
|
|
187
|
+
font-size: theme(font-size-base);
|
|
188
|
+
font-weight: theme(font-bold);
|
|
189
|
+
color: theme(text-color);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.meta {
|
|
193
|
+
display: flex;
|
|
194
|
+
flex-wrap: wrap;
|
|
195
|
+
gap: 0 margin(0.5);
|
|
196
|
+
white-space: nowrap;
|
|
197
|
+
color: theme(text-color-alt);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.imdb {
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: margin(0.25);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.action {
|
|
207
|
+
grid-area: action;
|
|
208
|
+
margin-left: auto;
|
|
209
|
+
padding: margin(0.5);
|
|
210
|
+
border: theme(playlinks-action-border, 1px solid currentColor);
|
|
211
|
+
border-radius: theme(playlinks-action-border-radius, margin(2));
|
|
212
|
+
font-weight: theme(playlinks-action-font-weight, 500);
|
|
213
|
+
color: theme(playlinks-action-text-color, text-color);
|
|
214
|
+
line-height: 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.close {
|
|
218
|
+
position: absolute;
|
|
219
|
+
top: margin(-0.75);
|
|
220
|
+
right: margin(-0.75);
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
-
size?: string
|
|
6
|
-
children?: Snippet
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
size?: string
|
|
6
|
+
children?: Snippet
|
|
7
|
+
// eslint-disable-next-line no-unused-vars
|
|
8
|
+
onclick?: (event: MouseEvent) => void
|
|
9
|
+
[key: string]: any
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const { children, size = '32px', onclick = () => null, ...rest }: Props = $props()
|
|
@@ -13,6 +13,7 @@ import { getFullUrlPath } from '$lib/url'
|
|
|
13
13
|
import { fetchAds } from '$lib/api/ads'
|
|
14
14
|
import { fetchConfig } from '$lib/api/config'
|
|
15
15
|
import { hasConsentedTo } from '$lib/consent'
|
|
16
|
+
import { getSplitTestVariantName } from '$lib/splitTest'
|
|
16
17
|
|
|
17
18
|
vi.mock('$lib/api/externalPages', () => ({
|
|
18
19
|
fetchLinkInjections: vi.fn(() => {}),
|
|
@@ -66,6 +67,7 @@ vi.mock('$lib/url', () => ({
|
|
|
66
67
|
|
|
67
68
|
vi.mock('$lib/splitTest', () => ({
|
|
68
69
|
trackSplitTestView: vi.fn(),
|
|
70
|
+
getSplitTestVariantName: vi.fn(),
|
|
69
71
|
}))
|
|
70
72
|
|
|
71
73
|
vi.mock('$lib/api/ads', () => ({
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { render, fireEvent, waitFor } from '@testing-library/svelte'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import HighlightedInjection from '../../../routes/components/HighlightedInjection.svelte'
|
|
5
|
+
import { generateInjection } from '../../helpers'
|
|
6
|
+
import { title } from '$lib/fakeData'
|
|
7
|
+
import { openModal } from '$lib/modal'
|
|
8
|
+
|
|
9
|
+
vi.mock('$lib/modal', () => ({
|
|
10
|
+
openModal: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {number} value
|
|
15
|
+
*/
|
|
16
|
+
async function mockScroll(value) {
|
|
17
|
+
Object.defineProperty(window, 'scrollY', { value, configurable: true })
|
|
18
|
+
window.dispatchEvent(new Event('scroll'))
|
|
19
|
+
|
|
20
|
+
return new Promise(res => setTimeout(res))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('HighlightedInjection.svelte', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.resetAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const linkInjections = [
|
|
29
|
+
generateInjection('Some sentence', 'Some title'),
|
|
30
|
+
{ ...generateInjection('Some sentence', 'Some title'), title_details: { ...title, title: 'Some second injection' } },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
it('Should show after scrolling the required distance', async () => {
|
|
34
|
+
const { queryByRole } = render(HighlightedInjection, { linkInjections })
|
|
35
|
+
|
|
36
|
+
await mockScroll(0)
|
|
37
|
+
expect(queryByRole('link')).not.toBeTruthy()
|
|
38
|
+
|
|
39
|
+
await mockScroll(200)
|
|
40
|
+
expect(queryByRole('link')).not.toBeTruthy()
|
|
41
|
+
|
|
42
|
+
await mockScroll(1000)
|
|
43
|
+
expect(queryByRole('link')).toBeTruthy()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('Should not hide after being shown by scrolling', async () => {
|
|
47
|
+
const { queryByRole } = render(HighlightedInjection, { linkInjections })
|
|
48
|
+
|
|
49
|
+
await mockScroll(1000)
|
|
50
|
+
expect(queryByRole('link')).toBeTruthy()
|
|
51
|
+
|
|
52
|
+
await mockScroll(0)
|
|
53
|
+
expect(queryByRole('link')).toBeTruthy()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('Should render the first injection in the list', async () => {
|
|
57
|
+
const { getByText } = render(HighlightedInjection, { linkInjections })
|
|
58
|
+
|
|
59
|
+
await mockScroll(1000)
|
|
60
|
+
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
expect(getByText(linkInjections[0].title_details.title)).toBeTruthy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('Should render the injection most relevant to the document title', async () => {
|
|
66
|
+
// @ts-ignore
|
|
67
|
+
document.body.innerHTML = `<h1>${linkInjections[1].title_details.title}</h1>`
|
|
68
|
+
|
|
69
|
+
const { getByText } = render(HighlightedInjection, { linkInjections })
|
|
70
|
+
|
|
71
|
+
await mockScroll(1000)
|
|
72
|
+
|
|
73
|
+
// @ts-ignore
|
|
74
|
+
expect(getByText(linkInjections[1].title_details.title)).toBeTruthy()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('Should open modal on click', async () => {
|
|
78
|
+
const { getByRole } = render(HighlightedInjection, { linkInjections })
|
|
79
|
+
|
|
80
|
+
await mockScroll(1000)
|
|
81
|
+
await fireEvent.click(getByRole('link'))
|
|
82
|
+
|
|
83
|
+
expect(openModal).toHaveBeenCalled()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('Should close elements when close button is closed', async () => {
|
|
87
|
+
const { getByLabelText, queryByRole } = render(HighlightedInjection, { linkInjections })
|
|
88
|
+
|
|
89
|
+
await mockScroll(1000)
|
|
90
|
+
await fireEvent.click(getByLabelText('Close'))
|
|
91
|
+
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(queryByRole('link')).not.toBeTruthy()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
expect(openModal).not.toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
})
|