@playpilot/tpi 5.6.0 → 5.7.0-beta.display.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 +10 -10
- package/events.md +6 -0
- package/package.json +1 -1
- package/src/lib/ads.ts +75 -0
- package/src/lib/constants.ts +2 -1
- package/src/lib/enums/ImageDimensions.ts +7 -0
- package/src/lib/enums/SplitTest.ts +14 -1
- package/src/lib/enums/TrackingEvent.ts +3 -0
- package/src/lib/image.ts +12 -0
- package/src/lib/types/campaign.d.ts +45 -0
- package/src/lib/types/script.d.ts +2 -0
- package/src/main.ts +1 -0
- package/src/routes/+page.svelte +3 -0
- package/src/routes/components/Ads/Display.svelte +121 -0
- package/src/routes/components/Ads/TopScroll.svelte +240 -0
- package/src/routes/components/Modal.svelte +101 -19
- package/src/routes/components/Playlink.svelte +185 -0
- package/src/routes/components/Playlinks.svelte +34 -168
- package/src/routes/components/Popover.svelte +30 -2
- package/src/routes/components/Title.svelte +1 -0
- package/src/routes/components/TitleModal.svelte +23 -1
- package/src/routes/components/TitlePopover.svelte +11 -1
- package/src/tests/lib/ads.test.js +55 -0
- package/src/tests/routes/+page.test.js +33 -0
- package/src/tests/routes/components/Ads/Display.test.js +59 -0
- package/src/tests/routes/components/Ads/TopScroll.test.js +104 -0
- package/src/tests/routes/components/Playlink.test.js +88 -0
- package/src/tests/routes/components/Playlinks.test.js +0 -30
- package/src/tests/routes/components/TitleModal.test.js +36 -0
- package/src/tests/routes/components/TitlePopover.test.js +20 -0
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
3
|
+
import { isInSplitTestVariant } from '$lib/splitTest'
|
|
2
4
|
import { onMount, setContext, tick, type Snippet } from 'svelte'
|
|
3
5
|
import { prefersReducedMotion } from 'svelte/motion'
|
|
4
6
|
import { fly } from 'svelte/transition'
|
|
5
7
|
|
|
6
8
|
interface Props {
|
|
7
9
|
children: Snippet
|
|
10
|
+
bubble?: Snippet | null
|
|
8
11
|
maxHeight?: number
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
let { children, maxHeight = $bindable() }: Props = $props()
|
|
14
|
+
let { children, bubble, maxHeight = $bindable() }: Props = $props()
|
|
12
15
|
|
|
13
16
|
setContext('scope', 'popover')
|
|
14
17
|
|
|
18
|
+
const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
|
|
19
|
+
|
|
15
20
|
let element: HTMLElement | null = $state(null)
|
|
16
21
|
let flip = $state(false)
|
|
17
22
|
|
|
@@ -59,15 +64,24 @@
|
|
|
59
64
|
}
|
|
60
65
|
</script>
|
|
61
66
|
|
|
62
|
-
<div class="popover" class:flip bind:this={element} style:--max-height={maxHeight ? maxHeight + 'px' : null} tabindex="-1" aria-hidden="true">
|
|
67
|
+
<div class="popover" class:flip class:inline={inlineBubble} class:has-bubble={!!bubble && inlineBubble} bind:this={element} style:--max-height={maxHeight ? maxHeight + 'px' : null} tabindex="-1" aria-hidden="true">
|
|
63
68
|
<div class="dialog" transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }} data-view-transition-old>
|
|
64
69
|
{@render children()}
|
|
65
70
|
</div>
|
|
71
|
+
|
|
72
|
+
{#if bubble}
|
|
73
|
+
<div transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }}>
|
|
74
|
+
{@render bubble()}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
66
77
|
</div>
|
|
67
78
|
|
|
68
79
|
<style lang="scss">
|
|
69
80
|
.popover {
|
|
70
81
|
--offset: #{margin(0.5)};
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column-reverse;
|
|
84
|
+
gap: margin(0.5);
|
|
71
85
|
position: absolute;
|
|
72
86
|
top: calc((var(--offset) - 1px) * -1); /* Add 1 pixel to account for rounding errors */
|
|
73
87
|
left: 0;
|
|
@@ -78,9 +92,18 @@
|
|
|
78
92
|
z-index: 2147483647; // As high as she goes
|
|
79
93
|
|
|
80
94
|
&.flip {
|
|
95
|
+
flex-direction: column;
|
|
81
96
|
top: auto;
|
|
82
97
|
bottom: calc(var(--offset) + 1px); /* Add 1 pixel to account for rounding errors */
|
|
83
98
|
transform: translateY(calc(100% + var(--offset)));
|
|
99
|
+
|
|
100
|
+
&.inline {
|
|
101
|
+
flex-direction: column-reverse;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&.inline {
|
|
106
|
+
gap: 0;
|
|
84
107
|
}
|
|
85
108
|
}
|
|
86
109
|
|
|
@@ -96,5 +119,10 @@
|
|
|
96
119
|
overflow-y: overlay;
|
|
97
120
|
overflow-x: hidden;
|
|
98
121
|
view-transition-name: playpilot-title-content;
|
|
122
|
+
|
|
123
|
+
.has-bubble & {
|
|
124
|
+
border-top-left-radius: 0;
|
|
125
|
+
border-top-right-radius: 0;
|
|
126
|
+
}
|
|
99
127
|
}
|
|
100
128
|
</style>
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
import { onMount } from 'svelte'
|
|
6
6
|
import Modal from './Modal.svelte'
|
|
7
7
|
import Title from './Title.svelte'
|
|
8
|
+
import TopScroll from './Ads/TopScroll.svelte'
|
|
9
|
+
import Display from './Ads/Display.svelte'
|
|
10
|
+
import { getFirstAdOfType } from '$lib/ads'
|
|
11
|
+
import { isInSplitTestVariant } from '$lib/splitTest'
|
|
12
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
8
13
|
|
|
9
14
|
interface Props {
|
|
10
15
|
onclose: () => void,
|
|
@@ -13,6 +18,11 @@
|
|
|
13
18
|
|
|
14
19
|
const { onclose, title }: Props = $props()
|
|
15
20
|
|
|
21
|
+
const topScrollAd = getFirstAdOfType('top_scroll')
|
|
22
|
+
const displayAd = getFirstAdOfType('display')
|
|
23
|
+
|
|
24
|
+
const isInDisplayAdFormatSplitTest = isInSplitTestVariant(SplitTest.DisplayAdPosition, 1) || isInSplitTestVariant(SplitTest.DisplayAdPosition, 2)
|
|
25
|
+
|
|
16
26
|
track(TrackingEvent.TitleModalView, title)
|
|
17
27
|
|
|
18
28
|
let hasTrackedScrolling = false
|
|
@@ -31,6 +41,18 @@
|
|
|
31
41
|
}
|
|
32
42
|
</script>
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
{#snippet bubble()}
|
|
45
|
+
{#if topScrollAd}
|
|
46
|
+
<TopScroll campaign={topScrollAd} />
|
|
47
|
+
{/if}
|
|
48
|
+
{/snippet}
|
|
49
|
+
|
|
50
|
+
{#snippet prepend()}
|
|
51
|
+
{#if displayAd}
|
|
52
|
+
<Display campaign={displayAd} />
|
|
53
|
+
{/if}
|
|
54
|
+
{/snippet}
|
|
55
|
+
|
|
56
|
+
<Modal {onclose} {onscroll} prepend={isInDisplayAdFormatSplitTest ? prepend : null} bubble={topScrollAd ? bubble : null}>
|
|
35
57
|
<Title {title} />
|
|
36
58
|
</Modal>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
3
|
import { track } from '$lib/tracking'
|
|
4
|
+
import { getFirstAdOfType } from '$lib/ads'
|
|
4
5
|
import type { TitleData } from '$lib/types/title'
|
|
5
6
|
import { onMount } from 'svelte'
|
|
6
7
|
import Popover from './Popover.svelte'
|
|
7
8
|
import Title from './Title.svelte'
|
|
9
|
+
import TopScroll from './Ads/TopScroll.svelte'
|
|
8
10
|
|
|
9
11
|
interface Props {
|
|
10
12
|
event: MouseEvent
|
|
@@ -13,6 +15,8 @@
|
|
|
13
15
|
|
|
14
16
|
const { event, title }: Props = $props()
|
|
15
17
|
|
|
18
|
+
const topScroll = getFirstAdOfType('top_scroll')
|
|
19
|
+
|
|
16
20
|
let maxHeight = $state(0)
|
|
17
21
|
let element: HTMLElement | null = $state(null)
|
|
18
22
|
|
|
@@ -47,8 +51,14 @@
|
|
|
47
51
|
}
|
|
48
52
|
</script>
|
|
49
53
|
|
|
54
|
+
{#snippet bubble()}
|
|
55
|
+
{#if topScroll}
|
|
56
|
+
<TopScroll campaign={topScroll} />
|
|
57
|
+
{/if}
|
|
58
|
+
{/snippet}
|
|
59
|
+
|
|
50
60
|
<div class="title-popover" bind:this={element} data-playpilot-title-popover role="region" aria-labelledby="title">
|
|
51
|
-
<Popover bind:maxHeight>
|
|
61
|
+
<Popover bind:maxHeight bubble={topScroll ? bubble : null}>
|
|
52
62
|
<Title {title} small compact={!!maxHeight && maxHeight < 250} />
|
|
53
63
|
</Popover>
|
|
54
64
|
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
2
|
+
import { fakeFetch } from '../helpers'
|
|
3
|
+
|
|
4
|
+
import { fetchAds, getFirstAdOfType } from '$lib/ads'
|
|
5
|
+
|
|
6
|
+
describe('$lib/api', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.resetAllMocks()
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
window.PlayPilotLinkInjections = null
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('fetchAds', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
window.PlayPilotLinkInjections = { token: 'a' }
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('Should call fetch with given url', async () => {
|
|
20
|
+
fakeFetch({ response: 'Some response' })
|
|
21
|
+
|
|
22
|
+
const response = await fetchAds()
|
|
23
|
+
|
|
24
|
+
expect(response).toBe('Some response')
|
|
25
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
26
|
+
expect.stringContaining('api-token'),
|
|
27
|
+
expect.objectContaining({}),
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('Should not call fetch if no api token is present', async () => {
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
window.PlayPilotLinkInjections = { token: '' }
|
|
34
|
+
|
|
35
|
+
await expect(async () => await fetchAds()).rejects.toThrowError('No token was provided')
|
|
36
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('getFirstAdOfType', () => {
|
|
41
|
+
it('Should return the first ad for the given type in the window object', () => {
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
window.PlayPilotLinkInjections = { ads: [{ campaign_format: 'top_scroll' }] }
|
|
44
|
+
|
|
45
|
+
expect(getFirstAdOfType('top_scroll')).toEqual({ campaign_format: 'top_scroll' })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('Should not return any ads if no valid ads are given', () => {
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
window.PlayPilotLinkInjections = { ads: [{ campaign_format: 'hero' }] }
|
|
51
|
+
|
|
52
|
+
expect(getFirstAdOfType('top_scroll')).toBe(null)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -10,6 +10,7 @@ import { generateInjection } from '../helpers'
|
|
|
10
10
|
import { injectLinksInDocument } from '$lib/linkInjection'
|
|
11
11
|
import { isCrawler } from '$lib/crawler'
|
|
12
12
|
import { getFullUrlPath } from '$lib/url'
|
|
13
|
+
import { fetchAds } from '$lib/ads'
|
|
13
14
|
|
|
14
15
|
vi.mock('$lib/api', () => ({
|
|
15
16
|
fetchLinkInjections: vi.fn(() => {}),
|
|
@@ -61,6 +62,10 @@ vi.mock('$lib/splitTest', () => ({
|
|
|
61
62
|
trackSplitTestView: vi.fn(),
|
|
62
63
|
}))
|
|
63
64
|
|
|
65
|
+
vi.mock('$lib/ads', () => ({
|
|
66
|
+
fetchAds: vi.fn(),
|
|
67
|
+
}))
|
|
68
|
+
|
|
64
69
|
describe('$routes/+page.svelte', () => {
|
|
65
70
|
beforeEach(() => {
|
|
66
71
|
document.body.innerHTML = ''
|
|
@@ -373,6 +378,34 @@ describe('$routes/+page.svelte', () => {
|
|
|
373
378
|
})
|
|
374
379
|
})
|
|
375
380
|
|
|
381
|
+
it('Should fetch ads if any injections are given', async () => {
|
|
382
|
+
// @ts-ignore
|
|
383
|
+
vi.mocked(pollLinkInjections).mockResolvedValueOnce({ ai_injections: [{}], manual_injections: [] })
|
|
384
|
+
render(page)
|
|
385
|
+
await waitFor(() => expect(fetchAds).toHaveBeenCalled())
|
|
386
|
+
|
|
387
|
+
// @ts-ignore
|
|
388
|
+
vi.mocked(pollLinkInjections).mockResolvedValueOnce({ ai_injections: [], manual_injections: [{}] })
|
|
389
|
+
render(page)
|
|
390
|
+
await waitFor(() => expect(fetchAds).toHaveBeenCalledTimes(2))
|
|
391
|
+
|
|
392
|
+
// @ts-ignore
|
|
393
|
+
vi.mocked(pollLinkInjections).mockResolvedValueOnce({ ai_injections: [{}], manual_injections: [{}] })
|
|
394
|
+
render(page)
|
|
395
|
+
await waitFor(() => expect(fetchAds).toHaveBeenCalledTimes(3))
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('Should not fetch ads if any no injections are given', async () => {
|
|
399
|
+
// @ts-ignore
|
|
400
|
+
vi.mocked(pollLinkInjections).mockResolvedValueOnce({ ai_injections: [], manual_injections: [] })
|
|
401
|
+
|
|
402
|
+
render(page)
|
|
403
|
+
|
|
404
|
+
await new Promise(res => setTimeout(res, 100)) // Await possible fetches
|
|
405
|
+
|
|
406
|
+
expect(fetchAds).not.toHaveBeenCalled()
|
|
407
|
+
})
|
|
408
|
+
|
|
376
409
|
describe('Config', () => {
|
|
377
410
|
describe('exclude_urls_pattern', () => {
|
|
378
411
|
it('Should not inject if config exclude_urls_pattern matches current url', async () => {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import Display from '../../../../routes/components/Ads/Display.svelte'
|
|
5
|
+
import { track } from '$lib/tracking'
|
|
6
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
7
|
+
|
|
8
|
+
vi.mock('$lib/tracking', () => ({
|
|
9
|
+
track: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
export const campaign = {
|
|
13
|
+
campaign_format: 'display',
|
|
14
|
+
campaign_type: 'video',
|
|
15
|
+
campaign_platforms: ['web', 'android_app', 'ios_app'],
|
|
16
|
+
campaign_name: 'some_campaign_NL',
|
|
17
|
+
campaign_start: '2025-04-24T00:00:00Z',
|
|
18
|
+
campaign_end: '2025-04-30T23:59:00Z',
|
|
19
|
+
campaign_region: 'nl',
|
|
20
|
+
content: {
|
|
21
|
+
header: 'Some top scroll header',
|
|
22
|
+
header_logo: 'https://example.com/',
|
|
23
|
+
header_logo_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
|
|
24
|
+
subheader: null,
|
|
25
|
+
image: 'https://example.com/',
|
|
26
|
+
image_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
|
|
27
|
+
video: null,
|
|
28
|
+
format: null,
|
|
29
|
+
},
|
|
30
|
+
cta: {
|
|
31
|
+
header: 'Button Label',
|
|
32
|
+
subheader: null,
|
|
33
|
+
image: null,
|
|
34
|
+
image_uuid: null,
|
|
35
|
+
url: 'https://google.com/',
|
|
36
|
+
},
|
|
37
|
+
content_playlist: null,
|
|
38
|
+
provider: null,
|
|
39
|
+
impression_trackers: [],
|
|
40
|
+
target_title_uuid: null,
|
|
41
|
+
target_title_sid: null,
|
|
42
|
+
backfill_providers: [],
|
|
43
|
+
autogenerated: false,
|
|
44
|
+
enabled: true,
|
|
45
|
+
hide_imdb_score: false,
|
|
46
|
+
hide_sponsored_message: false,
|
|
47
|
+
disclaimer: 'Some disclaimer',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('Display.svelte', () => {
|
|
51
|
+
it('Should fire track even on click', async () => {
|
|
52
|
+
/** @ts-ignore */
|
|
53
|
+
const { getByRole } = render(Display, { campaign })
|
|
54
|
+
|
|
55
|
+
await fireEvent.click(getByRole('link'))
|
|
56
|
+
|
|
57
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.DisplayAdClick, null, { campaign_name: campaign.campaign_name })
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { fireEvent, render, waitFor } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import TopScroll from '../../../../routes/components/Ads/TopScroll.svelte'
|
|
5
|
+
import { track } from '$lib/tracking'
|
|
6
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
7
|
+
|
|
8
|
+
vi.mock('$lib/tracking', () => ({
|
|
9
|
+
track: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
export const campaign = {
|
|
13
|
+
campaign_format: 'top_scroll',
|
|
14
|
+
campaign_type: 'video',
|
|
15
|
+
campaign_platforms: ['web', 'android_app', 'ios_app'],
|
|
16
|
+
campaign_name: 'some_campaign_NL',
|
|
17
|
+
campaign_start: '2025-04-24T00:00:00Z',
|
|
18
|
+
campaign_end: '2025-04-30T23:59:00Z',
|
|
19
|
+
campaign_region: 'nl',
|
|
20
|
+
content: {
|
|
21
|
+
header: 'Some top scroll header',
|
|
22
|
+
header_logo: 'https://example.com/',
|
|
23
|
+
header_logo_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
|
|
24
|
+
subheader: null,
|
|
25
|
+
image: 'https://example.com/',
|
|
26
|
+
image_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
|
|
27
|
+
video: null,
|
|
28
|
+
format: null,
|
|
29
|
+
},
|
|
30
|
+
cta: {
|
|
31
|
+
header: 'Button Label',
|
|
32
|
+
subheader: null,
|
|
33
|
+
image: null,
|
|
34
|
+
image_uuid: null,
|
|
35
|
+
url: 'https://google.com/',
|
|
36
|
+
},
|
|
37
|
+
content_playlist: null,
|
|
38
|
+
provider: null,
|
|
39
|
+
impression_trackers: [],
|
|
40
|
+
target_title_uuid: null,
|
|
41
|
+
target_title_sid: null,
|
|
42
|
+
backfill_providers: [],
|
|
43
|
+
autogenerated: false,
|
|
44
|
+
enabled: true,
|
|
45
|
+
hide_imdb_score: false,
|
|
46
|
+
hide_sponsored_message: false,
|
|
47
|
+
disclaimer: 'Some disclaimer',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('TopScroll.svelte', () => {
|
|
51
|
+
it('Should render the given content', () => {
|
|
52
|
+
/** @ts-ignore */
|
|
53
|
+
const { getByText, container } = render(TopScroll, { campaign })
|
|
54
|
+
|
|
55
|
+
expect(getByText(campaign.cta.header)).toBeTruthy()
|
|
56
|
+
expect(getByText(campaign.content.header)).toBeTruthy()
|
|
57
|
+
expect(container.querySelector('.tooltip')).toBeTruthy()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('Should not show disclaimer when not given', async () => {
|
|
61
|
+
const { getByText, container } = render(TopScroll, {
|
|
62
|
+
/** @ts-ignore */
|
|
63
|
+
campaign: { ...campaign, disclaimer: null },
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await waitFor(() => getByText(campaign.cta.header))
|
|
67
|
+
|
|
68
|
+
expect(container.querySelector('.tooltip')).not.toBeTruthy()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('Should toggle disclaimer on click', async () => {
|
|
72
|
+
/** @ts-ignore */
|
|
73
|
+
const { getByText, container, queryByText } = render(TopScroll, { campaign })
|
|
74
|
+
|
|
75
|
+
await waitFor(() => getByText(campaign.cta.header))
|
|
76
|
+
|
|
77
|
+
await fireEvent.click(/** @type {HTMLElement} */ (container.querySelector('.tooltip')))
|
|
78
|
+
expect(getByText(campaign.disclaimer)).toBeTruthy()
|
|
79
|
+
|
|
80
|
+
await fireEvent.click(/** @type {HTMLElement} */ (container.querySelector('.tooltip')))
|
|
81
|
+
expect(queryByText(campaign.disclaimer)).not.toBeTruthy()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('Should fire track even on click', async () => {
|
|
85
|
+
/** @ts-ignore */
|
|
86
|
+
const { getByRole } = render(TopScroll, { campaign })
|
|
87
|
+
|
|
88
|
+
await fireEvent.click(getByRole('link'))
|
|
89
|
+
|
|
90
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.TopScrollClick, null, { campaign_name: campaign.campaign_name })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('Should render as simple variant when content format is large', async () => {
|
|
94
|
+
const simpleCampaign = { ...campaign, content: { ...campaign.content, format: 'large' } }
|
|
95
|
+
/** @ts-ignore */
|
|
96
|
+
const { queryByText, container } = render(TopScroll, { campaign: simpleCampaign })
|
|
97
|
+
|
|
98
|
+
expect(queryByText(simpleCampaign.content.header)).not.toBeTruthy()
|
|
99
|
+
expect(queryByText(simpleCampaign.cta.header)).not.toBeTruthy()
|
|
100
|
+
expect(container.querySelector('.background')).not.toBeTruthy()
|
|
101
|
+
expect(container.querySelector('.disclaimer')).not.toBeTruthy()
|
|
102
|
+
expect(container.querySelector('.content-image')).toBeTruthy()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import Playlink from '../../../routes/components/Playlink.svelte'
|
|
5
|
+
|
|
6
|
+
vi.mock('$lib/tracking', () => ({
|
|
7
|
+
track: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
vi.mock('svelte', async (importActual) => ({
|
|
11
|
+
...(await importActual()),
|
|
12
|
+
getContext: vi.fn(),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
describe('Playlink.svelte', () => {
|
|
16
|
+
const playlink = { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } }
|
|
17
|
+
|
|
18
|
+
it('Should render category as words', () => {
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
let { unmount, getByText } = render(Playlink, { playlink })
|
|
21
|
+
expect(getByText('Stream')).toBeTruthy()
|
|
22
|
+
unmount()
|
|
23
|
+
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
;({ unmount, getByText } = render(Playlink, { playlink: { name: 'Some buy playlink', logo_url: 'logo', extra_info: { category: 'BUY' } } }))
|
|
26
|
+
expect(getByText('Buy')).toBeTruthy()
|
|
27
|
+
unmount()
|
|
28
|
+
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
;({ unmount, getByText } = render(Playlink, { playlink: { name: 'Some rent playlink', logo_url: 'logo', extra_info: { category: 'RENT' } } }))
|
|
31
|
+
expect(getByText('Rent')).toBeTruthy()
|
|
32
|
+
unmount()
|
|
33
|
+
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
;({ unmount, getByText } = render(Playlink, { playlink: { name: 'Some rent playlink', logo_url: 'logo', extra_info: { category: 'other' } } }))
|
|
36
|
+
expect(getByText('Stream')).toBeTruthy()
|
|
37
|
+
unmount()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('Should render the category when hideCategory is false', () => {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
const { queryByTestId } = render(Playlink, { playlink, hideCategory: false })
|
|
43
|
+
|
|
44
|
+
expect(queryByTestId('category')).toBeTruthy()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('Should not render the category when hideCategory is given', () => {
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
const { queryByTestId } = render(Playlink, { playlink, hideCategory: true })
|
|
50
|
+
|
|
51
|
+
expect(queryByTestId('category')).not.toBeTruthy()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('Should call given onclick function', async () => {
|
|
55
|
+
const onclick = vi.fn()
|
|
56
|
+
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
const { getByText } = render(Playlink, { playlink, onclick })
|
|
59
|
+
|
|
60
|
+
await fireEvent.click(getByText(playlink.name))
|
|
61
|
+
|
|
62
|
+
expect(onclick).toHaveBeenCalled()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('Should highlight and show category text for playlinks that have highlighted as true', () => {
|
|
66
|
+
// @ts-ignore
|
|
67
|
+
const { getByText } = render(Playlink, { playlink: { name: 'Some highlighted playlink', logo_url: 'logo', highlighted: true, cta_text: 'Some CTA', extra_info: { category: 'BUY' } } })
|
|
68
|
+
|
|
69
|
+
expect(getByText('Some highlighted playlink').closest('.playlink')?.classList).toContain('highlighted')
|
|
70
|
+
expect(getByText('Some CTA')).toBeTruthy()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('Should not highlight or show category text for playlinks that have highlighted as true but have no cta_text', () => {
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
const { getByText, queryByText } = render(Playlink, { playlink: { name: 'Some playlink', logo_url: 'logo', highlighted: true, cta_text: '', extra_info: { category: 'BUY' } } })
|
|
76
|
+
|
|
77
|
+
expect(getByText('Some playlink').closest('.playlink')?.classList).not.toContain('highlighted')
|
|
78
|
+
expect(queryByText('Some CTA')).not.toBeTruthy()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('Should not highlight playlinks that do not have highlighted set to true', () => {
|
|
82
|
+
// @ts-ignore
|
|
83
|
+
const { getByText, queryByText } = render(Playlink, { playlink })
|
|
84
|
+
|
|
85
|
+
expect(getByText('Some playlink').closest('.playlink')?.classList).not.toContain('highlighted')
|
|
86
|
+
expect(queryByText('Some CTA')).not.toBeTruthy()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -49,21 +49,6 @@ describe('Playlinks.svelte', () => {
|
|
|
49
49
|
expect(queryByTestId('commission-disclaimer')).not.toBeTruthy()
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
it('Should render category as words', () => {
|
|
53
|
-
const playlinks = [
|
|
54
|
-
{ name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
|
|
55
|
-
{ name: 'Some buy playlink', logo_url: 'logo', extra_info: { category: 'BUY' } },
|
|
56
|
-
{ name: 'Some rent playlink', logo_url: 'logo', extra_info: { category: 'RENT' } },
|
|
57
|
-
{ name: 'Some other playlink', logo_url: 'logo', extra_info: { category: 'other' } },
|
|
58
|
-
]
|
|
59
|
-
// @ts-ignore
|
|
60
|
-
const { getByText, getAllByText } = render(Playlinks, { playlinks, title })
|
|
61
|
-
|
|
62
|
-
expect(getAllByText('Stream')).toHaveLength(2)
|
|
63
|
-
expect(getByText('Buy')).toBeTruthy()
|
|
64
|
-
expect(getByText('Rent')).toBeTruthy()
|
|
65
|
-
})
|
|
66
|
-
|
|
67
52
|
it('Should call track function when clicked', async () => {
|
|
68
53
|
const playlinks = [
|
|
69
54
|
{ name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
|
|
@@ -103,19 +88,4 @@ describe('Playlinks.svelte', () => {
|
|
|
103
88
|
|
|
104
89
|
expect(getAllByText('Some playlink')).toHaveLength(1)
|
|
105
90
|
})
|
|
106
|
-
|
|
107
|
-
it('Should highlight and show category text for playlinks that have highlighted as true', () => {
|
|
108
|
-
const playlinks = [
|
|
109
|
-
{ name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
|
|
110
|
-
{ name: 'Some other playlink', logo_url: 'logo', highlighted: true, cta_text: '', extra_info: { category: 'BUY' } },
|
|
111
|
-
{ name: 'Some highlighted playlink', logo_url: 'logo', highlighted: true, cta_text: 'Some CTA', extra_info: { category: 'BUY' } },
|
|
112
|
-
]
|
|
113
|
-
// @ts-ignore
|
|
114
|
-
const { getByText } = render(Playlinks, { playlinks, title })
|
|
115
|
-
|
|
116
|
-
expect(getByText('Some playlink').closest('.playlink')?.classList).not.toContain('highlighted')
|
|
117
|
-
expect(getByText('Some other playlink').closest('.playlink')?.classList).not.toContain('highlighted')
|
|
118
|
-
expect(getByText('Some highlighted playlink').closest('.playlink')?.classList).toContain('highlighted')
|
|
119
|
-
expect(getByText('Some CTA')).toBeTruthy()
|
|
120
|
-
})
|
|
121
91
|
})
|
|
@@ -45,4 +45,40 @@ describe('TitleModal.svelte', () => {
|
|
|
45
45
|
|
|
46
46
|
expect(track).toHaveBeenCalledWith(TrackingEvent.TitleModalClose, title, { time_spent: 200 })
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
it('Should render top scroll ad when given', () => {
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
window.PlayPilotLinkInjections = { ads: [{ campaign_format: 'top_scroll', content: {}, cta: {} }] }
|
|
52
|
+
|
|
53
|
+
const { container } = render(TitleModal, { onclose, title })
|
|
54
|
+
|
|
55
|
+
expect(container.querySelector('.top-scroll')).toBeTruthy()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('Should not render top scroll ad when not given', () => {
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
window.PlayPilotLinkInjections = { ads: null }
|
|
61
|
+
|
|
62
|
+
const { container } = render(TitleModal, { onclose, title })
|
|
63
|
+
|
|
64
|
+
expect(container.querySelector('.top-scroll')).not.toBeTruthy()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('Should render display ad when given', () => {
|
|
68
|
+
// @ts-ignore
|
|
69
|
+
window.PlayPilotLinkInjections = { ads: [{ campaign_format: 'display', content: {}, cta: {} }] }
|
|
70
|
+
|
|
71
|
+
const { container } = render(TitleModal, { onclose, title })
|
|
72
|
+
|
|
73
|
+
expect(container.querySelector('.display')).toBeTruthy()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('Should not render top scroll ad when not given', () => {
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
window.PlayPilotLinkInjections = { ads: null }
|
|
79
|
+
|
|
80
|
+
const { container } = render(TitleModal, { onclose, title })
|
|
81
|
+
|
|
82
|
+
expect(container.querySelector('.display')).not.toBeTruthy()
|
|
83
|
+
})
|
|
48
84
|
})
|
|
@@ -37,4 +37,24 @@ describe('TitlePopover.svelte', () => {
|
|
|
37
37
|
|
|
38
38
|
expect(track).toHaveBeenCalledWith(TrackingEvent.TitlePopoverClose, title, { time_spent: 200 })
|
|
39
39
|
})
|
|
40
|
+
|
|
41
|
+
it('Should render top scroll ad when given', () => {
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
window.PlayPilotLinkInjections = { ads: [{ campaign_format: 'top_scroll', content: {}, cta: {} }] }
|
|
44
|
+
|
|
45
|
+
const event = new MouseEvent('mouseenter')
|
|
46
|
+
const { container } = render(TitlePopover, { event, title })
|
|
47
|
+
|
|
48
|
+
expect(container.querySelector('.top-scroll')).toBeTruthy()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('Should not render top scroll ad when not given', () => {
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
window.PlayPilotLinkInjections = { ads: null }
|
|
54
|
+
|
|
55
|
+
const event = new MouseEvent('mouseenter')
|
|
56
|
+
const { container } = render(TitlePopover, { event, title })
|
|
57
|
+
|
|
58
|
+
expect(container.querySelector('.top-scroll')).not.toBeTruthy()
|
|
59
|
+
})
|
|
40
60
|
})
|