@playpilot/tpi 5.18.1-beta.2 → 5.19.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 (27) hide show
  1. package/dist/link-injections.js +10 -10
  2. package/events.md +3 -1
  3. package/package.json +1 -1
  4. package/src/lib/api/api.ts +21 -2
  5. package/src/lib/enums/TrackingEvent.ts +1 -0
  6. package/src/lib/injection.ts +10 -8
  7. package/src/lib/scss/_mixins.scss +12 -0
  8. package/src/routes/+page.svelte +26 -9
  9. package/src/routes/components/Ads/Display.svelte +2 -2
  10. package/src/routes/components/ListTitle.svelte +3 -3
  11. package/src/routes/components/Modal.svelte +23 -8
  12. package/src/routes/components/{Playlink.svelte → Playlinks/Playlink.svelte} +2 -2
  13. package/src/routes/components/Rails/ParticipantsRail.svelte +27 -14
  14. package/src/routes/components/Title.svelte +3 -3
  15. package/src/routes/elements/+page.svelte +3 -3
  16. package/src/tests/lib/api/api.test.js +41 -2
  17. package/src/tests/routes/+page.test.js +40 -1
  18. package/src/tests/routes/components/{AfterArticlePlaylinks.test.js → Playlinks/AfterArticlePlaylinks.test.js} +5 -2
  19. package/src/tests/routes/components/{Playlink.test.js → Playlinks/Playlink.test.js} +1 -1
  20. package/src/tests/routes/components/{PlaylinkIcon.test.js → Playlinks/PlaylinkIcon.test.js} +1 -1
  21. package/src/tests/routes/components/{PlaylinkLabel.test.js → Playlinks/PlaylinkLabel.test.js} +1 -1
  22. package/src/tests/routes/components/{Playlinks.test.js → Playlinks/Playlinks.test.js} +1 -1
  23. package/src/tests/routes/components/Rails/ParticipantsRail.test.js +17 -19
  24. /package/src/routes/components/{AfterArticlePlaylinks.svelte → Playlinks/AfterArticlePlaylinks.svelte} +0 -0
  25. /package/src/routes/components/{PlaylinkIcon.svelte → Playlinks/PlaylinkIcon.svelte} +0 -0
  26. /package/src/routes/components/{PlaylinkLabel.svelte → Playlinks/PlaylinkLabel.svelte} +0 -0
  27. /package/src/routes/components/{Playlinks.svelte → Playlinks/Playlinks.svelte} +0 -0
package/events.md CHANGED
@@ -56,7 +56,9 @@ Event | Action | Info | Payload
56
56
  Event | Action | Info | Payload
57
57
  --- | --- | --- | ---
58
58
  `ali_injection_failed` | _Fires only inside of the Editor for each injection that failed_ | Only includes visible failures, for instance, it will ignore failures because of already existing links. If a user is shown a message about a failed injection, this event will fire. Includes data on the phrase and sentence for the injection. | `Title`, `phrase`, `sentence`
59
- `ali_injection_count` | _Fires the first time the Editor is shown_ | This logs the total amount of injections, the total amount of failed and manual injections, and the total amount of successful injections. | `total` (number of failed + successsful injections), `failed_automatic`, `failed_manual`, `final_injected` (number of successful injections)
59
+ `ali_injection_count` | _Fires the first time the Editor is shown_ | This logs the total amount of injections, the total amount of failed and manual injections, and the total amount of successful injections. | `total` (number of failed + successful injections),
60
+ `ali_injections_failed_count` | _Fires any time an article included failed injections_ | `total`, `sentence_not_found`, `already_linked`, `broken_html`, `duplicate`, `unknown` (All separate counts of failed injection categories)
61
+ `failed_automatic`, `failed_manual`, `final_injected` (number of successful injections)
60
62
  `ali_fetch_config_failed` | _Fires whenever the config object failed to fetch_ | When this happens, injections are aborted.
61
63
  `ali_auth_failed` | _Fires whenever authentication for the Editor fails._
62
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.18.1-beta.2",
3
+ "version": "5.19.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -1,4 +1,7 @@
1
1
  import { apiBaseUrl } from '$lib/constants'
2
+ import { isEditorialModeEnabled } from './auth'
3
+
4
+ const cache: Record<string, any> = {}
2
5
 
3
6
  type Options = {
4
7
  headers?: Record<string, string>
@@ -7,6 +10,12 @@ type Options = {
7
10
  }
8
11
 
9
12
  export async function api<T>(path: string, { headers = {}, method = 'GET', body = null }: Options = {}): Promise<T> {
13
+ // For GET methods we store all results in a cache object. This cache is super simple and has no invalidation at all.
14
+ // Once something is stored it's there forever since the page is only short lived.
15
+ // We never use the cache for editiorial mode since in the editor requests do change.
16
+ const useCache = !isEditorialModeEnabled() && method === 'GET'
17
+ if (useCache && (path in cache)) return cache[path]
18
+
10
19
  const baseHeaders = { 'Content-Type': 'application/json' }
11
20
 
12
21
  const response = await fetch(apiBaseUrl + path, {
@@ -19,9 +28,19 @@ export async function api<T>(path: string, { headers = {}, method = 'GET', body
19
28
 
20
29
  const text = await response.text()
21
30
 
31
+ let parsed: unknown = text
32
+
22
33
  try {
23
- return JSON.parse(text)
34
+ if (text) parsed = JSON.parse(text)
24
35
  } catch {
25
- return text as T
36
+ // Ignore
26
37
  }
38
+
39
+ if (useCache) cache[path] = parsed
40
+
41
+ return parsed as T
42
+ }
43
+
44
+ export function clearCache(): void {
45
+ for(const key in cache) delete cache[key]
27
46
  }
@@ -30,6 +30,7 @@ export const TrackingEvent = Object.freeze({
30
30
  // Fails
31
31
  InjectionFailed: 'ali_injection_failed',
32
32
  TotalInjectionsCount: 'ali_injection_count',
33
+ FailedInjectionsCount: 'ali_failed_injection_count',
33
34
  FetchingConfigFailed: 'ali_fetch_config_failed',
34
35
  AuthFailed: 'ali_auth_failed',
35
36
 
@@ -1,6 +1,6 @@
1
1
  import { mount, unmount } from 'svelte'
2
2
  import TitlePopover from '../routes/components/TitlePopover.svelte'
3
- import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
3
+ import AfterArticlePlaylinks from '../routes/components/Playlinks/AfterArticlePlaylinks.svelte'
4
4
  import { cleanPhrase, findNumberOfMatchesInString, findShortestMatchBetweenPhrases, findTextNodeContaining, getIndexOfPhraseInElement, getNumberOfLeadingAndTrailingSpaces, isNodeInLink, replaceBetween, replaceStartingFrom } from './text'
5
5
  import type { LinkInjection, LinkInjectionTypes } from './types/injection'
6
6
  import { isHoldingSpecialKey } from './event'
@@ -322,13 +322,15 @@ function addCSSVariablesToLinks(): void {
322
322
  '--playpilot-injection-background-color-hover',
323
323
  ]
324
324
 
325
- createdLinkElements.forEach(element => {
326
- variables.forEach((value) => {
327
- if (getComputedStyle(element).getPropertyValue(value)) {
328
- element.dataset.usedCssVariables = `${element.dataset.usedCssVariables || ''} ${value}`
329
- }
330
- })
331
- })
325
+ for (const element of createdLinkElements) {
326
+ const style = getComputedStyle(element)
327
+
328
+ for (const value of variables) {
329
+ if (!style.getPropertyValue(value)) continue
330
+
331
+ element.dataset.usedCssVariables = `${element.dataset.usedCssVariables || ''} ${value}`
332
+ }
333
+ }
332
334
  }
333
335
 
334
336
  /**
@@ -19,3 +19,15 @@
19
19
  }
20
20
  }
21
21
  }
22
+
23
+ @mixin desktop() {
24
+ @media (min-width: 600px) {
25
+ @content;
26
+ }
27
+ }
28
+
29
+ @mixin motion() {
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ @content;
32
+ }
33
+ }
@@ -132,15 +132,7 @@
132
132
  window.PlayPilotLinkInjections.evaluated_link_injections = filteredInjections
133
133
  }
134
134
 
135
- const successfulInjections = filteredInjections.filter(i => !i.failed)
136
-
137
- if (!successfulInjections.length) return
138
- if (isEditorialMode) return
139
-
140
- track(TrackingEvent.ArticleInjected, null, {
141
- manual: successfulInjections.filter(i => i.manual).length,
142
- ai: successfulInjections.filter(i => !i.manual).length,
143
- })
135
+ if (!isEditorialMode) trackInjections(filteredInjections)
144
136
  }
145
137
 
146
138
  // Set elements to be used by script, if a selector is passed from the config request we update
@@ -170,6 +162,31 @@
170
162
 
171
163
  if (!existingElement) document.body.appendChild(styleElement)
172
164
  }
165
+
166
+ function trackInjections(filteredInjections: LinkInjection[]): void {
167
+ const successfulInjections = filteredInjections.filter(i => !i.failed)
168
+ const failedInjections = filteredInjections.filter(i => i.failed)
169
+
170
+ if (successfulInjections.length) {
171
+ track(TrackingEvent.ArticleInjected, null, {
172
+ manual: successfulInjections.filter(i => i.manual).length,
173
+ ai: successfulInjections.filter(i => !i.manual).length,
174
+ })
175
+ }
176
+
177
+ if (failedInjections.length) {
178
+ const countForMessage = (message: string): number => failedInjections.filter(({ failed_message }) => failed_message?.includes(message)).length
179
+
180
+ track(TrackingEvent.FailedInjectionsCount, null, {
181
+ total: failedInjections.length,
182
+ sentence_not_found: countForMessage('sentence was not found'),
183
+ already_linked: countForMessage('already inside of a link'),
184
+ broken_html: countForMessage('broken HTML'),
185
+ duplicate: countForMessage('duplicate'),
186
+ unknown: countForMessage('unknown reasons'),
187
+ })
188
+ }
189
+ }
173
190
  </script>
174
191
 
175
192
  <div class="playpilot-link-injections" data-playpilot-link-injections>
@@ -104,7 +104,7 @@
104
104
  font-size: margin(0.75);
105
105
  text-decoration: underline;
106
106
 
107
- @media (min-width: 600px) {
107
+ @include desktop() {
108
108
  padding: margin(1);
109
109
 
110
110
  .compact & {
@@ -127,7 +127,7 @@
127
127
  font-size: margin(0.875);
128
128
  text-decoration: none;
129
129
 
130
- @media (min-width: 600px) {
130
+ @include desktop() {
131
131
  padding: margin(1);
132
132
 
133
133
  .compact & {
@@ -7,8 +7,8 @@
7
7
  import type { TitleData } from '$lib/types/title'
8
8
  import IconArrow from './Icons/IconArrow.svelte'
9
9
  import IconIMDb from './Icons/IconIMDb.svelte'
10
- import PlaylinkIcon from './PlaylinkIcon.svelte'
11
- import PlaylinkLabel from './PlaylinkLabel.svelte'
10
+ import PlaylinkIcon from './Playlinks/PlaylinkIcon.svelte'
11
+ import PlaylinkLabel from './Playlinks/PlaylinkLabel.svelte'
12
12
  import TitlePoster from './TitlePoster.svelte'
13
13
 
14
14
  interface Props {
@@ -124,7 +124,7 @@
124
124
  font-size: var(--playpilot-detail-font-size, 14px);
125
125
  line-height: normal;
126
126
 
127
- @media (min-width: 600px) {
127
+ @include desktop() {
128
128
  padding-right: margin(1);
129
129
  }
130
130
  }
@@ -32,6 +32,7 @@
32
32
  }: Props = $props()
33
33
 
34
34
  const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
35
+ const historyHash = '#playpilot'
35
36
 
36
37
  let windowWidth = $state(0)
37
38
  let modalElement: HTMLElement | null = $state(null)
@@ -52,9 +53,20 @@
52
53
 
53
54
  hasPreviousModal = !!getPreviousModal()
54
55
 
56
+ // Add modal state to the browser history. This allows us to close to modal when using the back button.
57
+ // Only do this for the very first modal opened in the stack. The back button always fully closes all modals.
58
+ if (!hasPreviousModal) window.history.pushState({ modal: true }, '', historyHash)
59
+
55
60
  requestAnimationFrame(setInitialScrollPosition)
56
61
 
57
- return () => document.body.style.overflowY = baseOverflowStyle || ''
62
+ return () => {
63
+ document.body.style.overflowY = baseOverflowStyle || ''
64
+
65
+ // Remove the modal entry from above from the browser history. This is done only once for the entire stack.
66
+ // We check for getPreviousModal not because we really care about the previous modal, but to check if all
67
+ // modals were closed and none will open again within this stack.
68
+ if (!getPreviousModal() && history.state?.modal) history.back()
69
+ }
58
70
  })
59
71
 
60
72
  function setInitialScrollPosition(): void {
@@ -70,7 +82,10 @@
70
82
  }
71
83
  </script>
72
84
 
73
- <svelte:window onkeydown={({ key }) => { if (key === 'Escape') destroyAllModals() }} bind:innerWidth={windowWidth} />
85
+ <svelte:window
86
+ onkeydown={({ key }) => { if (key === 'Escape') destroyAllModals() }}
87
+ onhashchange={() => destroyAllModals()}
88
+ bind:innerWidth={windowWidth} />
74
89
 
75
90
  <div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
76
91
  {#if prepend}
@@ -133,7 +148,7 @@
133
148
  left: 0;
134
149
  background: var(--playpilot-detail-backdrop, rgba(0, 0, 0, 0.65));
135
150
 
136
- @media (min-width: $max-width) {
151
+ @include desktop() {
137
152
  padding: margin(2) 0;
138
153
  }
139
154
 
@@ -159,7 +174,7 @@
159
174
  max-height: 80dvh;
160
175
  }
161
176
 
162
- @media (min-width: $max-width) {
177
+ @include desktop() {
163
178
  max-height: unset;
164
179
  margin-top: 0;
165
180
  padding-bottom: 0;
@@ -217,7 +232,7 @@
217
232
  top: calc(var(--dialog-offset) + margin(1));
218
233
  right: margin(1);
219
234
 
220
- @media (min-width: $max-width) {
235
+ @include desktop() {
221
236
  position: absolute;
222
237
  top: margin(1);
223
238
  }
@@ -253,7 +268,7 @@
253
268
  padding-top: margin(3);
254
269
  margin: auto auto margin(0.5);
255
270
 
256
- @media (min-width: $max-width) {
271
+ @include desktop() {
257
272
  padding-top: 0;
258
273
  margin-top: 0;
259
274
  }
@@ -266,7 +281,7 @@
266
281
  max-width: $max-width;
267
282
  margin: margin(0.5);
268
283
 
269
- @media (min-width: $max-width) {
284
+ @include desktop() {
270
285
  width: 100%;
271
286
  margin: 0 0 margin(0.5);
272
287
  }
@@ -275,7 +290,7 @@
275
290
  width: 100%;
276
291
  margin: auto 0 0;
277
292
 
278
- @media (min-width: $max-width) {
293
+ @include desktop() {
279
294
  margin-top: 0;
280
295
  }
281
296
  }
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import Disclaimer from './Ads/Disclaimer.svelte'
2
+ import Disclaimer from '../Ads/Disclaimer.svelte'
3
3
  import { hasConsentedTo } from '$lib/consent'
4
4
  import { removeImageUrlPrefix } from '$lib/image'
5
5
  import { t } from '$lib/localization'
@@ -119,7 +119,7 @@
119
119
  opacity: 0.35;
120
120
  pointer-events: none;
121
121
 
122
- @media (prefers-reduced-motion: no-preference) {
122
+ @include motion() {
123
123
  animation: sheen 4000ms ease-in-out infinite;
124
124
  }
125
125
  }
@@ -1,30 +1,43 @@
1
1
  <script lang="ts">
2
+ import { participants } from '$lib/fakeData'
2
3
  import { openModal } from '$lib/modal'
3
4
  import type { ParticipantData } from '$lib/types/participant'
4
5
  import Rail from './Rail.svelte'
5
6
 
6
- interface Props {
7
- participants: ParticipantData[]
8
- }
7
+ async function fetchParticipants(): Promise<ParticipantData[]> {
8
+ // This is just a fake loading state for now
9
+ await new Promise(res => setTimeout(res, 500))
9
10
 
10
- const { participants }: Props = $props()
11
+ return participants
12
+ }
11
13
  </script>
12
14
 
13
- <Rail heading="Cast">
14
- {#each participants.slice(0, 15) as participant}
15
- <button class="participant" onclick={event => openModal({ event, type: 'participant', data: participant })}>
16
- <span class="truncate">{participant.name}</span>
15
+ {#await fetchParticipants()}
16
+ <Rail heading="Cast">
17
+ {#each { length: 5 }}
18
+ <div class="participant"></div>
19
+ {/each}
20
+ </Rail>
21
+ {:then participants}
22
+ {#if participants?.length}
23
+ <Rail heading="Cast">
24
+ {#each participants.slice(0, 15) as participant}
25
+ <button class="participant" data-testid="participant" onclick={event => openModal({ event, type: 'participant', data: participant })}>
26
+ <span class="truncate">{participant.name}</span>
17
27
 
18
- <div class="character truncate">{participant.character}</div>
19
- </button>
20
- {/each}
21
- </Rail>
28
+ <div class="character truncate">{participant.character}</div>
29
+ </button>
30
+ {/each}
31
+ </Rail>
32
+ {/if}
33
+ {/await}
22
34
 
23
35
  <style lang="scss">
24
36
  .participant {
25
37
  display: block;
26
38
  flex: 0 0 10rem;
27
39
  width: 10rem;
40
+ min-height: margin(3.375); // Matches 54 pixels, the height of a card with both name and character
28
41
  padding: margin(0.5);
29
42
  border: 0;
30
43
  border-radius: var(--playpilot-cast-border-radius, var(--playpilot-playlink-border-radius, margin(0.5)));
@@ -36,8 +49,8 @@
36
49
  font-size: var(--playpilot-cast-font-size, var(--playpilot-playlinks-font-size, margin(0.75)));
37
50
  white-space: nowrap;
38
51
 
39
- &:hover,
40
- &:active {
52
+ &:is(button):hover,
53
+ &:is(button):active {
41
54
  filter: var(--playpilot-cast-hover-filter, var(--playpilot-playlink-hover-filter, brightness(1.1)));
42
55
  text-decoration: none !important;
43
56
  }
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Genres from './Genres.svelte'
3
- import Playlinks from './Playlinks.svelte'
3
+ import Playlinks from './Playlinks/Playlinks.svelte'
4
4
  import Description from './Description.svelte'
5
5
  import IconIMDb from './Icons/IconIMDb.svelte'
6
6
  import ParticipantsRail from './Rails/ParticipantsRail.svelte'
@@ -58,8 +58,8 @@
58
58
 
59
59
  <!-- Temporarily not available on production as there is not yet an API endpoint for either -->
60
60
  {#if process.env.NODE_ENV !== 'production'}
61
- {#if true || title.participants?.length}
62
- <ParticipantsRail participants={participants} />
61
+ {#if true}
62
+ <ParticipantsRail />
63
63
  {/if}
64
64
 
65
65
  <SimilarRail />
@@ -5,11 +5,11 @@
5
5
  import Disclaimer from '../components/Ads/Disclaimer.svelte'
6
6
  import Display from '../components/Ads/Display.svelte'
7
7
  import TopScroll from '../components/Ads/TopScroll.svelte'
8
- import AfterArticlePlaylinks from '../components/AfterArticlePlaylinks.svelte'
8
+ import AfterArticlePlaylinks from '../components/Playlinks/AfterArticlePlaylinks.svelte'
9
9
  import Description from '../components/Description.svelte'
10
10
  import Genres from '../components/Genres.svelte'
11
- import Playlink from '../components/Playlink.svelte'
12
- import Playlinks from '../components/Playlinks.svelte'
11
+ import Playlink from '../components/Playlinks/Playlink.svelte'
12
+ import Playlinks from '../components/Playlinks/Playlinks.svelte'
13
13
  import Popover from '../components/Popover.svelte'
14
14
  import RoundButton from '../components/RoundButton.svelte'
15
15
  import SkeletonText from '../components/SkeletonText.svelte'
@@ -1,9 +1,16 @@
1
- import { beforeEach, describe, expect, it } from 'vitest'
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
2
3
  import { fakeFetch } from '../../helpers'
3
- import { api } from '$lib/api/api'
4
+ import { api, clearCache } from '$lib/api/api'
5
+ import { isEditorialModeEnabled } from '$lib/api/auth'
6
+
7
+ vi.mock('$lib/api/auth', () => ({
8
+ isEditorialModeEnabled: vi.fn(() => false),
9
+ }))
4
10
 
5
11
  describe('$lib/api/api', () => {
6
12
  beforeEach(() => {
13
+ clearCache()
7
14
  fakeFetch()
8
15
  })
9
16
 
@@ -51,5 +58,37 @@ describe('$lib/api/api', () => {
51
58
  await expect(async () => await api('/some-path')).rejects.toThrow()
52
59
  })
53
60
  })
61
+
62
+ describe('cache', () => {
63
+ it('Should fetch only once if multiple requests are made for the same path', async () => {
64
+ await api('/some-path')
65
+ await api('/some-path')
66
+
67
+ expect(global.fetch).toHaveBeenCalledTimes(1)
68
+ })
69
+
70
+ it('Should fetch multiple times if multiple requests are made for different paths', async () => {
71
+ await api('/some-path')
72
+ await api('/some-other-path')
73
+
74
+ expect(global.fetch).toHaveBeenCalledTimes(2)
75
+ })
76
+
77
+ it('Should not cache POST requests', async () => {
78
+ await api('/some-path', { method: 'POST' })
79
+ await api('/some-path', { method: 'POST' })
80
+
81
+ expect(global.fetch).toHaveBeenCalledTimes(2)
82
+ })
83
+
84
+ it('Should not cache when in editorial mode', async () => {
85
+ vi.mocked(isEditorialModeEnabled).mockReturnValueOnce(true)
86
+
87
+ await api('/some-path')
88
+ await api('/some-path')
89
+
90
+ expect(global.fetch).toHaveBeenCalledTimes(2)
91
+ })
92
+ })
54
93
  })
55
94
  })
@@ -344,7 +344,46 @@ describe('$routes/+page.svelte', () => {
344
344
 
345
345
  await waitFor(() => expect(injectLinksInDocument).toHaveBeenCalled())
346
346
 
347
- expect(track).toHaveBeenCalledOnce()
347
+ expect(track).not.toHaveBeenCalledWith(TrackingEvent.ArticleInjected, null, expect.any(Object))
348
+ })
349
+
350
+ it('Should track failed injections event when injections are injected with failures', async () => {
351
+ const injections = [
352
+ { ...generateInjection('a', 'b'), failed: true, failed_message: 'sentence was not found' },
353
+ { ...generateInjection('a', 'b'), failed: true, failed_message: 'already inside of a link' },
354
+ { ...generateInjection('a', 'b'), failed: true, failed_message: 'already inside of a link' },
355
+ { ...generateInjection('a', 'b'), failed: true, failed_message: 'broken HTML' },
356
+ { ...generateInjection('a', 'b'), failed: true, failed_message: 'broken HTML' },
357
+ { ...generateInjection('a', 'b'), failed: true, failed_message: 'duplicate' },
358
+ { ...generateInjection('a', 'b'), failed: true, failed_message: 'unknown reasons' },
359
+ ]
360
+
361
+ // @ts-ignore
362
+ vi.mocked(pollLinkInjections).mockResolvedValueOnce({ manual_injections: injections })
363
+ vi.mocked(injectLinksInDocument).mockReturnValueOnce(injections)
364
+
365
+ render(page)
366
+
367
+ await waitFor(() => {
368
+ expect(track).toHaveBeenCalledWith(TrackingEvent.FailedInjectionsCount, null, {
369
+ total: injections.length,
370
+ sentence_not_found: 1,
371
+ already_linked: 2,
372
+ broken_html: 2,
373
+ duplicate: 1,
374
+ unknown: 1,
375
+ })
376
+ })
377
+ })
378
+
379
+ it('Should not track failed injection event when no injections failed', async () => {
380
+ // @ts-ignore
381
+ vi.mocked(pollLinkInjections).mockResolvedValueOnce({ ai_injections: [generateInjection('a', 'b')], manual_injections: [{ ...generateInjection('a', 'b'), manual: true }] })
382
+ vi.mocked(injectLinksInDocument).mockReturnValueOnce([generateInjection('a', 'b'), { ...generateInjection('a', 'b'), manual: true }])
383
+
384
+ render(page)
385
+
386
+ expect(track).not.toHaveBeenCalledWith(TrackingEvent.FailedInjectionsCount, null, expect.any(Object))
348
387
  })
349
388
 
350
389
  it('Should not initialize if isCrawler is true', async () => {
@@ -1,7 +1,7 @@
1
1
  import { fireEvent, render } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import AfterArticlePlaylinks from '../../../routes/components/AfterArticlePlaylinks.svelte'
4
+ import AfterArticlePlaylinks from '../../../../routes/components/Playlinks/AfterArticlePlaylinks.svelte'
5
5
  import { title } from '$lib/fakeData'
6
6
  import { track } from '$lib/tracking'
7
7
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
@@ -69,7 +69,7 @@ describe('AfterArticlePlaylinks.svelte', () => {
69
69
  })
70
70
 
71
71
  it('Should not render "and" after final playlink when only 1 playlink is present', () => {
72
- /** @type {LinkInjection} */
72
+ /** @type {import('$lib/types/injection').LinkInjection} */
73
73
  const injection = { ...linkInjections[0], title_details: { ...linkInjections[0].title_details, providers: [linkInjections[0].title_details.providers[0]] }}
74
74
  const { container } = render(AfterArticlePlaylinks, { linkInjections: [injection] })
75
75
 
@@ -88,6 +88,7 @@ describe('AfterArticlePlaylinks.svelte', () => {
88
88
  })
89
89
 
90
90
  it('Should render titles without playlinks as empty state', () => {
91
+ /** @type {import('$lib/types/injection').LinkInjection} */
91
92
  const injection = { ...linkInjections[0], title_details: { ...linkInjections[0].title_details, providers: [] }}
92
93
  const { getByText } = render(AfterArticlePlaylinks, { linkInjections: [injection] })
93
94
 
@@ -95,6 +96,7 @@ describe('AfterArticlePlaylinks.svelte', () => {
95
96
  })
96
97
 
97
98
  it('Should display as modal button if after_article_style is modal_button', () => {
99
+ /** @type {import('$lib/types/injection').LinkInjection} */
98
100
  const injection = { ...linkInjections[0], after_article_style: 'modal_button' }
99
101
  const { getByText } = render(AfterArticlePlaylinks, { linkInjections: [injection] })
100
102
 
@@ -104,6 +106,7 @@ describe('AfterArticlePlaylinks.svelte', () => {
104
106
 
105
107
  it('Should fire given onclickmodal function when display as modal_button', async () => {
106
108
  const onclickmodal = vi.fn()
109
+ /** @type {import('$lib/types/injection').LinkInjection} */
107
110
  const injection = { ...linkInjections[0], after_article_style: 'modal_button' }
108
111
  const { getByText } = render(AfterArticlePlaylinks, { linkInjections: [injection], onclickmodal })
109
112
 
@@ -1,7 +1,7 @@
1
1
  import { fireEvent, render } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import Playlink from '../../../routes/components/Playlink.svelte'
4
+ import Playlink from '../../../../routes/components/Playlinks/Playlink.svelte'
5
5
  import { hasConsentedTo } from '$lib/consent'
6
6
 
7
7
  vi.mock('$lib/tracking', () => ({
@@ -1,7 +1,7 @@
1
1
  import { fireEvent, render } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import PlaylinkIcon from '../../../routes/components/PlaylinkIcon.svelte'
4
+ import PlaylinkIcon from '../../../../routes/components/Playlinks/PlaylinkIcon.svelte'
5
5
 
6
6
  describe('PlaylinkIcon.svelte', () => {
7
7
  const playlink = { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } }
@@ -1,7 +1,7 @@
1
1
  import { fireEvent, render } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import PlaylinkLabel from '../../../routes/components/PlaylinkLabel.svelte'
4
+ import PlaylinkLabel from '../../../../routes/components/Playlinks/PlaylinkLabel.svelte'
5
5
 
6
6
  describe('PlaylinkLabel.svelte', () => {
7
7
  const playlink = { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } }
@@ -1,7 +1,7 @@
1
1
  import { fireEvent, render } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import Playlinks from '../../../routes/components/Playlinks.svelte'
4
+ import Playlinks from '../../../../routes/components/Playlinks/Playlinks.svelte'
5
5
  import { title } from '$lib/fakeData'
6
6
  import { track } from '$lib/tracking'
7
7
  import { TrackingEvent } from '$lib/enums/TrackingEvent'