@playpilot/tpi 5.23.4 → 5.24.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/events.md CHANGED
@@ -37,6 +37,7 @@ Event | Action | Info | Payload
37
37
  `ali_participant_modal_view` | _Fires any time a title modal is viewed_ | The title modal opens when viewing a participant modal both on desktop and mobile | `participant` (name of the participant)
38
38
  `ali_participant_modal_close` | _Fires any time a title modal is closed_ | | `participant` (name of the participant) `time_spent` (time between modal_view and modal_close milliseconds)
39
39
  'ali_similar_title_click' | _Fires any time a similar titles rail item is clicked_ | Title | `title_source` (original name of the title the rail item was clicked in)
40
+ 'ali_participant_click' | _Fires any time a participants rail item is clicked_ | null | `title_source` (original name of the title the rail item was clicked in), `participant` (name of the clicked participant)
40
41
 
41
42
  ### Popover
42
43
  Event | Action | Info | Payload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.23.4",
3
+ "version": "5.24.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -0,0 +1,16 @@
1
+ import { getApiToken } from '$lib/token'
2
+ import type { ParticipantData } from '$lib/types/participant'
3
+ import type { TitleData } from '../types/title'
4
+ import { api } from './api'
5
+
6
+ export async function fetchTitlesForParticipant(participant: ParticipantData, { page = 1 }: { page?: number } = {}): Promise<TitleData[]> {
7
+ const response = await api<{ results: TitleData[] }>(`/titles/browse?api-token=${getApiToken()}&participant_sid=${participant.sid}&page=${page}`)
8
+
9
+ return response.results
10
+ }
11
+
12
+ export async function fetchParticipantsForTitle(title: TitleData): Promise<ParticipantData[]> {
13
+ const response = await api<{ results: ParticipantData[] }>(`/participants/browse?api-token=${getApiToken()}&title_sid=${title.sid}`)
14
+
15
+ return response.results
16
+ }
@@ -0,0 +1,9 @@
1
+ import { getApiToken } from '$lib/token'
2
+ import type { TitleData } from '../types/title'
3
+ import { api } from './api'
4
+
5
+ export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]> {
6
+ const response = await api<{ results: TitleData[] }>(`/titles/browse?api-token=${getApiToken()}&related_to_sid=${title.sid}`)
7
+
8
+ return response.results
9
+ }
@@ -116,6 +116,11 @@ export const translations = {
116
116
  [Language.Swedish]: 'E-post',
117
117
  [Language.Danish]: 'E-mail',
118
118
  },
119
+ 'Show More': {
120
+ [Language.English]: 'Show more',
121
+ [Language.Swedish]: 'Visa mer',
122
+ [Language.Danish]: 'Vis mere',
123
+ },
119
124
 
120
125
  // Genres
121
126
  'All': {
@@ -22,6 +22,7 @@ export const TrackingEvent = Object.freeze({
22
22
 
23
23
  // Rails
24
24
  SimilarTitleClick: 'ali_similar_title_click',
25
+ ParticipantClick: 'ali_title_participant_click',
25
26
 
26
27
  // After article
27
28
  AfterArticlePlaylinkClick: 'ali_after_article_playlink_click',
package/src/lib/modal.ts CHANGED
@@ -12,7 +12,7 @@ type ModalType = 'title' | 'participant'
12
12
 
13
13
  type Modal = {
14
14
  injection?: LinkInjection | null
15
- component: object
15
+ component?: object
16
16
  type: ModalType
17
17
  data: TitleData | ParticipantData | null
18
18
  scrollPosition?: number
@@ -25,8 +25,8 @@ const modals: Modal[] = []
25
25
  * Ignore clicks that used modifier keys or that were not left click.
26
26
  */
27
27
  export function openModal(
28
- { type = 'title', event = null, injection = null, data = null, scrollPosition = 0 }:
29
- { type?: ModalType, event?: MouseEvent | null, injection?: LinkInjection | null, data?: TitleData | ParticipantData | null, scrollPosition?: number } = {}): void {
28
+ { type = 'title', event = null, injection = null, data = null, scrollPosition = 0, returnToTitle = null }:
29
+ { type?: ModalType, event?: MouseEvent | null, injection?: LinkInjection | null, data?: TitleData | ParticipantData | null, scrollPosition?: number, returnToTitle?: TitleData | null } = {}): void {
30
30
  if (event && isHoldingSpecialKey(event)) return
31
31
 
32
32
  event?.preventDefault()
@@ -35,10 +35,23 @@ export function openModal(
35
35
 
36
36
  const target = getPlayPilotWrapperElement()
37
37
  const sharedProps = { initialScrollPosition: scrollPosition }
38
- const component = type === 'title' ?
39
- mount(TitleModal, { target, props: { title: data as TitleData, ...sharedProps } }) :
40
- mount(ParticipantModal, { target, props: { participant: data as ParticipantData, ...sharedProps } })
38
+ const component = getModalComponentByType({ type, target, data, props: sharedProps })
41
39
 
40
+ // When a return title is given it is added to the list of modals but is not actually opened.
41
+ // This is only so that the user can return the title they opened another modal from. This will be from the popover.
42
+ // For example title in popover -> open similar title -> go back to title from popover, but in modal
43
+ if (returnToTitle) addModalToList({ type, injection, data: returnToTitle })
44
+
45
+ addModalToList({ type, injection, data, scrollPosition, component })
46
+ }
47
+
48
+ function getModalComponentByType({ type = 'title', target, data, props = {} }: { type: ModalType, target: Element, data: TitleData | ParticipantData | null, props?: Record<string, any> }) {
49
+ return type === 'title' ?
50
+ mount(TitleModal, { target, props: { title: data as TitleData, ...props } }) :
51
+ mount(ParticipantModal, { target, props: { participant: data as ParticipantData, ...props } })
52
+ }
53
+
54
+ function addModalToList({ type = 'title', injection = null, data, scrollPosition = 0, component }: Modal) {
42
55
  modals.push({ type, injection, data, scrollPosition, component })
43
56
  }
44
57
 
@@ -48,7 +61,7 @@ export function closeCurrentModal(outro: boolean = true): void {
48
61
 
49
62
  saveCurrentModalScrollPosition()
50
63
 
51
- unmount(modals[modals.length - 1].component, { outro })
64
+ unmount(modals[modals.length - 1].component!, { outro })
52
65
  }
53
66
 
54
67
  /** Unmount the last modal is the list of modals and remove it from the list */
@@ -29,9 +29,9 @@
29
29
  }
30
30
  </script>
31
31
 
32
- <a class="title" href={titleUrl(title)} {onclick}>
32
+ <a class="title" href={titleUrl(title)} {onclick} data-testid="title">
33
33
  <div class="poster">
34
- <TitlePoster {title} width={30} height={43} />
34
+ <TitlePoster {title} width={30} height={43} lazy />
35
35
  </div>
36
36
 
37
37
  <div class="content">
@@ -89,7 +89,7 @@
89
89
  display: flex;
90
90
  align-items: center;
91
91
  width: 100%;
92
- background: var(--playpilot-list-item-background, var(--playpilot-lighter));
92
+ background: var(--playpilot-list-item-background, var(--playpilot-playlink-background, var(--playpilot-lighter)));
93
93
  padding: margin(0.5);
94
94
  border: 0;
95
95
  border-radius: var(--playpilot-list-item-border-radius, margin(0.5));
@@ -97,7 +97,7 @@
97
97
  font-style: normal !important;
98
98
 
99
99
  &:hover {
100
- background: var(--playpilot-list-item-hover-background, var(--playpilot-content));
100
+ filter: var(--playpilot-list-item-hover-filter, var(--playpilot-playlink-hover-filter, brightness(1.1)));
101
101
  }
102
102
 
103
103
  &:last-child {
@@ -112,7 +112,7 @@
112
112
  width: margin(4.125);
113
113
  aspect-ratio: 2/3;
114
114
  border-radius: var(--playpilot-detail-image-border-radius, margin(0.5));
115
- background: var(--playpilot-detail-image-background, var(--playpilot-content));
115
+ background: var(--playpilot-detail-image-background, var(--playpilot-genre-background, var(--playpilot-content)));
116
116
  overflow: hidden;
117
117
  }
118
118
 
@@ -213,7 +213,7 @@
213
213
 
214
214
  .backdrop {
215
215
  z-index: 0;
216
- position: absolute;
216
+ position: fixed;
217
217
  top: 0;
218
218
  right: 0;
219
219
  bottom: 0;
@@ -1,10 +1,14 @@
1
1
  <script lang="ts">
2
+ import { onMount } from 'svelte'
2
3
  import { heading } from '$lib/actions/heading'
4
+ import { fetchTitlesForParticipant } from '$lib/api/participants'
3
5
  import { SplitTest } from '$lib/enums/SplitTest'
4
6
  import { openModal } from '$lib/modal'
5
7
  import { trackSplitTestView } from '$lib/splitTest'
6
8
  import type { ParticipantData } from '$lib/types/participant'
9
+ import type { TitleData } from '$lib/types/title'
7
10
  import ListTitle from './ListTitle.svelte'
11
+ import { t } from '$lib/localization'
8
12
 
9
13
  interface Props {
10
14
  participant: ParticipantData
@@ -14,12 +18,37 @@
14
18
 
15
19
  const { name, birth_date, death_date } = $derived(participant)
16
20
 
21
+ const pageSize = 30
22
+
17
23
  trackSplitTestView(SplitTest.ParticipantPlaylinkFormat)
18
24
 
25
+ let titles: TitleData[] = $state([])
26
+ let page = $state(1)
27
+ let hasMorePages = $state(true)
28
+ let loading = $state(true)
29
+
30
+ onMount(loadMore)
31
+
19
32
  function formatDate(dateString: string): string {
20
33
  const date = new Date(dateString)
21
34
  return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
22
35
  }
36
+
37
+ async function loadMore() {
38
+ loading = true
39
+
40
+ try {
41
+ const results = await fetchTitlesForParticipant(participant, { page })
42
+
43
+ titles = [...titles, ...results]
44
+ hasMorePages = results?.length >= pageSize
45
+ } catch {
46
+ hasMorePages = false
47
+ } finally {
48
+ loading = false
49
+ page += 1
50
+ }
51
+ }
23
52
  </script>
24
53
 
25
54
  <div class="header">
@@ -36,11 +65,17 @@
36
65
  <div class="heading small" use:heading={3} id="credits">Credits</div>
37
66
 
38
67
  <div class="list">
39
- {#each window.PlayPilotLinkInjections?.evaluated_link_injections?.map(i => i.title_details) || [] as title}
40
- {#if title}
41
- <ListTitle {title} onclick={(event) => openModal({ event, data: title })} />
42
- {/if}
68
+ {#each titles as title}
69
+ <ListTitle {title} onclick={(event) => openModal({ event, data: title })} />
43
70
  {/each}
71
+
72
+ {#if loading}
73
+ {#each { length: 6 }}
74
+ <div class="skeleton" data-testid="skeleton"></div>
75
+ {/each}
76
+ {:else if hasMorePages}
77
+ <button class="more" onclick={loadMore}>{t('Show More')}</button>
78
+ {/if}
44
79
  </div>
45
80
  </div>
46
81
 
@@ -89,4 +124,26 @@
89
124
  flex-direction: column;
90
125
  gap: margin(0.5);
91
126
  }
127
+
128
+ .skeleton {
129
+ min-height: margin(7.25);
130
+ border-radius: var(--playpilot-playlink-border-radius, margin(0.5));
131
+ background: var(--playpilot-list-item-background, var(--playpilot-lighter));
132
+ }
133
+
134
+ .more {
135
+ cursor: pointer;
136
+ appearance: none;
137
+ padding: margin(0.5) margin(1);
138
+ border: 0;
139
+ border-radius: var(--playpilot-participants-load-more-border-radius, var(--playpilot-list-item-border-radius, margin(0.5)));
140
+ background: var(--playpilot-participants-load-more-background, var(--playpilot-button-background, var(--playpilot-content)));
141
+ color: inherit;
142
+ font-size: inherit;
143
+ font-family: inherit;
144
+
145
+ &:hover {
146
+ filter: brightness(1.2);
147
+ }
148
+ }
92
149
  </style>
@@ -1,18 +1,25 @@
1
1
  <script lang="ts">
2
- import { participants } from '$lib/fakeData'
2
+ import { fetchParticipantsForTitle } from '$lib/api/participants'
3
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
4
  import { openModal } from '$lib/modal'
5
+ import { track } from '$lib/tracking'
4
6
  import type { ParticipantData } from '$lib/types/participant'
7
+ import type { TitleData } from '$lib/types/title'
5
8
  import Rail from './Rail.svelte'
6
9
 
7
- async function fetchParticipants(): Promise<ParticipantData[]> {
8
- // This is just a fake loading state for now
9
- await new Promise(res => setTimeout(res, 500))
10
+ interface Props {
11
+ title: TitleData
12
+ }
13
+
14
+ const { title }: Props = $props()
10
15
 
11
- return participants
16
+ function onclick(event: MouseEvent, participant: ParticipantData): void {
17
+ openModal({ event, type: 'participant', data: participant })
18
+ track(TrackingEvent.ParticipantClick, null, { title_source: title.original_title, participant: participant.name })
12
19
  }
13
20
  </script>
14
21
 
15
- {#await fetchParticipants()}
22
+ {#await fetchParticipantsForTitle(title)}
16
23
  <Rail heading="Cast">
17
24
  {#each { length: 5 }}
18
25
  <div class="participant"></div>
@@ -22,10 +29,12 @@
22
29
  {#if participants?.length}
23
30
  <Rail heading="Cast">
24
31
  {#each participants.slice(0, 15) as participant}
25
- <button class="participant" data-testid="participant" onclick={event => openModal({ event, type: 'participant', data: participant })}>
32
+ <button class="participant" data-testid="participant" onclick={event => onclick(event, participant)}>
26
33
  <span class="truncate">{participant.name}</span>
27
34
 
28
- <div class="character truncate">{participant.character}</div>
35
+ {#if participant.character}
36
+ <div class="character truncate">{participant.character}</div>
37
+ {/if}
29
38
  </button>
30
39
  {/each}
31
40
  </Rail>
@@ -34,7 +43,8 @@
34
43
 
35
44
  <style lang="scss">
36
45
  .participant {
37
- display: block;
46
+ display: flex;
47
+ flex-direction: column;
38
48
  flex: 0 0 10rem;
39
49
  width: 10rem;
40
50
  min-height: margin(3.375); // Matches 54 pixels, the height of a card with both name and character
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { fetchSimilarTitles } from '$lib/api/titles'
2
3
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
4
  import { track } from '$lib/tracking'
4
5
  import type { TitleData } from '$lib/types/title'
@@ -10,15 +11,7 @@
10
11
 
11
12
  const { title }: Props = $props()
12
13
 
13
- const titles = fetchTitles()
14
-
15
- async function fetchTitles(): Promise<TitleData[]> {
16
- // This is just a fake loading state for now
17
- await new Promise(res => setTimeout(res, 500))
18
-
19
- // Imagine this being a fetch request that returns titles instead.
20
- return (window.PlayPilotLinkInjections?.evaluated_link_injections?.map(i => i.title_details) || []) as TitleData[]
21
- }
14
+ const titles = fetchSimilarTitles(title)
22
15
  </script>
23
16
 
24
17
  <TitlesRail {titles} heading="Similar movies & shows" onclick={(targetTitle) => track(TrackingEvent.SimilarTitleClick, targetTitle, { title_source: title.original_title })} />
@@ -4,6 +4,7 @@
4
4
  import type { TitleData } from '$lib/types/title'
5
5
  import { openModal } from '$lib/modal'
6
6
  import { titleUrl } from '$lib/routes'
7
+ import { getContext } from 'svelte'
7
8
 
8
9
  interface Props {
9
10
  titles: Promise<TitleData[]> | TitleData[]
@@ -13,8 +14,11 @@
13
14
 
14
15
  const { titles, heading = '', onclick = () => null }: Props = $props()
15
16
 
17
+ const isPopover = getContext('scope') === 'popover'
18
+ const returnToTitle: TitleData | null = isPopover ? getContext('title') : null
19
+
16
20
  function openTitle(event: MouseEvent, title: TitleData): void {
17
- openModal({ event, data: title })
21
+ openModal({ event, data: title, returnToTitle })
18
22
  onclick(title)
19
23
  }
20
24
  </script>
@@ -12,6 +12,7 @@
12
12
  import { heading } from '$lib/actions/heading'
13
13
  import { removeImageUrlPrefix } from '$lib/image'
14
14
  import { titleUrl } from '$lib/routes'
15
+ import { setContext } from 'svelte'
15
16
 
16
17
  interface Props {
17
18
  title: TitleData
@@ -20,15 +21,17 @@
20
21
 
21
22
  const { title, small = false }: Props = $props()
22
23
 
24
+ setContext('title', title)
25
+
23
26
  let posterLoaded = $state(false)
24
27
  let backgroundLoaded = $state(false)
25
28
  let useBackgroundFallback = $state(false)
26
29
  </script>
27
30
 
28
- <div class="content" class:small data-playpilot-link-injections-title>
31
+ <div class="content" class:small>
29
32
  <div class="header">
30
33
  <div class="poster" class:loaded={posterLoaded}>
31
- <TitlePoster {title} onload={() => posterLoaded = true} />
34
+ <TitlePoster {title} onload={() => posterLoaded = true} lazy={false} />
32
35
  </div>
33
36
 
34
37
  <div class="heading" use:heading={2} class:truncate={small} id="title">{title.title}</div>
@@ -63,11 +66,8 @@
63
66
  <Description text={title.description} blurb={title.blurb} />
64
67
  {/if}
65
68
 
66
- <!-- Temporarily not available on production as there is not yet an API endpoint for either -->
67
- {#if process.env.NODE_ENV !== 'production'}
68
- <ParticipantsRail />
69
- <SimilarRail {title} />
70
- {/if}
69
+ <ParticipantsRail {title} />
70
+ <SimilarRail {title} />
71
71
  </div>
72
72
  </div>
73
73
 
@@ -7,10 +7,11 @@
7
7
  title: TitleData
8
8
  width?: number
9
9
  height?: number
10
+ lazy?: boolean
10
11
  onload?: () => void
11
12
  }
12
13
 
13
- const { title, width = 96, height = 144, onload = () => null }: Props = $props()
14
+ const { title, width = 96, height = 144, lazy = true, onload = () => null }: Props = $props()
14
15
  </script>
15
16
 
16
17
  <img
@@ -19,6 +20,7 @@
19
20
  {width}
20
21
  {height}
21
22
  onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl}
23
+ loading={lazy ? 'lazy' : null}
22
24
  {onload} />
23
25
 
24
26
  <style lang="scss">
@@ -26,5 +28,6 @@
26
28
  display: block;
27
29
  width: 100%;
28
30
  height: auto;
31
+ color: var(--playpilot-detail-text-color, var(--playpilot-text-color-alt));
29
32
  }
30
33
  </style>
@@ -43,6 +43,13 @@ describe('modal.js', () => {
43
43
 
44
44
  expect(mount).not.toHaveBeenCalled()
45
45
  })
46
+
47
+ it('Should add additional modal to the stack when returnToTitle is given', () => {
48
+ // @ts-ignore
49
+ openModal({ ...modal, returnToTitle: title })
50
+
51
+ expect(getAllModals()).toHaveLength(2)
52
+ })
46
53
  })
47
54
 
48
55
  describe('closeCurrentModal', () => {
@@ -0,0 +1,76 @@
1
+ import { fireEvent, render, waitFor } from '@testing-library/svelte'
2
+ import { describe, expect, it, vi, beforeEach } from 'vitest'
3
+
4
+ import Participant from '../../../routes/components/Participant.svelte'
5
+ import { participants, title } from '$lib/fakeData'
6
+ import { fetchTitlesForParticipant } from '$lib/api/participants'
7
+
8
+ vi.mock('$lib/tracking', () => ({
9
+ track: vi.fn(),
10
+ }))
11
+
12
+ vi.mock('$lib/api/participants', () => ({
13
+ fetchTitlesForParticipant: vi.fn(),
14
+ }))
15
+
16
+ describe('Participant.svelte', () => {
17
+ beforeEach(() => {
18
+ vi.resetAllMocks()
19
+ })
20
+
21
+ it('Should call fetchTitlesForParticipant on mount', () => {
22
+ render(Participant, { participant: participants[0] })
23
+
24
+ expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 1 })
25
+ })
26
+
27
+ it('Should render fetched titles after showing skeletons', async () => {
28
+ vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce([title, title])
29
+
30
+ const { getAllByTestId } = render(Participant, { participant: participants[0] })
31
+
32
+ expect(getAllByTestId('skeleton')).toHaveLength(6)
33
+
34
+ await waitFor(() => {
35
+ expect(getAllByTestId('title')).toHaveLength(2)
36
+ })
37
+ })
38
+
39
+ it('Should not show load more button while loading and when fewer than page size of titles are fetched', async () => {
40
+ vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce([title, title])
41
+
42
+ const { queryByText, getAllByTestId } = render(Participant, { participant: participants[0] })
43
+
44
+ expect(queryByText('Show More')).not.toBeTruthy()
45
+
46
+ await waitFor(() => {
47
+ expect(getAllByTestId('title')).toHaveLength(2)
48
+ })
49
+
50
+ expect(queryByText('Show More')).not.toBeTruthy()
51
+ })
52
+
53
+ it('Should show load more button after loading and if more than page size of titles are fetched', async () => {
54
+ const resolved = Array(30).fill(title)
55
+ vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce(resolved)
56
+
57
+ const { getByText } = render(Participant, { participant: participants[0] })
58
+
59
+ await waitFor(() => {
60
+ expect(getByText('Show more')).toBeTruthy()
61
+ })
62
+ })
63
+
64
+ it('Should fetch more titles when load more button is clicked', async () => {
65
+ const resolved = Array(30).fill(title)
66
+ vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce(resolved)
67
+
68
+ const { getByText } = render(Participant, { participant: participants[0] })
69
+
70
+ await waitFor(() => getByText('Show more'))
71
+
72
+ await fireEvent.click(getByText('Show more'))
73
+
74
+ expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 2 })
75
+ })
76
+ })
@@ -14,6 +14,10 @@ vi.mock('$lib/tracking', () => ({
14
14
  track: vi.fn(),
15
15
  }))
16
16
 
17
+ vi.mock('$lib/api/participants', () => ({
18
+ fetchTitlesForParticipant: vi.fn(),
19
+ }))
20
+
17
21
  describe('ParticipantModal.svelte', () => {
18
22
  beforeEach(() => {
19
23
  vi.resetAllMocks()
@@ -1,18 +1,31 @@
1
1
  import { fireEvent, render, waitFor } from '@testing-library/svelte'
2
- import { describe, expect, it, vi } from 'vitest'
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import ParticipantsRail from '../../../../routes/components/Rails/ParticipantsRail.svelte'
5
5
  import { openModal } from '$lib/modal'
6
- import { participants } from '$lib/fakeData'
6
+ import { participants, title } from '$lib/fakeData'
7
+ import { track } from '$lib/tracking'
8
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
+ import { fetchParticipantsForTitle } from '$lib/api/participants'
7
10
 
8
11
  vi.mock('$lib/modal', () => ({
9
12
  openModal: vi.fn(),
10
13
  }))
11
14
 
15
+ vi.mock('$lib/tracking', () => ({
16
+ track: vi.fn(),
17
+ }))
18
+
19
+ vi.mock('$lib/api/participants', () => ({
20
+ fetchParticipantsForTitle: vi.fn(),
21
+ }))
22
+
12
23
  describe('ParticipantsRail.svelte', () => {
13
24
  it('Should render each given participant', async () => {
25
+ vi.mocked(fetchParticipantsForTitle).mockResolvedValueOnce(participants)
26
+
14
27
  // @ts-ignore
15
- const { getByText } = render(ParticipantsRail)
28
+ const { getByText } = render(ParticipantsRail, { title })
16
29
 
17
30
  await waitFor(() => {
18
31
  expect(getByText(participants[0].name)).toBeTruthy()
@@ -20,15 +33,20 @@ describe('ParticipantsRail.svelte', () => {
20
33
  })
21
34
  })
22
35
 
23
- // TODO: Re-enable when participants are actually fetched
24
- // it('Should not render when no participants are returned', () => {
25
- // const { container } = render(ParticipantsRail)
36
+ it('Should not render when no participants are returned', async () => {
37
+ vi.mocked(fetchParticipantsForTitle).mockResolvedValueOnce([])
26
38
 
27
- // expect(container.querySelectorAll('.participant').length).toBe(0)
28
- // })
39
+ const { container } = render(ParticipantsRail)
40
+
41
+ await waitFor(() => {
42
+ expect(container.querySelectorAll('.participant').length).toBe(0)
43
+ })
44
+ })
29
45
 
30
46
  it('Should open modal on click of participant', async () => {
31
- const { getAllByTestId } = render(ParticipantsRail)
47
+ vi.mocked(fetchParticipantsForTitle).mockResolvedValueOnce(participants)
48
+
49
+ const { getAllByTestId } = render(ParticipantsRail, { title })
32
50
 
33
51
  await waitFor(async () => {
34
52
  await fireEvent.click(getAllByTestId('participant')[0])
@@ -36,4 +54,16 @@ describe('ParticipantsRail.svelte', () => {
36
54
 
37
55
  expect(openModal).toHaveBeenCalledWith({ event: expect.any(Object), type: 'participant', data: participants[0] })
38
56
  })
57
+
58
+ it('Should fire track event on click of participant', async () => {
59
+ vi.mocked(fetchParticipantsForTitle).mockResolvedValueOnce(participants)
60
+
61
+ const { getAllByTestId } = render(ParticipantsRail, { title })
62
+
63
+ await waitFor(async () => {
64
+ await fireEvent.click(getAllByTestId('participant')[0])
65
+ })
66
+
67
+ expect(track).toHaveBeenCalledWith(TrackingEvent.ParticipantClick, null, { title_source: title.original_title, participant: participants[0].name })
68
+ })
39
69
  })
@@ -0,0 +1,26 @@
1
+ import { render } from '@testing-library/svelte'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { title } from '$lib/fakeData'
5
+ import SimilarRail from '../../../../routes/components/Rails/SimilarRail.svelte'
6
+ import { fetchSimilarTitles } from '$lib/api/titles'
7
+
8
+ vi.mock('$lib/modal', () => ({
9
+ openModal: vi.fn(),
10
+ }))
11
+
12
+ vi.mock('$lib/tracking', () => ({
13
+ track: vi.fn(),
14
+ }))
15
+
16
+ vi.mock('$lib/api/titles', () => ({
17
+ fetchSimilarTitles: vi.fn(),
18
+ }))
19
+
20
+ describe('ParticipantsRail.svelte', () => {
21
+ it('Should fetch titles', async () => {
22
+ render(SimilarRail, { title })
23
+
24
+ expect(fetchSimilarTitles).toHaveBeenCalledWith(title)
25
+ })
26
+ })
@@ -33,7 +33,7 @@ describe('TitlesRail.svelte', () => {
33
33
 
34
34
  await fireEvent.click(getAllByRole('link')[0])
35
35
 
36
- expect(openModal).toHaveBeenCalledWith({ event: expect.any(Object), data: title })
36
+ expect(openModal).toHaveBeenCalledWith({ event: expect.any(Object), data: title, returnToTitle: null })
37
37
  })
38
38
 
39
39
  it('Should fire given onclick function when title is clicked', async () => {