@playpilot/tpi 5.17.0 → 5.18.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.
Files changed (73) hide show
  1. package/dist/link-injections.js +9 -9
  2. package/eslint.config.js +16 -0
  3. package/events.md +3 -0
  4. package/package.json +1 -1
  5. package/src/app.d.ts +13 -13
  6. package/src/lib/actions/heading.ts +2 -2
  7. package/src/lib/actions/middlemouse.ts +2 -2
  8. package/src/lib/{ads.ts → api/ads.ts} +19 -22
  9. package/src/lib/api/api.ts +21 -0
  10. package/src/lib/{auth.ts → api/auth.ts} +8 -12
  11. package/src/lib/api/config.ts +16 -0
  12. package/src/lib/{api.ts → api/externalPages.ts} +17 -48
  13. package/src/lib/api/search.ts +14 -0
  14. package/src/lib/{session.ts → api/session.ts} +3 -3
  15. package/src/lib/array.ts +2 -2
  16. package/src/lib/consent.ts +1 -1
  17. package/src/lib/data/translations.ts +1 -1
  18. package/src/lib/enums/SplitTest.ts +2 -2
  19. package/src/lib/enums/TrackingEvent.ts +5 -0
  20. package/src/lib/event.ts +1 -1
  21. package/src/lib/fakeData.ts +4 -4
  22. package/src/lib/hash.ts +1 -1
  23. package/src/lib/image.ts +2 -2
  24. package/src/lib/{linkInjection.ts → injection.ts} +25 -18
  25. package/src/lib/meta.ts +1 -1
  26. package/src/lib/modal.ts +1 -1
  27. package/src/lib/playlink.ts +1 -1
  28. package/src/lib/routes.ts +9 -0
  29. package/src/lib/splitTest.ts +4 -4
  30. package/src/lib/text.ts +1 -1
  31. package/src/lib/token.ts +3 -0
  32. package/src/lib/tracking.ts +5 -5
  33. package/src/lib/types/global.d.ts +1 -1
  34. package/src/lib/types/injection.d.ts +1 -1
  35. package/src/lib/types/script.d.ts +18 -3
  36. package/src/lib/types/session.d.ts +1 -1
  37. package/src/lib/types/title.d.ts +2 -2
  38. package/src/main.ts +12 -12
  39. package/src/routes/+page.svelte +5 -4
  40. package/src/routes/components/Debugger.svelte +26 -1
  41. package/src/routes/components/Editorial/Editor.svelte +7 -7
  42. package/src/routes/components/Editorial/EditorItem.svelte +1 -1
  43. package/src/routes/components/Editorial/ManualInjection.svelte +8 -8
  44. package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +1 -1
  45. package/src/routes/components/Editorial/Search/TitleSearch.svelte +1 -1
  46. package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +3 -2
  47. package/src/routes/components/Editorial/Session.svelte +1 -1
  48. package/src/routes/components/ListTitle.svelte +2 -2
  49. package/src/routes/components/ParticipantModal.svelte +11 -0
  50. package/src/routes/components/Playlinks.svelte +1 -1
  51. package/src/routes/components/Rails/SimilarRail.svelte +9 -1
  52. package/src/routes/components/Rails/TitlesRail.svelte +11 -7
  53. package/src/routes/components/TitleModal.svelte +3 -3
  54. package/src/routes/components/TitlePopover.svelte +1 -1
  55. package/src/tests/lib/{ads.test.js → api/ads.test.js} +17 -14
  56. package/src/tests/lib/api/api.test.js +49 -0
  57. package/src/tests/lib/{auth.test.js → api/auth.test.js} +10 -23
  58. package/src/tests/lib/api/config.test.js +53 -0
  59. package/src/tests/lib/{api.test.js → api/externalPages.test.js} +71 -101
  60. package/src/tests/lib/{search.test.js → api/search.test.js} +10 -9
  61. package/src/tests/lib/{session.test.js → api/session.test.js} +4 -4
  62. package/src/tests/lib/{linkInjection.test.js → injections.test.js} +26 -2
  63. package/src/tests/lib/routes.test.js +15 -0
  64. package/src/tests/routes/+page.test.js +17 -9
  65. package/src/tests/routes/components/Editorial/Editor.test.js +3 -3
  66. package/src/tests/routes/components/Editorial/EditorItem.test.js +1 -1
  67. package/src/tests/routes/components/Editorial/ManualInjection.test.js +2 -2
  68. package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +2 -2
  69. package/src/tests/routes/components/Editorial/Session.test.js +2 -2
  70. package/src/tests/routes/components/ParticipantModal.test.js +35 -0
  71. package/src/tests/routes/components/Rails/{TitleRail.test.js → TitlesRail.test.js} +10 -1
  72. package/src/tests/setup.js +2 -0
  73. package/src/lib/search.ts +0 -23
@@ -0,0 +1,3 @@
1
+ export function getApiToken(): string | undefined {
2
+ return window.PlayPilotLinkInjections?.token
3
+ }
@@ -1,7 +1,7 @@
1
- import { hasConsentedTo } from "./consent"
2
- import { mobileBreakpoint } from "./constants"
3
- import type { TitleData } from "./types/title"
4
- import { getFullUrlPath } from "./url"
1
+ import { hasConsentedTo } from './consent'
2
+ import { mobileBreakpoint } from './constants'
3
+ import type { TitleData } from './types/title'
4
+ import { getFullUrlPath } from './url'
5
5
 
6
6
  const baseUrl = 'https://insights.playpilot.net'
7
7
 
@@ -53,7 +53,7 @@ export async function track(event: string, title: TitleData | null = null, paylo
53
53
  }
54
54
 
55
55
  /** Save this event to the window object. Used when calling .debug() */
56
- function pushEventToWindow(data: { event: string, payload: Record<string, any> }) {
56
+ function pushEventToWindow(data: { event: string, payload: Record<string, any> }): void {
57
57
  if (!window.PlayPilotLinkInjections.tracked_events) window.PlayPilotLinkInjections.tracked_events = []
58
58
  window.PlayPilotLinkInjections.tracked_events?.push(data)
59
59
  }
@@ -1,4 +1,4 @@
1
- import type { ScriptConfig } from "./script"
1
+ import type { ScriptConfig } from './script'
2
2
 
3
3
  declare global {
4
4
  interface Window {
@@ -1,4 +1,4 @@
1
- import type { TitleData } from "./title"
1
+ import type { TitleData } from './title'
2
2
 
3
3
  export type LinkInjection = {
4
4
  sid: string
@@ -1,21 +1,36 @@
1
- import type { Campaign } from "./campaign"
2
- import type { ConsentOptions } from "./consent"
3
- import type { LinkInjection } from "./injection"
1
+ import type { Campaign } from './campaign'
2
+ import type { ConsentOptions } from './consent'
3
+ import type { LinkInjection } from './injection'
4
4
 
5
5
  export type ScriptConfig = {
6
+ // The API token to authenticate with the backend.
6
7
  token: string
8
+ // Editiorial token as given by the Partner Portal, required to access and authenticate the Editor.
7
9
  editorial_token?: string
10
+ // The organization sid is returned from the external pages endpoint and is set by the script.
8
11
  organization_sid?: string | null,
12
+ // The domain sid is returned from the external pages endpoint and is set by the script.
9
13
  domain_sid?: string | null,
14
+ // Selector for the article content. Should be as narrow as possible. Will fall back to `article` > `main` > `body` if not provided. Can be set via config endpoint.
10
15
  selector?: string
16
+ // Selector for the after article playlinks. Will be placed _after_ the given selector.
11
17
  after_article_selector?: string
18
+ // Selector to change the insert position of the after article playlinks. Should only be used if no viable selector can be given with `after_article_selector`.
12
19
  after_article_insert_position?: InsertPosition | ''
20
+ // The language of the page will be inferred from the html `lang` attribute. Can be set manually using this option in the config object.
13
21
  language?: string | null
22
+ // Set to the last success external-pages fetch.
14
23
  last_successful_fetch?: LinkInjectionResponse | null
24
+ // Lists all tracked events through the `track()` function.
15
25
  tracked_events?: { event: string, payload: Record<string, any> }[]
26
+ // Lists all split test identifiers as created by each running split test. Identifiers are only created when a split test is fired.
16
27
  split_test_identifiers?: Record<string, number>
28
+ // All link injections as returned from external-pages, with AI and manual injections merged.
17
29
  evaluated_link_injections?: LinkInjection[]
30
+ // By default the script requires consent from the user through tcfapi. Can be disabled using this setting.
18
31
  require_consent?: boolean
32
+ // Used to check if a user has consented via tcfapi to various consent categories.
19
33
  consents?: ConsentOptions
34
+ // All ads as fetched from the ads endpoint. This is used as the primary store for ads, each individual ads gets it's data from here.
20
35
  ads?: Campaign[]
21
36
  }
@@ -1,4 +1,4 @@
1
- import type { LinkInjectionResponse } from "./injection"
1
+ import type { LinkInjectionResponse } from './injection'
2
2
 
3
3
  export type SessionResponse = Omit<
4
4
  LinkInjectionResponse,
@@ -1,5 +1,5 @@
1
- import type { ParticipantData } from "./participant"
2
- import type { PlaylinkData } from "./playlink"
1
+ import type { ParticipantData } from './participant'
2
+ import type { PlaylinkData } from './playlink'
3
3
 
4
4
  export type TitleData = {
5
5
  sid: string
package/src/main.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { mount } from 'svelte'
2
2
  import App from './routes/+page.svelte'
3
- import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/linkInjection'
3
+ import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/injection'
4
4
  import { getPageMetaData } from '$lib/meta'
5
5
  import { setConsent } from '$lib/consent'
6
6
  import type { Campaign } from '$lib/types/campaign'
@@ -81,36 +81,36 @@ window.PlayPilotLinkInjections = {
81
81
  const elements = getLinkInjectionElements(parentElement)
82
82
 
83
83
  console.groupCollapsed('Config')
84
- console.table(Object.entries(this))
84
+ console.table(Object.entries(this))
85
85
  console.groupEnd()
86
86
 
87
87
  console.groupCollapsed('Elements')
88
- console.log('Parent element', parentElement)
89
- console.log('Valid elements', elements)
88
+ console.log('Parent element', parentElement)
89
+ console.log('Valid elements', elements)
90
90
  console.groupEnd()
91
91
 
92
92
  console.groupCollapsed('Last fetch')
93
- console.log(this.last_successful_fetch)
93
+ console.log(this.last_successful_fetch)
94
94
  console.groupEnd()
95
95
 
96
96
  console.groupCollapsed('Meta')
97
- console.log(getPageMetaData())
97
+ console.log(getPageMetaData())
98
98
  console.groupEnd()
99
99
 
100
100
  console.groupCollapsed('Page text')
101
- console.log(getPageText(elements))
101
+ console.log(getPageText(elements))
102
102
  console.groupEnd()
103
103
 
104
104
  console.groupCollapsed('Evaluated injections')
105
- console.log(this.evaluated_link_injections)
105
+ console.log(this.evaluated_link_injections)
106
106
  console.groupEnd()
107
107
 
108
108
  console.groupCollapsed('Tracked events')
109
- console.log(this.tracked_events)
109
+ console.log(this.tracked_events)
110
110
  console.groupEnd()
111
111
 
112
112
  console.groupCollapsed('Split tests')
113
- console.log(this.split_test_identifiers)
113
+ console.log(this.split_test_identifiers)
114
114
  console.groupEnd()
115
115
  },
116
116
 
@@ -135,13 +135,13 @@ window.PlayPilotLinkInjections = {
135
135
  subheader: 'Some cta subheader',
136
136
  url: 'https://google.com/',
137
137
  image: null,
138
- image_uuid: null
138
+ image_uuid: null,
139
139
  },
140
140
  disclaimer: 'Some disclaimer',
141
141
  }
142
142
 
143
143
  this.ads = [{ ...campaign, ...override }]
144
- }
144
+ },
145
145
  }
146
146
 
147
147
  export default window.PlayPilotLinkInjections
@@ -1,12 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte'
3
- import { fetchConfig, pollLinkInjections } from '$lib/api'
4
- import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
3
+ import { pollLinkInjections } from '$lib/api/externalPages'
4
+ import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/injection'
5
5
  import { setTrackingSids, track } from '$lib/tracking'
6
6
  import { getFullUrlPath } from '$lib/url'
7
7
  import { isCrawler } from '$lib/crawler'
8
8
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
- import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/auth'
9
+ import { fetchAds } from '$lib/api/ads'
10
+ import { fetchConfig } from '$lib/api/config'
11
+ import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/api/auth'
10
12
  import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
11
13
  import Editor from './components/Editorial/Editor.svelte'
12
14
  import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
@@ -14,7 +16,6 @@
14
16
  import TrackingPixels from './components/TrackingPixels.svelte'
15
17
  import Consent from './components/Consent.svelte'
16
18
  import Debugger from './components/Debugger.svelte'
17
- import { fetchAds } from '$lib/ads'
18
19
 
19
20
  let parentElement: HTMLElement | null = $state(null)
20
21
  let elements: HTMLElement[] = $state([])
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { SplitTest } from '$lib/enums/SplitTest'
2
3
  import { onDestroy } from 'svelte'
3
4
 
4
5
  const secrets = ['tpidebug', 'debugtpi']
@@ -12,7 +13,7 @@
12
13
  if (interval) clearInterval(interval)
13
14
  })
14
15
 
15
- function dataToReadable() {
16
+ function dataToReadable(): Record<string, undefined | Record<string, any>[]> {
16
17
  const data = window.PlayPilotLinkInjections
17
18
 
18
19
  const succesfulInjections = data.evaluated_link_injections?.filter(injection => !injection.failed) || []
@@ -65,6 +66,16 @@
65
66
  disclaimer: 'Disclaimer',
66
67
  })
67
68
  }
69
+
70
+ function forceSplitTestVariant(variantIndex: number): void {
71
+ window.PlayPilotLinkInjections.split_test_identifiers = {}
72
+
73
+ Object.values(SplitTest).forEach(({ key, numberOfVariants }) => {
74
+ const variantDivide = 1 / numberOfVariants
75
+
76
+ window.PlayPilotLinkInjections.split_test_identifiers![key] = Math.min(variantDivide * variantIndex, 1)
77
+ })
78
+ }
68
79
  </script>
69
80
 
70
81
  <svelte:window {onkeydown} />
@@ -98,6 +109,14 @@
98
109
 
99
110
  <button onclick={() => insertMockAd('card')}>Insert mock card ad</button>
100
111
  <button onclick={() => insertMockAd('top_scroll')}>Insert mock top scroll ad</button>
112
+
113
+ <hr />
114
+
115
+ <small>Force split test variant</small>
116
+
117
+ {#each { length: 4 }, i}
118
+ <button onclick={() => forceSplitTestVariant(i)}>{i}</button>
119
+ {/each}
101
120
  </div>
102
121
  {/if}
103
122
 
@@ -119,6 +138,11 @@
119
138
  color: var(--playpilot-primary);
120
139
  font-family: inherit;
121
140
  font-weight: bold;
141
+
142
+ &:hover {
143
+ color: black;
144
+ background: var(--playpilot-primary);
145
+ }
122
146
  }
123
147
 
124
148
  .debugger {
@@ -134,6 +158,7 @@
134
158
  overflow: auto;
135
159
  color: white;
136
160
  font-family: "Consolas", monospace;
161
+ color: var(--playpilot-primary);
137
162
  }
138
163
 
139
164
  .item {
@@ -1,4 +1,11 @@
1
1
  <script lang="ts">
2
+ import type { Position } from '$lib/types/position'
3
+ import type { LinkInjection } from '$lib/types/injection'
4
+ import { track } from '$lib/tracking'
5
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
6
+ import { heading } from '$lib/actions/heading'
7
+ import { separateLinkInjectionTypes } from '$lib/injection'
8
+ import { saveLinkInjections } from '$lib/api/externalPages'
2
9
  import { fade, fly, slide } from 'svelte/transition'
3
10
  import EditorItem from './EditorItem.svelte'
4
11
  import DragHandle from './DragHandle.svelte'
@@ -7,15 +14,8 @@
7
14
  import ManualInjection from './ManualInjection.svelte'
8
15
  import RoundButton from '../RoundButton.svelte'
9
16
  import Session from './Session.svelte'
10
- import { saveLinkInjections } from '$lib/api'
11
17
  import { untrack } from 'svelte'
12
18
  import AIIndicator from './AIIndicator.svelte'
13
- import type { Position } from '$lib/types/position'
14
- import type { LinkInjection } from '$lib/types/injection'
15
- import { track } from '$lib/tracking'
16
- import { TrackingEvent } from '$lib/enums/TrackingEvent'
17
- import { heading } from '$lib/actions/heading'
18
- import { separateLinkInjectionTypes } from '$lib/linkInjection'
19
19
 
20
20
  interface Props {
21
21
  linkInjections: LinkInjection[],
@@ -13,7 +13,7 @@
13
13
  import type { LinkInjection } from '$lib/types/injection'
14
14
  import type { TitleData } from '$lib/types/title'
15
15
  import { cleanPhrase, truncateAroundPhrase } from '$lib/text'
16
- import { getLinkInjectionElements, getLinkInjectionsParentElement, isValidPlaylinkType } from '$lib/linkInjection'
16
+ import { getLinkInjectionElements, getLinkInjectionsParentElement, isValidPlaylinkType } from '$lib/injection'
17
17
  import { imagePlaceholderDataUrl } from '$lib/constants'
18
18
  import { removeImageUrlPrefix } from '$lib/image'
19
19
  import ReportIssueModal from './ReportIssueModal.svelte'
@@ -1,18 +1,18 @@
1
1
  <script lang="ts">
2
+ import type { LinkInjection } from '$lib/types/injection'
3
+ import type { TitleData } from '$lib/types/title'
2
4
  import { onMount } from 'svelte'
5
+ import { playPilotBaseUrl } from '$lib/constants'
6
+ import { generateInjectionKey } from '$lib/api/externalPages'
7
+ import { getLinkInjectionsParentElement } from '$lib/injection'
8
+ import { heading } from '$lib/actions/heading'
9
+ import { findSurroundingPhrases, cleanPhrase } from '$lib/text'
10
+ import { getIndexOfSelection } from '$lib/selection'
3
11
  import IconBack from '../Icons/IconBack.svelte'
4
12
  import RoundButton from '../RoundButton.svelte'
5
13
  import Alert from './Alert.svelte'
6
14
  import TextInput from './TextInput.svelte'
7
15
  import TitleSearch from './Search/TitleSearch.svelte'
8
- import { playPilotBaseUrl } from '$lib/constants'
9
- import { generateInjectionKey } from '$lib/api'
10
- import { getLinkInjectionsParentElement } from '$lib/linkInjection'
11
- import type { LinkInjection } from '$lib/types/injection'
12
- import type { TitleData } from '$lib/types/title'
13
- import { heading } from '$lib/actions/heading'
14
- import { findSurroundingPhrases, cleanPhrase } from '$lib/text'
15
- import { getIndexOfSelection } from '$lib/selection'
16
16
 
17
17
  interface Props {
18
18
  pageText: string
@@ -3,7 +3,7 @@
3
3
  import IconAlign from '../Icons/IconAlign.svelte'
4
4
  import Switch from './Switch.svelte'
5
5
  import type { LinkInjection } from '$lib/types/injection'
6
- import { isValidPlaylinkType } from '$lib/linkInjection'
6
+ import { isValidPlaylinkType } from '$lib/injection'
7
7
  import Alert from './Alert.svelte'
8
8
 
9
9
  interface Props {
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { searchTitles } from '$lib/search'
2
+ import { searchTitles } from '$lib/api/search'
3
3
  import type { TitleData } from '$lib/types/title'
4
4
  import TextInput from '../TextInput.svelte'
5
5
  import TitleSearchItem from './TitleSearchItem.svelte'
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
- import { imagePlaceholderDataUrl, playPilotBaseUrl } from '$lib/constants'
2
+ import { imagePlaceholderDataUrl } from '$lib/constants'
3
3
  import { removeImageUrlPrefix } from '$lib/image'
4
+ import { titleUrl } from '$lib/routes'
4
5
  import type { TitleData } from '$lib/types/title'
5
6
  import IconIMDb from '../../Icons/IconIMDb.svelte'
6
7
  import IconNewTab from '../../Icons/IconNewTab.svelte'
@@ -36,7 +37,7 @@
36
37
  </div>
37
38
 
38
39
  <a
39
- href="{playPilotBaseUrl}/{title.type}/{title.slug}"
40
+ href={titleUrl(title)}
40
41
  target="_blank"
41
42
  class="open-in-new-tab"
42
43
  onclick={event => event.stopImmediatePropagation()}>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { fetchAsSession, isAllowedToEdit, saveCurrentSession, sessionPollPeriodMilliseconds } from '$lib/session'
2
+ import { fetchAsSession, isAllowedToEdit, saveCurrentSession, sessionPollPeriodMilliseconds } from '$lib/api/session'
3
3
  import { onMount } from 'svelte'
4
4
  import Alert from './Alert.svelte'
5
5
 
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { playPilotBaseUrl } from '$lib/constants'
2
+ import { titleUrl } from '$lib/routes'
3
3
  import { SplitTest } from '$lib/enums/SplitTest'
4
4
  import { t } from '$lib/localization'
5
5
  import { mergePlaylinks } from '$lib/playlink'
@@ -29,7 +29,7 @@
29
29
  }
30
30
  </script>
31
31
 
32
- <a class="title" href="{playPilotBaseUrl}/{title.type}/{title.slug}" {onclick}>
32
+ <a class="title" href={titleUrl(title)} {onclick}>
33
33
  <div class="poster">
34
34
  <TitlePoster {title} width={30} height={43} />
35
35
  </div>
@@ -3,6 +3,9 @@
3
3
  import Modal from './Modal.svelte'
4
4
  import Participant from './Participant.svelte'
5
5
  import type { ParticipantData } from '$lib/types/participant'
6
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
7
+ import { track } from '$lib/tracking'
8
+ import { onMount } from 'svelte'
6
9
 
7
10
  interface Props {
8
11
  participant: ParticipantData
@@ -22,6 +25,14 @@
22
25
  }, initialScrollPosition = 0 }: Props = $props()
23
26
 
24
27
  let windowWidth = $state(0)
28
+
29
+ track(TrackingEvent.ParticipantModalView, null, { participant: participant.name })
30
+
31
+ onMount(() => {
32
+ const openTimestamp = Date.now()
33
+
34
+ return () => track(TrackingEvent.ParticipantModalClose, null, { participant: participant.name, time_spent: Date.now() - openTimestamp })
35
+ })
25
36
  </script>
26
37
 
27
38
  <svelte:window bind:innerWidth={windowWidth} />
@@ -6,9 +6,9 @@
6
6
  import type { PlaylinkData } from '$lib/types/playlink'
7
7
  import type { TitleData } from '$lib/types/title'
8
8
  import { heading } from '$lib/actions/heading'
9
+ import { campaignToPlaylink, getFirstAdOfType } from '$lib/api/ads'
9
10
  import { getContext } from 'svelte'
10
11
  import Playlink from './Playlink.svelte'
11
- import { campaignToPlaylink, getFirstAdOfType } from '$lib/ads'
12
12
 
13
13
  interface Props {
14
14
  playlinks: PlaylinkData[]
@@ -1,7 +1,15 @@
1
1
  <script lang="ts">
2
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
+ import { track } from '$lib/tracking'
2
4
  import type { TitleData } from '$lib/types/title'
3
5
  import TitlesRail from './TitlesRail.svelte'
4
6
 
7
+ interface Props {
8
+ title: TitleData
9
+ }
10
+
11
+ const { title }: Props = $props()
12
+
5
13
  const titles = fetchTitles()
6
14
 
7
15
  async function fetchTitles(): Promise<TitleData[]> {
@@ -13,4 +21,4 @@
13
21
  }
14
22
  </script>
15
23
 
16
- <TitlesRail {titles} heading="Similar movies & shows" />
24
+ <TitlesRail {titles} heading="Similar movies & shows" onclick={(targetTitle) => track(TrackingEvent.SimilarTitleClick, targetTitle, { title_source: title.original_title })} />
@@ -1,16 +1,22 @@
1
1
  <script lang="ts">
2
2
  import TitlePoster from '../TitlePoster.svelte'
3
3
  import Rail from './Rail.svelte'
4
- import { playPilotBaseUrl } from '$lib/constants'
5
4
  import type { TitleData } from '$lib/types/title'
6
5
  import { openModal } from '$lib/modal'
6
+ import { titleUrl } from '$lib/routes'
7
7
 
8
8
  interface Props {
9
9
  titles: Promise<TitleData[]> | TitleData[]
10
- heading?: string
10
+ heading?: string,
11
+ onclick?: (title: TitleData) => void
11
12
  }
12
13
 
13
- const { titles, heading = '' }: Props = $props()
14
+ const { titles, heading = '', onclick = () => null }: Props = $props()
15
+
16
+ function openTitle(event: MouseEvent, title: TitleData): void {
17
+ openModal({ event, data: title })
18
+ onclick(title)
19
+ }
14
20
  </script>
15
21
 
16
22
  <div class="titles">
@@ -28,14 +34,12 @@
28
34
  {/each}
29
35
  {:then titles}
30
36
  {#each titles as title}
31
- {@const href = `${playPilotBaseUrl}/${title.type}/${title.slug}`}
32
-
33
37
  <div class="title" data-testid="title">
34
- <a class="poster" {href} onclick={(event) => openModal({ event, data: title })}>
38
+ <a class="poster" href={titleUrl(title)} onclick={(event) => openTitle(event, title)}>
35
39
  <TitlePoster {title} width={96} height={144} />
36
40
  </a>
37
41
 
38
- <a {href} class="heading" onclick={(event) => openModal({ event, data: title })}>
42
+ <a href={titleUrl(title)} class="heading" onclick={(event) => openTitle(event, title)}>
39
43
  {title.title}
40
44
  </a>
41
45
  </div>
@@ -2,12 +2,12 @@
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 { getFirstAdOfType } from '$lib/api/ads'
5
6
  import { onMount } from 'svelte'
6
7
  import Modal from './Modal.svelte'
7
8
  import Title from './Title.svelte'
8
9
  import TopScroll from './Ads/TopScroll.svelte'
9
10
  import Display from './Ads/Display.svelte'
10
- import { getFirstAdOfType } from '$lib/ads'
11
11
 
12
12
  interface Props {
13
13
  title: TitleData
@@ -19,10 +19,10 @@
19
19
  const topScrollAd = getFirstAdOfType('top_scroll')
20
20
  const displayAd = getFirstAdOfType('card')
21
21
 
22
- track(TrackingEvent.TitleModalView, title)
23
-
24
22
  let hasTrackedScrolling = false
25
23
 
24
+ track(TrackingEvent.TitleModalView, title)
25
+
26
26
  onMount(() => {
27
27
  const openTimestamp = Date.now()
28
28
 
@@ -1,7 +1,7 @@
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
+ import { getFirstAdOfType } from '$lib/api/ads'
5
5
  import type { TitleData } from '$lib/types/title'
6
6
  import { onMount } from 'svelte'
7
7
  import Popover from './Popover.svelte'
@@ -1,10 +1,15 @@
1
1
  import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
2
- import { fakeFetch } from '../helpers'
3
2
 
4
- import { fetchAds, getFirstAdOfType } from '$lib/ads'
3
+ import { fetchAds, getFirstAdOfType } from '$lib/api/ads'
5
4
  import { hasConsentedTo } from '$lib/consent'
6
5
  import { track } from '$lib/tracking'
7
6
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
7
+ import { api } from '$lib/api/api'
8
+
9
+ vi.mock('$lib/api/api', () => ({
10
+ api: vi.fn(),
11
+ }))
12
+
8
13
 
9
14
  vi.mock('$lib/tracking', () => ({
10
15
  track: vi.fn(),
@@ -14,9 +19,10 @@ vi.mock('$lib/consent', () => ({
14
19
  hasConsentedTo: vi.fn(() => true),
15
20
  }))
16
21
 
17
- describe('$lib/ads', () => {
22
+ describe('$lib/api/ads', () => {
18
23
  afterEach(() => {
19
24
  vi.resetAllMocks()
25
+
20
26
  // @ts-ignore
21
27
  window.PlayPilotLinkInjections = null
22
28
 
@@ -29,35 +35,32 @@ describe('$lib/ads', () => {
29
35
  window.PlayPilotLinkInjections = { token: 'a' }
30
36
  })
31
37
 
32
- it('Should call fetch with given url', async () => {
33
- fakeFetch({ response: 'Some response' })
38
+ it('Should call api with given url', async () => {
39
+ vi.mocked(api).mockResolvedValueOnce('Some response')
34
40
 
35
41
  const response = await fetchAds()
36
42
 
37
43
  expect(response).toBe('Some response')
38
- expect(global.fetch).toHaveBeenCalledWith(
39
- expect.stringContaining('api-token'),
40
- expect.objectContaining({}),
41
- )
44
+ expect(api).toHaveBeenCalledWith(expect.stringContaining('api-token'))
42
45
  })
43
46
 
44
- it('Should not call fetch if no api token is present', async () => {
47
+ it('Should not call api if no api token is present', async () => {
45
48
  // @ts-ignore
46
49
  window.PlayPilotLinkInjections = { token: '' }
47
50
 
48
51
  await expect(async () => await fetchAds()).rejects.toThrowError('No token was provided')
49
- expect(global.fetch).not.toHaveBeenCalled()
52
+ expect(api).not.toHaveBeenCalled()
50
53
  })
51
54
 
52
- it('Should not call fetch if user did not consent to ads', async () => {
55
+ it('Should not call api if user did not consent to ads', async () => {
53
56
  vi.mocked(hasConsentedTo).mockImplementation(() => false)
54
57
 
55
58
  await fetchAds()
56
- expect(global.fetch).not.toHaveBeenCalled()
59
+ expect(api).not.toHaveBeenCalled()
57
60
  })
58
61
 
59
62
  it('Should fire track event when ads failed to fetch', async () => {
60
- fakeFetch({ ok: false, status: 505 })
63
+ vi.mocked(api).mockRejectedValueOnce({ ok: false, status: 505 })
61
64
 
62
65
  await expect(async () => await fetchAds()).rejects.toThrowError()
63
66
  expect(track).toHaveBeenCalledWith(TrackingEvent.AdsFetchFailed, null, { status: 505 })
@@ -0,0 +1,49 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+ import { fakeFetch } from '../../helpers'
3
+ import { api } from '$lib/api/api'
4
+
5
+ describe('$lib/api/api', () => {
6
+ beforeEach(() => {
7
+ fakeFetch()
8
+ })
9
+
10
+ describe('$lib/api', () => {
11
+ describe('api', () => {
12
+ it('Should fetch and return result', async () => {
13
+ fakeFetch({ response: 'Some response' })
14
+
15
+ const result = await api('/some-path')
16
+
17
+ expect(global.fetch).toHaveBeenCalledWith(
18
+ expect.stringContaining('/some-path'),
19
+ expect.objectContaining({
20
+ headers: expect.any(Object),
21
+ body: null,
22
+ method: 'GET',
23
+ }),
24
+ )
25
+ expect(result).toBe('Some response')
26
+ })
27
+
28
+ it('Should fetch with POST and given stringified body', async () => {
29
+ fakeFetch({ response: 'Some response' })
30
+
31
+ await api('/some-path', { method: 'POST', body: { key: 'some value' } })
32
+
33
+ expect(global.fetch).toHaveBeenCalledWith(
34
+ expect.stringContaining('/some-path'),
35
+ expect.objectContaining({
36
+ method: 'POST',
37
+ body: '{"key":"some value"}',
38
+ }),
39
+ )
40
+ })
41
+
42
+ it('Should return error when response was not ok', async () => {
43
+ fakeFetch({ ok: false })
44
+
45
+ await expect(async () => await api('/some-path')).rejects.toThrow()
46
+ })
47
+ })
48
+ })
49
+ })