@playpilot/tpi 8.14.0 → 8.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.14.0",
3
+ "version": "8.15.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -26,8 +26,6 @@ export async function fetchLinkInjections(
26
26
  const apiUrl = `/external-pages/?api-token=${apiToken}&include_title_details=true${isEditorialMode ? '&editorial_mode_enabled=true' : ''}&language=${language}`
27
27
  let response: LinkInjectionResponse
28
28
 
29
- // We use separate requests when running the AI or setting the editor session vs when only getting the results.
30
- // For regular requests we use a GET endpoint, but when saving data we POST to the same url.
31
29
  if (method === 'POST') {
32
30
  const { pageText } = getPageTextAndElements()
33
31
 
@@ -44,14 +42,11 @@ export async function fetchLinkInjections(
44
42
  body: params,
45
43
  })
46
44
  } else {
47
- // When getting injections without posting we append the URL of the page to the URL for the request.
48
- // All other params are only relevant during the POST request.
49
45
  response = await api<LinkInjectionResponse>(apiUrl + `&url=${url}`, {
50
46
  method: 'GET',
51
47
  })
52
48
  }
53
49
 
54
- // This is used when debugging (using window.PlayPilotLinkInjections.debug())
55
50
  window.PlayPilotLinkInjections.last_successful_fetch = response
56
51
 
57
52
  return response
@@ -67,8 +62,6 @@ export async function pollLinkInjections(
67
62
  ): Promise<LinkInjectionResponse> {
68
63
  let currentTry = 0
69
64
 
70
- // Clear pollTimeout if it is already running to prevent multiple timeouts from running at the same time
71
- // This is mostly handy during HMR, but also during navigation changes
72
65
  if (pollTimeout) clearTimeout(pollTimeout)
73
66
 
74
67
  const poll = async (resolve: Function, reject: Function): Promise<void> => {
@@ -27,3 +27,13 @@ export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]>
27
27
 
28
28
  return response.results
29
29
  }
30
+
31
+ export async function fetchTitleBySid(sid: string): Promise<TitleData> {
32
+ const data = await fetchTitles({ sids: sid, no_region_filter: true })
33
+
34
+ const title = data.results[0]
35
+
36
+ if (!title) throw new Error('No title was returned for sid: ' + sid)
37
+
38
+ return title
39
+ }
@@ -286,6 +286,11 @@ export const translations = {
286
286
  [Language.Swedish]: 'Utforska',
287
287
  [Language.Danish]: 'Udforsk',
288
288
  },
289
+ 'Page Not Found': {
290
+ [Language.English]: 'Page not found',
291
+ [Language.Swedish]: 'Sidan hittades inte',
292
+ [Language.Danish]: 'Siden blev ikke fundet',
293
+ },
289
294
  'Mentioned In This Article': {
290
295
  [Language.English]: 'Mentioned in this article',
291
296
  [Language.Swedish]: 'Nämnda i den här artikeln',
@@ -5,6 +5,7 @@ import { destroyAllModals, openModalForInjectedLink } from './modal'
5
5
  import { clearCurrentlyHoveredInjection, destroyLinkPopover, destroyLinkPopoverOnMouseleave, isPopoverActive, openPopoverForInjectedLink } from './popover'
6
6
  import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './afterArticle'
7
7
  import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
8
+ import { exploreTitleUrl, titleUrl } from './routes'
8
9
  import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
9
10
 
10
11
  export const keyDataAttribute = 'data-playpilot-injection-key'
@@ -202,11 +203,15 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
202
203
  const injectionElement = document.createElement('span')
203
204
  injectionElement.dataset.playpilotInjectionKey = injection.key
204
205
 
206
+ const openInExplore = !!window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore
207
+
208
+ const href = openInExplore ? exploreTitleUrl(injection.title_details!) : titleUrl(injection.title_details!)
209
+
205
210
  const linkElement = document.createElement('a')
206
211
  linkElement.dataset.playpilotPosterUrl = injection.title_details?.standing_poster
207
212
  linkElement.innerText = injection.title
208
- linkElement.href = injection.playpilot_url
209
- linkElement.target = '_blank'
213
+ linkElement.href = href
214
+ linkElement.target = openInExplore ? '' : '_blank'
210
215
  linkElement.rel = 'noopener nofollow noreferrer'
211
216
 
212
217
  injectionElement.insertAdjacentElement('beforeend', linkElement)
@@ -311,7 +316,12 @@ function addCSSVariablesToLinks(): void {
311
316
 
312
317
  function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
313
318
  window.addEventListener('mousemove', destroyLinkPopoverOnMouseleave)
314
- window.addEventListener('click', (event) => openModalForInjectedLink(event, injections))
319
+
320
+ window.addEventListener('click', (event) => {
321
+ if (window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore) return
322
+
323
+ openModalForInjectedLink(event, injections)
324
+ })
315
325
 
316
326
  const createdInjectionElements = document.querySelectorAll<HTMLElement>(keySelector)
317
327
 
package/src/lib/routes.ts CHANGED
@@ -4,3 +4,7 @@ import type { TitleData } from './types/title'
4
4
  export function titleUrl(title: TitleData): string {
5
5
  return `${playPilotBaseUrl}/${title.type}/${title.slug}/`
6
6
  }
7
+
8
+ export function exploreTitleUrl(title: TitleData): string {
9
+ return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=modal&sid=${title.sid}`
10
+ }
@@ -97,6 +97,11 @@ export type ConfigResponse = {
97
97
  in_text_disclaimer_selector?: string
98
98
  in_text_disclaimer_insert_position?: InsertPosition
99
99
 
100
+ /**
101
+ * Open TPI links in explore rather than in modals in the article
102
+ */
103
+ open_tpi_links_in_explore?: boolean
104
+
100
105
  /**
101
106
  * These options are all relevant for the Explore component, which can be inserted as a widget on any page or as a modal.
102
107
  * `explore_navigation_selector` is used to select the navigation element that should be copied and inserted _after_.
@@ -1,4 +1,5 @@
1
1
  export type ExploreRoute = {
2
2
  key: string
3
3
  component: any
4
+ appendComponent?: any
4
5
  }
@@ -35,6 +35,7 @@
35
35
  <style lang="scss">
36
36
  .description:first-child {
37
37
  margin: margin(-1) 0 margin(1);
38
+ max-width: theme(description-max-width, 100%);
38
39
  }
39
40
 
40
41
  .paragraph {
@@ -7,6 +7,8 @@
7
7
  import ExploreHome from './Routes/ExploreHome.svelte'
8
8
  import ExploreResults from './Routes/ExploreResults.svelte'
9
9
  import ExploreLayout from './ExploreLayout.svelte'
10
+ import ExploreTitle from './Routes/ExploreTitle.svelte'
11
+ import ExploreModal from './Routes/ExploreModal.svelte'
10
12
 
11
13
  const routes: ExploreRoute[] = [
12
14
  {
@@ -20,25 +22,59 @@
20
22
  key: 'home',
21
23
  component: ExploreHome,
22
24
  })
25
+
26
+ routes.push({
27
+ key: 'title',
28
+ component: ExploreTitle,
29
+ })
30
+
31
+ routes.push({
32
+ key: 'modal',
33
+ component: ExploreHome,
34
+ appendComponent: ExploreModal,
35
+ })
23
36
  }
24
37
 
25
- let currentRoute: ExploreRoute = $state(routes[0])
38
+ const initialRouteKey = getCurrentRouteParam() || routes[0].key
39
+
40
+ let currentRoute: ExploreRoute = $state(routes.find(({ key }) => key === initialRouteKey) || routes[0])
26
41
  let searchQuery: string = $state('')
27
42
  let filter: ExploreFilter = $state({})
28
43
 
29
44
  const CurrentRouteComponent = $derived(currentRoute.component)
45
+ const CurrentAppendComponent = $derived(currentRoute.appendComponent)
30
46
 
31
47
  $effect(() => {
32
48
  if (searchQuery) currentRoute = routes.find(route => route.key === 'results')!
33
49
  })
34
50
 
35
- function navigate(key: string): void {
51
+ function navigate(key: string, pushState: boolean = true): void {
36
52
  currentRoute = routes.find(route => route.key === key) || routes[0]
37
53
 
54
+ const currentUrl = new URL(document.location.toString())
55
+
56
+ if (key !== 'title' && key !== 'modal') currentUrl.searchParams.delete('sid')
57
+
58
+ if (key === routes[0].key) currentUrl.searchParams.delete('route')
59
+ else currentUrl.searchParams.set('route', currentRoute.key)
60
+
61
+ if (pushState) history.pushState({}, '', currentUrl)
62
+
38
63
  track(TrackingEvent.ExploreNavigate, null, { route: currentRoute.key })
39
64
  }
65
+
66
+ function getCurrentRouteParam(): string {
67
+ return new URL(document.location.toString()).searchParams.get('route') || routes[0].key
68
+ }
69
+
70
+ function onhashchange(): void {
71
+ navigate(getCurrentRouteParam(), false)
72
+ }
40
73
  </script>
41
74
 
75
+ <svelte:window on:popstate={onhashchange} />
76
+
42
77
  <ExploreLayout {navigate} bind:searchQuery bind:filter>
43
78
  <CurrentRouteComponent {searchQuery} {filter} {navigate} />
79
+ <CurrentAppendComponent {navigate} />
44
80
  </ExploreLayout>
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
3
+ import { openModal } from '$lib/modal'
4
+ import type { TitleData } from '$lib/types/title'
5
+
6
+ interface Props {
7
+ navigate?: (key: string, pushState?: boolean) => void
8
+ }
9
+
10
+ const { navigate = () => null }: Props = $props()
11
+
12
+ openModalViaRoute()
13
+
14
+ async function openModalViaRoute(): Promise<void> {
15
+ const currentUrl = new URL(document.location.toString())
16
+ const sid = currentUrl.searchParams.get('sid')
17
+
18
+ if (!sid) return
19
+
20
+ try {
21
+ const [title, railTitles] = (await Promise.allSettled([
22
+ fetchTitleBySid(sid),
23
+ fetchSimilarTitles({ sid } as unknown as TitleData)])
24
+ ).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
25
+
26
+ openModal({
27
+ type: 'titles-rail',
28
+ data: [(title as TitleData), ...(railTitles as TitleData[])],
29
+ props: {
30
+ onclose: () => navigate('home'),
31
+ pushState: false,
32
+ },
33
+ })
34
+ } catch {
35
+ navigate('home')
36
+ }
37
+ }
38
+ </script>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { onDestroy } from 'svelte'
2
3
  import { fetchTitles } from '$lib/api/titles'
3
4
  import { MetaEvent, TrackingEvent } from '$lib/enums/TrackingEvent'
4
5
  import { openModal } from '$lib/modal'
@@ -18,8 +19,7 @@
18
19
 
19
20
  interface Props {
20
21
  searchQuery?: string,
21
- filter?: ExploreFilter,
22
- navigate?: (key: string) => void
22
+ filter?: ExploreFilter
23
23
  }
24
24
 
25
25
  const { searchQuery = '', filter = {} }: Props = $props()
@@ -51,6 +51,10 @@
51
51
  if (filter) setFilter()
52
52
  })
53
53
 
54
+ onDestroy(() => {
55
+ emptyFilter()
56
+ })
57
+
54
58
  async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
55
59
  latestRequestId += 1
56
60
  const requestId = latestRequestId
@@ -108,6 +112,12 @@
108
112
  promise = getTitlesForFilter()
109
113
  }
110
114
 
115
+ function emptyFilter(): void {
116
+ for (const key of Object.keys(filter)) {
117
+ delete filter[key]
118
+ }
119
+ }
120
+
111
121
  function resetTitles(): void {
112
122
  page = 1
113
123
  titles = []
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ import { fetchTitleBySid } from '$lib/api/titles'
3
+ import { mobileBreakpoint } from '$lib/constants'
4
+ import { t } from '$lib/localization'
5
+ import Button from '../../Button.svelte'
6
+ import IconArrow from '../../Icons/IconArrow.svelte'
7
+ import Title from '../../Title.svelte'
8
+
9
+ interface Props {
10
+ navigate?: (key: string) => void
11
+ }
12
+
13
+ const { navigate = () => null }: Props = $props()
14
+
15
+ let screenWidth = $state(window.innerWidth)
16
+
17
+ const isMobile = $derived(screenWidth < mobileBreakpoint)
18
+ const sid = new URL(document.location.toString()).searchParams.get('sid')
19
+ </script>
20
+
21
+ <svelte:window bind:innerWidth={screenWidth} />
22
+
23
+ {#snippet empty()}
24
+ {@render back()}
25
+
26
+ <p class="empty">{t('Page Not Found')}</p>
27
+ {/snippet}
28
+
29
+ {#snippet back(offset = false)}
30
+ <div class="back" class:offset>
31
+ <Button variant="link" onclick={() => { navigate('home') }}>
32
+ <IconArrow direction="left" />
33
+ {t('Home')}
34
+ </Button>
35
+ </div>
36
+ {/snippet}
37
+
38
+ {#if sid}
39
+ {#await fetchTitleBySid(sid)}
40
+ {@render back()}
41
+
42
+ Loading...
43
+ {:then title}
44
+ {@render back(true)}
45
+
46
+ <div class="title" data-testid="title">
47
+ <Title {title} useVideoBackground={isMobile} />
48
+ </div>
49
+ {:catch}
50
+ {@render empty()}
51
+ {/await}
52
+ {:else}
53
+ {@render empty()}
54
+ {/if}
55
+
56
+ <style lang="scss">
57
+ .title {
58
+ --playpilot-description-max-width: 600px;
59
+ --playpilot-playlinks-max-width: 600px;
60
+ position: relative;
61
+ border-radius: theme(border-radius);
62
+ overflow: hidden;
63
+ margin-top: margin(1);
64
+
65
+ @include desktop() {
66
+ --playpilot-detail-header-offset: #{margin(5)};
67
+ --playpilot-detail-background-height: #{margin(35)};
68
+ --playpilot-detail-background-opacity: 0.5;
69
+ }
70
+ }
71
+
72
+ .back {
73
+ --playpilot-button-text-color: white;
74
+ font-weight: theme(font-bold);
75
+
76
+ &.offset {
77
+ z-index: 1;
78
+ position: absolute;
79
+ margin-left: margin(1);
80
+ margin-top: margin(1);
81
+ }
82
+ }
83
+
84
+ .empty {
85
+ margin-top: margin(1);
86
+ padding: margin(1);
87
+ border: 1px solid theme(content);
88
+ max-width: margin(20);
89
+ border-radius: theme(border-radius);
90
+ font-size: theme(font-size-large);
91
+ font-weight: theme(font-bold);
92
+ color: theme(text-color);
93
+ }
94
+ </style>
@@ -21,6 +21,7 @@
21
21
  blur?: boolean
22
22
  closeButtonStyle?: 'shadow' | 'flat'
23
23
  initialScrollPosition?: number
24
+ pushState?: boolean
24
25
  onscroll?: () => void
25
26
  onclose?: () => void
26
27
  }
@@ -35,6 +36,7 @@
35
36
  blur = false,
36
37
  closeButtonStyle = 'shadow',
37
38
  initialScrollPosition = 0,
39
+ pushState = true,
38
40
  onscroll = () => null,
39
41
  onclose = () => null,
40
42
  }: Props = $props()
@@ -63,7 +65,7 @@
63
65
 
64
66
  // Add modal state to the browser history. This allows us to close to modal when using the back button.
65
67
  // Only do this for the very first modal opened in the stack. The back button always fully closes all modals.
66
- if (!hasPreviousModal) window.history.pushState({ modal: true }, '', historyHash)
68
+ if (pushState && !hasPreviousModal) window.history.pushState({ modal: true }, '', historyHash)
67
69
 
68
70
  requestAnimationFrame(setInitialScrollPosition)
69
71
 
@@ -12,10 +12,11 @@
12
12
  items: Record<string, any>[]
13
13
  initialIndex?: number
14
14
  onchange?: (index: number) => void
15
+ onclose?: () => void
15
16
  each: Snippet<[item: any, currentIndex: number]>
16
17
  }
17
18
 
18
- const { items, initialIndex = 0, onchange = () => null, each }: Props = $props()
19
+ const { items, initialIndex = 0, onchange = () => null, onclose = () => null, each, ...restProps }: Props = $props()
19
20
 
20
21
  const transitionDuration = 300
21
22
 
@@ -35,7 +36,7 @@
35
36
  }
36
37
  </script>
37
38
 
38
- <Modal blur>
39
+ <Modal blur {onclose} {...restProps}>
39
40
  {#snippet dialog()}
40
41
  <div class="rail-modal" style:--transition-duration="{transitionDuration}ms">
41
42
  <TinySlider threshold={40} moveThreshold={40} transitionDuration={initialized ? transitionDuration : 0} bind:this={slider}>
@@ -70,7 +71,7 @@
70
71
  </div>
71
72
 
72
73
  <div class="close" transition:scale|global>
73
- <RoundButton size="42px" onclick={() => destroyAllModals()} aria-label="Close">
74
+ <RoundButton size="42px" onclick={() => { onclose(); destroyAllModals() }} aria-label="Close">
74
75
  <IconClose size={24} />
75
76
  </RoundButton>
76
77
  </div>
@@ -6,14 +6,15 @@
6
6
  import RailModal from './RailModal.svelte'
7
7
 
8
8
  interface Props {
9
- titles: TitleData[]
10
- initialIndex?: number
9
+ titles: TitleData[],
10
+ initialIndex?: number,
11
+ onclose?: () => void
11
12
  }
12
13
 
13
- const { titles, initialIndex = 0 }: Props = $props()
14
+ const { titles, initialIndex = 0, onclose = () => null, ...restProps }: Props = $props()
14
15
  </script>
15
16
 
16
- <RailModal items={titles} {initialIndex} onchange={(index) => track(TrackingEvent.ExploreTitleRailSetIndex, titles[index], { index })}>
17
+ <RailModal items={titles} {initialIndex} {onclose} onchange={(index) => track(TrackingEvent.ExploreTitleRailSetIndex, titles[index], { index })} {...restProps}>
17
18
  {#snippet each(title, currentIndex)}
18
19
  <Title {title} useVideoBackground={title.sid === titles[currentIndex]?.sid} />
19
20
  {/snippet}
@@ -123,6 +123,7 @@
123
123
  display: grid;
124
124
  grid-template-columns: repeat(auto-fill, minmax(margin(15), 1fr));
125
125
  gap: margin(0.5);
126
+ max-width: theme(playlinks-max-width, 100%);
126
127
 
127
128
  &.list {
128
129
  grid-template-columns: 1fr;
@@ -135,7 +135,7 @@
135
135
 
136
136
  .content {
137
137
  position: relative;
138
- padding: margin(1);
138
+ padding: theme(detail-padding, margin(1));
139
139
  color: theme(detail-text-color, text-color);
140
140
  font-family: theme(detail-font-family, font-family);
141
141
  font-weight: theme(detail-font-weight, normal);
@@ -172,7 +172,7 @@
172
172
 
173
173
  @include desktop {
174
174
  display: block;
175
- padding-top: margin(3);
175
+ padding-top: theme(title-header-offset, margin(3));
176
176
  }
177
177
 
178
178
  .small & {
@@ -217,6 +217,11 @@
217
217
  }
218
218
  }
219
219
 
220
+ .main {
221
+ z-index: 1;
222
+ position: relative;
223
+ }
224
+
220
225
  .imdb {
221
226
  display: flex;
222
227
  align-items: center;
@@ -234,6 +239,8 @@
234
239
  }
235
240
 
236
241
  .actions {
242
+ z-index: 1;
243
+ position: relative;
237
244
  grid-area: actions;
238
245
  display: flex;
239
246
  gap: margin(0.5);
@@ -245,14 +252,15 @@
245
252
  top: 0;
246
253
  left: 0;
247
254
  width: 100%;
248
- height: margin(20);
255
+ height: theme(detail-background-height, margin(20));
249
256
  border-radius: theme(detail-background-border-radius, 0px);
250
257
  overflow: hidden;
251
258
  background: theme(detail-background, lighter);
252
259
  mask-image: linear-gradient(to bottom, black 40%, transparent);
260
+ opacity: theme(detail-background-opacity, 1);
253
261
 
254
262
  @include desktop() {
255
- height: margin(12);
263
+ height: theme(detail-background-height, margin(12));
256
264
  mask-image: linear-gradient(to bottom, black 60%, transparent);
257
265
  }
258
266
 
@@ -29,6 +29,10 @@
29
29
  setTimeout(insertExplore, 100)
30
30
  </script>
31
31
 
32
+ <svelte:head>
33
+ <title>PlayPilot Link Injections - Explore</title>
34
+ </svelte:head>
35
+
32
36
  <button onclick={(() => {
33
37
  destroyExplore()
34
38
  window.PlayPilotLinkInjections.config.explore_use_router = !useExploreRouter()
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
 
3
3
  import { api } from '$lib/api/api'
4
- import { fetchSimilarTitles, fetchTitles } from '$lib/api/titles'
4
+ import { fetchSimilarTitles, fetchTitleBySid, fetchTitles } from '$lib/api/titles'
5
5
  import { title } from '$lib/fakeData'
6
6
  import { getApiToken } from '$lib/token'
7
7
  import { fakeFetch } from '../../helpers'
@@ -74,4 +74,26 @@ describe('$lib/api/titles', () => {
74
74
  await expect(async () => await fetchSimilarTitles(title)).rejects.toThrow()
75
75
  })
76
76
  })
77
+
78
+ describe('fetchTitleBySid', () => {
79
+ it('Should call api with given sid and return the first result', async () => {
80
+ vi.mocked(api).mockResolvedValueOnce({ results: [title] })
81
+
82
+ const response = await fetchTitleBySid(title.sid)
83
+
84
+ expect(api).toHaveBeenCalledWith(`/titles/browse?api-token=some-token&sids=${title.sid}&no_region_filter=true&language=en-US&include_count=false`)
85
+ expect(response).toEqual(title)
86
+ })
87
+
88
+ it('Should throw when no title is returned', async () => {
89
+ vi.mocked(api).mockResolvedValueOnce({ results: [] })
90
+
91
+ await expect(async () => await fetchTitleBySid(title.sid)).rejects.toThrow('No title was returned')
92
+ })
93
+
94
+ it('Should throw when api returns error', async () => {
95
+ vi.mocked(api).mockRejectedValueOnce({ error: 'message' })
96
+ await expect(async () => await fetchTitleBySid(title.sid)).rejects.toThrow()
97
+ })
98
+ })
77
99
  })
@@ -6,6 +6,7 @@ import { mount, unmount } from 'svelte'
6
6
  import { fakeFetch, generateInjection } from '../helpers'
7
7
  import { openModalForInjectedLink } from '$lib/modal'
8
8
  import { getLinkInjectionElements } from '$lib/injectionElements'
9
+ import { titleUrl } from '$lib/routes'
9
10
 
10
11
  vi.mock('svelte', () => ({
11
12
  mount: vi.fn(),
@@ -64,8 +65,9 @@ describe('injection.ts', () => {
64
65
 
65
66
  const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
66
67
 
68
+ // @ts-ignore
69
+ expect(link.href).toBe(titleUrl(injection.title_details))
67
70
  expect(link.innerText).toBe(injection.title)
68
- expect(link.href).toBe(injection.playpilot_url)
69
71
  })
70
72
 
71
73
  it('Should replace given words as expected when more than 1 injection per sentence is present', () => {
@@ -83,11 +85,13 @@ describe('injection.ts', () => {
83
85
 
84
86
  const links = /** @type {HTMLAnchorElement[]} */ (Array.from(document.querySelectorAll('a')))
85
87
 
88
+ // @ts-ignore
89
+ expect(links[0].href).toBe(titleUrl(linkInjections[0].title_details))
86
90
  expect(links[0].innerText).toBe(linkInjections[0].title)
87
- expect(links[0].href).toBe(linkInjections[0].playpilot_url)
88
91
 
92
+ // @ts-ignore
93
+ expect(links[1].href).toBe(titleUrl(linkInjections[1].title_details))
89
94
  expect(links[1].innerText).toBe(linkInjections[1].title)
90
- expect(links[1].href).toBe(linkInjections[1].playpilot_url)
91
95
  })
92
96
 
93
97
  it('Should ignore injections that are marked as inactive', () => {
@@ -946,6 +950,43 @@ describe('injection.ts', () => {
946
950
 
947
951
  expect(document.querySelector('a')?.closest('[data-playpilot-injection-key]')).toBeTruthy()
948
952
  })
953
+
954
+ describe('config.open_tpi_links_in_explore', () => {
955
+ beforeEach(() => {
956
+ window.PlayPilotLinkInjections.config = {
957
+ open_tpi_links_in_explore: true,
958
+ explore_navigation_path: 'https://some-path.com/explore',
959
+ }
960
+ })
961
+
962
+ it('Should use href with explore links if open_tpi_links_in_explore is true', () => {
963
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
964
+
965
+ document.body.innerHTML = `<p>${injection.sentence}</p>`
966
+
967
+ const elements = Array.from(document.querySelectorAll('p'))
968
+
969
+ injectLinksInDocument(elements, { aiInjections: [injection], manualInjections: [] })
970
+
971
+ const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
972
+ expect(link.href).toBe(window.PlayPilotLinkInjections.config.explore_navigation_path + `?route=title&sid=${injection.title_details?.sid}`)
973
+ expect(link.target).not.toBeTruthy()
974
+ })
975
+
976
+ it('Should not open modal when link is clicked when open_tpi_links_in_explore is true', async () => {
977
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
978
+
979
+ const elements = Array.from(document.body.querySelectorAll('p'))
980
+ const injection = generateInjection('This is a sentence with an injection.', 'an injection')
981
+
982
+ injectLinksInDocument(elements, { aiInjections: [injection], manualInjections: [] })
983
+
984
+ const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
985
+ await fireEvent.click(link)
986
+
987
+ expect(openModalForInjectedLink).not.toHaveBeenCalled()
988
+ })
989
+ })
949
990
  })
950
991
 
951
992
  describe('clearLinkInjections', () => {