@playpilot/tpi 6.4.0-beta.8 → 6.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "6.4.0-beta.8",
3
+ "version": "6.4.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -1,4 +1,4 @@
1
- export function focustrap(node: HTMLElement) {
1
+ export function focustrap(node: HTMLElement): { destroy(): void } {
2
2
  function getFocusableElements(): HTMLElement[] {
3
3
  return Array.from(node.querySelectorAll<HTMLElement>('button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])'))
4
4
  }
@@ -4,4 +4,9 @@ export const SplitTest = {
4
4
  numberOfVariants: 2,
5
5
  variantNames: ['Separated', 'Inline'] as string[],
6
6
  },
7
+ PostersScrollReveal: {
8
+ key: 'posters_scroll_reveal',
9
+ numberOfVariants: 2,
10
+ variantNames: ['Default', 'With Posters'] as string[],
11
+ },
7
12
  } as const
@@ -10,6 +10,12 @@ declare global {
10
10
  mockAd(override?: Record<any, any> = {}): void
11
11
  }
12
12
  }
13
+
14
+ declare namespace svelteHTML {
15
+ interface HTMLAttributes<T> {
16
+ 'onupdateconsent'?: (event: CustomEvent<{}>) => void
17
+ }
18
+ }
13
19
  }
14
20
 
15
21
  export { }
@@ -8,6 +8,7 @@ export type PlaylinkData = {
8
8
  cta_text?: string | null
9
9
  // This doesn't actually exists on the API objects, it's only present from ads
10
10
  action_text?: string | null,
11
+ pixels?: string[]
11
12
  extra_info: {
12
13
  category: PlaylinkCategory
13
14
  }
@@ -17,7 +17,7 @@
17
17
  // If require_consent has been explicitely turned off we return right away and call `onchange`.
18
18
  // We don't need to set consent values as require_consent=false will mean all consent is true.
19
19
  if (window.PlayPilotLinkInjections.require_consent === false) {
20
- onchange()
20
+ update()
21
21
  return
22
22
  }
23
23
 
@@ -66,7 +66,15 @@
66
66
  affiliate: consent(1) && consent(7),
67
67
  })
68
68
 
69
- onchange()
69
+ update()
70
70
  })
71
71
  }
72
+
73
+ function update() {
74
+ window.dispatchEvent(new CustomEvent('updateconsent', {
75
+ bubbles: true,
76
+ }))
77
+
78
+ onchange()
79
+ }
72
80
  </script>
@@ -27,6 +27,8 @@
27
27
  const succesfulInjections = data.evaluated_link_injections?.filter(injection => !injection.failed) || []
28
28
  const failedInjections = data.evaluated_link_injections?.filter(injection => injection.failed) || []
29
29
 
30
+ const visiblePixels = Array.from(document.querySelectorAll<HTMLImageElement>('[data-playpilot-pixel]'))
31
+
30
32
  return {
31
33
  'Config': [
32
34
  { label: 'Domain', data: data.domain_sid },
@@ -38,6 +40,7 @@
38
40
  [`Failed injections (${failedInjections.length})`]: failedInjections.map(injection => ({ label: injection.title, data: `Reason: ${injection.failed_message} | Sentence: ${injection.sentence}` })),
39
41
  [`Fetched ads (${data.ads?.length || 0})`]: data.ads?.map(ad => ({ label: ad.campaign_name, data: ad })),
40
42
  [`Tracking events (${data.tracked_events?.length || 0})`]: data.tracked_events?.map(event => ({ label: event.event, data: event.payload })),
43
+ [`Visible pixels (${visiblePixels.length})`]: visiblePixels.map((pixel, index) => ({ label: index + 1, data: pixel.src })),
41
44
  }
42
45
  }
43
46
 
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Disclaimer from '../Ads/Disclaimer.svelte'
3
+ import TrackingPixels from '../TrackingPixels.svelte'
3
4
  import { hasConsentedTo } from '$lib/consent'
4
5
  import { removeImageUrlPrefix } from '$lib/image'
5
6
  import { t } from '$lib/localization'
@@ -14,7 +15,7 @@
14
15
 
15
16
  const { playlink, disclaimer = '', hideCategory = false, onclick = () => null }: Props = $props()
16
17
 
17
- const { name, url, logo_url, highlighted, cta_text, action_text, extra_info: { category } } = $derived(playlink)
18
+ const { name, url, logo_url, highlighted, cta_text, action_text, pixels, extra_info: { category } } = $derived(playlink)
18
19
 
19
20
  const categoryStrings = {
20
21
  SVOD: t('Stream'),
@@ -31,8 +32,9 @@
31
32
  href={url}
32
33
  target="_blank"
33
34
  class="playlink"
34
- class:highlighted={highlighted && cta_text}
35
+ class:highlighted={highlighted}
35
36
  class:no-category={hideCategory}
37
+ class:no-subtext={!cta_text && hideCategory}
36
38
  data-playlink={name}
37
39
  rel="sponsored">
38
40
 
@@ -57,6 +59,10 @@
57
59
  <Disclaimer {disclaimer} small />
58
60
  {/if}
59
61
  </div>
62
+
63
+ {#if pixels?.length}
64
+ <TrackingPixels {pixels} />
65
+ {/if}
60
66
  </svelte:element>
61
67
 
62
68
  <style lang="scss">
@@ -162,6 +168,10 @@
162
168
  .no-category & {
163
169
  grid-template-areas: "image name action" "image cta action";
164
170
  }
171
+
172
+ .no-subtext & {
173
+ grid-template-areas: "image name action";
174
+ }
165
175
  }
166
176
 
167
177
  .name {
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import TrackingPixels from '../TrackingPixels.svelte'
2
3
  import { removeImageUrlPrefix } from '$lib/image'
3
4
  import type { PlaylinkData } from '$lib/types/playlink'
4
5
 
@@ -11,13 +12,18 @@
11
12
 
12
13
  const { playlink, size = 30, onclick = () => null }: Props = $props()
13
14
 
14
- const { name, url, logo_url } = $derived(playlink)
15
+ const { name, url, logo_url, pixels } = $derived(playlink)
15
16
  </script>
16
17
 
17
18
  <a href={url} target="_blank" class="playlink" data-playlink={name} rel="sponsored" {onclick} style:--size="{size}px">
18
19
  <img src={removeImageUrlPrefix(logo_url)} alt={name} height={size} width={size} />
20
+
21
+ {#if pixels?.length}
22
+ <TrackingPixels {pixels} />
23
+ {/if}
19
24
  </a>
20
25
 
26
+
21
27
  <style lang="scss">
22
28
  .playlink {
23
29
  display: inline-block;
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { mobileBreakpoint } from '$lib/constants'
3
+ import { SplitTest } from '$lib/enums/SplitTest'
4
+ import { isInSplitTestVariant, trackSplitTestAction, trackSplitTestView } from '$lib/splitTest'
2
5
  import { onMount } from 'svelte'
3
6
 
4
7
  const linksWithPosters = Array.from(document.querySelectorAll<HTMLElement>('[data-playpilot-poster-url]'))
@@ -6,14 +9,39 @@
6
9
  const targetThreshold = 0.7
7
10
  const posterSelector = '[data-playpilot-poster]'
8
11
 
12
+ let windowWidth = $state(0)
9
13
  let postersShownForLink: Record<string, HTMLElement> = {}
10
14
  let scrollTimeout: ReturnType<typeof setTimeout> | null = null
11
15
 
16
+ const isMobile = $derived(windowWidth < mobileBreakpoint)
17
+
12
18
  onMount(() => {
19
+ if (!isMobile) return
20
+
21
+ trackSplitTestView(SplitTest.PostersScrollReveal)
22
+
13
23
  return destroyAllPosters
14
24
  })
15
25
 
26
+ $effect(() => {
27
+ if (!isMobile) return
28
+ if (!linksWithPosters.length) return
29
+
30
+ const onclick = () => trackSplitTestAction(SplitTest.PostersScrollReveal, 'click')
31
+
32
+ if (!isInSplitTestVariant(SplitTest.PostersScrollReveal)) return
33
+
34
+ for(const link of linksWithPosters) link.addEventListener('click', onclick)
35
+
36
+ return () => {
37
+ for(const link of linksWithPosters) link.removeEventListener('click', onclick)
38
+ }
39
+ })
40
+
16
41
  function onscroll(): void {
42
+ if (!isMobile) return
43
+ if (!isInSplitTestVariant(SplitTest.PostersScrollReveal)) return
44
+
17
45
  setScrollTimeout()
18
46
 
19
47
  const windowHeight = window.innerHeight
@@ -84,4 +112,4 @@
84
112
  }
85
113
  </script>
86
114
 
87
- <svelte:window {onscroll} />
115
+ <svelte:window {onscroll} bind:innerWidth={windowWidth} />
@@ -8,7 +8,6 @@
8
8
  import Title from './Title.svelte'
9
9
  import TopScroll from './Ads/TopScroll.svelte'
10
10
  import Display from './Ads/Display.svelte'
11
- import TrackingPixels from './TrackingPixels.svelte'
12
11
 
13
12
  interface Props {
14
13
  event: MouseEvent
@@ -68,9 +67,6 @@
68
67
  <div class="title-popover" bind:this={element} data-playpilot-title-popover role="region" aria-labelledby="title">
69
68
  <Popover append={displayAd ? append : null} bubble={topScroll ? bubble : null}>
70
69
  <Title {title} small />
71
-
72
- <!-- Temporary tracking pixel to verify our own tracking works as expected. The random id exists to bypass cache on subsequent requests. -->
73
- <TrackingPixels pixels={[`https://imp.pxf.io/i/2439446/837174/9358?id=${Math.random()}`]} />
74
70
  </Popover>
75
71
  </div>
76
72
 
@@ -6,13 +6,19 @@
6
6
  }
7
7
 
8
8
  const { pixels }: Props = $props()
9
+
10
+ let key = $state(Math.random())
9
11
  </script>
10
12
 
11
- {#if hasConsentedTo('pixels')}
12
- {#each pixels as src}
13
- <img {src} alt="" />
14
- {/each}
15
- {/if}
13
+ <svelte:window onupdateconsent={() => key = Math.random()} />
14
+
15
+ {#key key}
16
+ {#if hasConsentedTo('pixels')}
17
+ {#each pixels as src}
18
+ <img {src} alt="" data-playpilot-pixel />
19
+ {/each}
20
+ {/if}
21
+ {/key}
16
22
 
17
23
  <style lang="scss">
18
24
  img {
@@ -15,10 +15,13 @@ describe('Consent.svelte', () => {
15
15
  expect(onchange).toHaveBeenCalled()
16
16
  })
17
17
 
18
- it('Should call onchange after user has consented', () => {
18
+ it('Should call onchange and fire window event after user has consented', () => {
19
19
  // @ts-ignore
20
20
  window.PlayPilotLinkInjections = { require_consent: true }
21
21
 
22
+ const mock = vi.fn()
23
+ window.addEventListener('updateconsent', mock)
24
+
22
25
  // @ts-ignore
23
26
  window.__tcfapi = (command, _version, callback) => {
24
27
  if (command !== 'addEventListener') return
@@ -39,6 +42,7 @@ describe('Consent.svelte', () => {
39
42
  window.__tcfapi()
40
43
 
41
44
  expect(onchange).toHaveBeenCalled()
45
+ expect(mock).toHaveBeenCalled()
42
46
  })
43
47
 
44
48
  it('Should not call onchange after user has consented but eventStatus is invalid', () => {
@@ -79,12 +79,11 @@ describe('Playlink.svelte', () => {
79
79
  expect(getByText('Some CTA')).toBeTruthy()
80
80
  })
81
81
 
82
- it('Should not highlight or show category text for playlinks that have highlighted as true but have no cta_text', () => {
82
+ it('Should highlight playlinks that have highlighted as true even if they have no cta_text', () => {
83
83
  // @ts-ignore
84
- const { getByText, queryByText } = render(Playlink, { playlink: { name: 'Some playlink', logo_url: 'logo', highlighted: true, cta_text: '', extra_info: { category: 'BUY' } } })
84
+ const { getByText } = render(Playlink, { playlink: { name: 'Some playlink', logo_url: 'logo', highlighted: true, cta_text: '', extra_info: { category: 'BUY' } } })
85
85
 
86
- expect(getByText('Some playlink').closest('.playlink')?.classList).not.toContain('highlighted')
87
- expect(queryByText('Some CTA')).not.toBeTruthy()
86
+ expect(getByText('Some playlink').closest('.playlink')?.classList).toContain('highlighted')
88
87
  })
89
88
 
90
89
  it('Should not highlight playlinks that do not have highlighted set to true', () => {
@@ -131,4 +130,36 @@ describe('Playlink.svelte', () => {
131
130
 
132
131
  expect(getByTestId('disclaimer')).toBeTruthy()
133
132
  })
133
+
134
+
135
+ it('Should render given tracking pixels', () => {
136
+ vi.mocked(hasConsentedTo).mockImplementation(() => true)
137
+
138
+ const playlink = { name: 'Some playlink', logo_url: 'logo', pixels: ['https://some-pixel.com/a.jpg'], extra_info: { category: 'SVOD' } }
139
+ // @ts-ignore
140
+ const { container } = render(Playlink, { playlink })
141
+
142
+ expect(container.querySelector('img[src*="https://some-pixel.com/a.jpg"]')).toBeTruthy()
143
+ })
144
+
145
+ it('Should include no-subtext class if no cta_text is given and hideCategory is true', () => {
146
+ // @ts-ignore
147
+ const { container } = render(Playlink, { playlink: { ...playlink, cta_text: '' }, hideCategory: true })
148
+
149
+ expect(container.querySelector('.no-subtext')).toBeTruthy()
150
+ })
151
+
152
+ it('Should not include no-subtext class if no cta_text is given but hideCategory is false', () => {
153
+ // @ts-ignore
154
+ const { container } = render(Playlink, { playlink: { ...playlink, cta_text: '' }, hideCategory: false })
155
+
156
+ expect(container.querySelector('.no-subtext')).not.toBeTruthy()
157
+ })
158
+
159
+ it('Should not include no-subtext class if cta_text is given and hideCategory is true', () => {
160
+ // @ts-ignore
161
+ const { container } = render(Playlink, { playlink: { ...playlink, cta_text: 'Some text' }, hideCategory: true })
162
+
163
+ expect(container.querySelector('.no-subtext')).not.toBeTruthy()
164
+ })
134
165
  })
@@ -3,6 +3,10 @@ import { describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import PlaylinkIcon from '../../../../routes/components/Playlinks/PlaylinkIcon.svelte'
5
5
 
6
+ vi.mock('$lib/consent', () => ({
7
+ hasConsentedTo: vi.fn(() => true),
8
+ }))
9
+
6
10
  describe('PlaylinkIcon.svelte', () => {
7
11
  const playlink = { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } }
8
12
 
@@ -24,4 +28,12 @@ describe('PlaylinkIcon.svelte', () => {
24
28
  expect(getByAltText(playlink.name).getAttribute('width')).toBe('20')
25
29
  expect(getByAltText(playlink.name).getAttribute('height')).toBe('20')
26
30
  })
31
+
32
+ it('Should render given tracking pixels', () => {
33
+ const playlink = { name: 'Some playlink', logo_url: 'logo', pixels: ['https://some-pixel.com/a.jpg'], extra_info: { category: 'SVOD' } }
34
+ // @ts-ignore
35
+ const { container } = render(PlaylinkIcon, { playlink })
36
+
37
+ expect(container.querySelector('img[src*="https://some-pixel.com/a.jpg"]')).toBeTruthy()
38
+ })
27
39
  })
@@ -1,6 +1,14 @@
1
- import { render } from '@testing-library/svelte'
1
+ import { fireEvent, render } from '@testing-library/svelte'
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest'
3
3
  import PostersScrollReveal from '../../../routes/components/PostersScrollReveal.svelte'
4
+ import { mobileBreakpoint } from '$lib/constants'
5
+ import { isInSplitTestVariant, trackSplitTestAction } from '$lib/splitTest'
6
+
7
+ vi.mock('$lib/splitTest', () => ({
8
+ trackSplitTestView: vi.fn(),
9
+ trackSplitTestAction: vi.fn(),
10
+ isInSplitTestVariant: vi.fn(() => true),
11
+ }))
4
12
 
5
13
  function createPosterLink(top = 0, url = 'poster.jpg') {
6
14
  const link = document.createElement('a')
@@ -22,8 +30,15 @@ describe('FloatingPoster', () => {
22
30
  writable: true,
23
31
  })
24
32
 
33
+ Object.defineProperty(window, 'innerWidth', {
34
+ value: mobileBreakpoint - 1,
35
+ writable: true,
36
+ })
37
+
25
38
  vi.useFakeTimers()
26
39
  vi.resetAllMocks()
40
+
41
+ vi.mocked(isInSplitTestVariant).mockReturnValue(true)
27
42
  })
28
43
 
29
44
  it('Should create poster when link is in viewport', async () => {
@@ -38,6 +53,21 @@ describe('FloatingPoster', () => {
38
53
  expect(poster?.getAttribute('src')).toBe('poster.jpg')
39
54
  })
40
55
 
56
+ it('Should not create posters on desktop when link is in viewport', async () => {
57
+ createPosterLink(500)
58
+
59
+ Object.defineProperty(window, 'innerWidth', {
60
+ value: mobileBreakpoint + 1,
61
+ writable: true,
62
+ })
63
+
64
+ render(PostersScrollReveal)
65
+
66
+ window.dispatchEvent(new Event('scroll'))
67
+
68
+ expect(document.querySelector('[data-playpilot-poster]')).not.toBeTruthy()
69
+ })
70
+
41
71
  it('Should not create poster when link is below in viewport', async () => {
42
72
  createPosterLink(2000)
43
73
 
@@ -117,4 +147,16 @@ describe('FloatingPoster', () => {
117
147
  window.dispatchEvent(new Event('scroll'))
118
148
  expect(document.querySelector('[data-playpilot-poster]')).toBeTruthy()
119
149
  })
150
+
151
+ it('Should fire split test tracking on click', async () => {
152
+ createPosterLink(500)
153
+
154
+ render(PostersScrollReveal)
155
+
156
+ window.dispatchEvent(new Event('scroll'))
157
+
158
+ await fireEvent.click(/** @type {HTMLElement} */ (document.querySelector('[data-playpilot-poster]')))
159
+
160
+ expect(trackSplitTestAction).toHaveBeenCalled()
161
+ })
120
162
  })
@@ -1,4 +1,4 @@
1
- import { render } from '@testing-library/svelte'
1
+ import { render, waitFor } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import TrackingPixels from '../../../routes/components/TrackingPixels.svelte'
@@ -26,4 +26,22 @@ describe('TrackingPixels.svelte', () => {
26
26
 
27
27
  expect(queryByRole('presentation')).not.toBeTruthy()
28
28
  })
29
+
30
+ it('Should re-render when updateconsent is fired', async () => {
31
+ vi.mocked(hasConsentedTo).mockImplementation(() => false)
32
+
33
+ const pixels = ['https://image.com/a.jpg']
34
+ const { queryByRole } = render(TrackingPixels, { pixels })
35
+
36
+ expect(queryByRole('presentation')).not.toBeTruthy()
37
+
38
+ vi.mocked(hasConsentedTo).mockImplementation(() => true)
39
+ window.dispatchEvent(new CustomEvent('updateconsent', {
40
+ bubbles: true,
41
+ }))
42
+
43
+ await waitFor(() => {
44
+ expect(queryByRole('presentation')).toBeTruthy()
45
+ })
46
+ })
29
47
  })