@playpilot/tpi 5.34.1 → 6.0.0-beta.explore.15

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 (69) hide show
  1. package/dist/link-injections.js +25 -10
  2. package/package.json +1 -1
  3. package/src/lib/api/titles.ts +13 -1
  4. package/src/lib/data/countries.json +216 -0
  5. package/src/lib/data/translations.ts +5 -0
  6. package/src/lib/enums/SplitTest.ts +1 -6
  7. package/src/lib/explore.ts +59 -0
  8. package/src/lib/fakeData.ts +1 -0
  9. package/src/lib/images/titles-list.webp +0 -0
  10. package/src/lib/modal.ts +7 -6
  11. package/src/lib/scss/global.scss +0 -2
  12. package/src/lib/trailer.ts +22 -0
  13. package/src/lib/types/api.d.ts +6 -0
  14. package/src/lib/types/config.d.ts +12 -0
  15. package/src/lib/types/filter.d.ts +2 -0
  16. package/src/lib/types/title.d.ts +4 -1
  17. package/src/routes/+page.svelte +6 -1
  18. package/src/routes/components/Ads/TopScroll.svelte +4 -18
  19. package/src/routes/components/Button.svelte +101 -0
  20. package/src/routes/components/Debugger.svelte +25 -0
  21. package/src/routes/components/Explore/Explore.svelte +240 -0
  22. package/src/routes/components/Explore/ExploreCallToAction.svelte +58 -0
  23. package/src/routes/components/Explore/ExploreModal.svelte +15 -0
  24. package/src/routes/components/Explore/Filter/Dropdown.svelte +72 -0
  25. package/src/routes/components/Explore/Filter/Filter.svelte +99 -0
  26. package/src/routes/components/Explore/Filter/FilterItem.svelte +57 -0
  27. package/src/routes/components/Explore/Filter/FilterSorting.svelte +70 -0
  28. package/src/routes/components/Explore/Filter/Search.svelte +57 -0
  29. package/src/routes/components/Explore/Filter/TogglesWithSearch.svelte +142 -0
  30. package/src/routes/components/GridTitle.svelte +122 -0
  31. package/src/routes/components/GridTitleSkeleton.svelte +36 -0
  32. package/src/routes/components/Icons/IconArrow.svelte +10 -2
  33. package/src/routes/components/Icons/IconClose.svelte +9 -1
  34. package/src/routes/components/Icons/IconFilter.svelte +5 -0
  35. package/src/routes/components/Icons/IconPlay.svelte +3 -0
  36. package/src/routes/components/Icons/IconSearch.svelte +3 -0
  37. package/src/routes/components/ListTitle.svelte +10 -68
  38. package/src/routes/components/ListTitleSkeleton.svelte +42 -0
  39. package/src/routes/components/Modal.svelte +5 -23
  40. package/src/routes/components/Participant.svelte +0 -4
  41. package/src/routes/components/Playlinks/PlaylinkIcon.svelte +1 -1
  42. package/src/routes/components/Playlinks/PlaylinksCompact.svelte +62 -0
  43. package/src/routes/components/Share.svelte +5 -23
  44. package/src/routes/components/Title.svelte +22 -22
  45. package/src/routes/components/TitleModal.svelte +4 -1
  46. package/src/routes/components/Trailer.svelte +18 -0
  47. package/src/routes/components/YouTubeEmbedOverlay.svelte +96 -0
  48. package/src/routes/elements/+page.svelte +39 -2
  49. package/src/routes/explore/+page.svelte +60 -0
  50. package/src/tests/lib/api/ads.test.js +0 -1
  51. package/src/tests/lib/api/titles.test.js +55 -0
  52. package/src/tests/lib/explore.test.js +139 -0
  53. package/src/tests/lib/trailer.test.js +56 -0
  54. package/src/tests/routes/components/Button.test.js +28 -0
  55. package/src/tests/routes/components/Explore/Explore.test.js +94 -0
  56. package/src/tests/routes/components/Explore/Filter/Dropdown.test.js +16 -0
  57. package/src/tests/routes/components/Explore/Filter/Filter.test.js +28 -0
  58. package/src/tests/routes/components/Explore/Filter/FilterItem.test.js +50 -0
  59. package/src/tests/routes/components/Explore/Filter/FilterSorting.test.js +34 -0
  60. package/src/tests/routes/components/Explore/Filter/Search.test.js +26 -0
  61. package/src/tests/routes/components/Explore/Filter/TogglesWithSearch.test.js +53 -0
  62. package/src/tests/routes/components/GridTitle.test.js +42 -0
  63. package/src/tests/routes/components/ListTitle.test.js +1 -1
  64. package/src/tests/routes/components/Playlinks/PlaylinksCompact.test.js +42 -0
  65. package/src/tests/routes/components/Share.test.js +12 -12
  66. package/src/tests/routes/components/Title.test.js +13 -0
  67. package/src/tests/routes/components/Trailer.test.js +20 -0
  68. package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +31 -0
  69. package/src/tests/setup.js +2 -0
@@ -6,8 +6,6 @@
6
6
  import SwipeHandle from './SwipeHandle.svelte'
7
7
  import { onMount, setContext, type Snippet } from 'svelte'
8
8
  import { prefersReducedMotion } from 'svelte/motion'
9
- import { isInSplitTestVariant } from '$lib/splitTest'
10
- import { SplitTest } from '$lib/enums/SplitTest'
11
9
  import { mobileBreakpoint } from '$lib/constants'
12
10
  import { destroyAllModals, getPreviousModal, goBackToPreviousModal } from '$lib/modal'
13
11
  import { focustrap } from '$lib/actions/focustrap'
@@ -34,7 +32,6 @@
34
32
  onclose = () => null,
35
33
  }: Props = $props()
36
34
 
37
- const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
38
35
  const historyHash = '#playpilot'
39
36
 
40
37
  let windowWidth = $state(0)
@@ -106,7 +103,7 @@
106
103
  <!-- svelte-ignore a11y_no_static_element_interactions -->
107
104
  <div
108
105
  class="modal"
109
- class:has-bubble={!!bubble && inlineBubble}
106
+ class:has-bubble={!!bubble}
110
107
  class:has-prepend={!!prepend}
111
108
  style:--dialog-offset="{dialogOffset}px"
112
109
  onclick={closeOnBackdropClick}
@@ -120,7 +117,7 @@
120
117
  {/if}
121
118
 
122
119
  {#if bubble}
123
- <div class="bubble" class:inline={inlineBubble} transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
120
+ <div class="bubble" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
124
121
  {@render bubble()}
125
122
  </div>
126
123
  {/if}
@@ -304,28 +301,13 @@
304
301
  .bubble {
305
302
  z-index: 1;
306
303
  position: relative;
307
- width: calc(100% - margin(1));
304
+ width: 100%;
308
305
  max-width: $max-width;
309
- margin: margin(0.5);
306
+ margin: auto 0 0;
310
307
 
311
308
  @include desktop() {
312
309
  width: 100%;
313
- margin: 0 0 margin(0.5);
314
- }
315
-
316
- &.inline {
317
- width: 100%;
318
- margin: auto 0 0;
319
-
320
- @include desktop() {
321
- margin-top: 0;
322
- }
323
- }
324
-
325
- .prepend + & {
326
- &:not(.inline) {
327
- margin-top: 0;
328
- }
310
+ margin-top: 0;
329
311
  }
330
312
  }
331
313
  </style>
@@ -2,9 +2,7 @@
2
2
  import { onMount } from 'svelte'
3
3
  import { heading } from '$lib/actions/heading'
4
4
  import { fetchTitlesForParticipant } from '$lib/api/participants'
5
- import { SplitTest } from '$lib/enums/SplitTest'
6
5
  import { openModal } from '$lib/modal'
7
- import { trackSplitTestView } from '$lib/splitTest'
8
6
  import type { ParticipantData } from '$lib/types/participant'
9
7
  import type { TitleData } from '$lib/types/title'
10
8
  import ListTitle from './ListTitle.svelte'
@@ -20,8 +18,6 @@
20
18
 
21
19
  const pageSize = 30
22
20
 
23
- trackSplitTestView(SplitTest.ParticipantPlaylinkFormat)
24
-
25
21
  let titles: TitleData[] = $state([])
26
22
  let page = $state(1)
27
23
  let hasMorePages = $state(true)
@@ -24,7 +24,7 @@
24
24
  width: var(--size);
25
25
  height: var(--size);
26
26
  background: theme(playlink-background, light);
27
- border-radius: theme(playlink-border-radius, border-radius);
27
+ border-radius: theme(playlink-border-radius, calc(var(--size) * 0.25));
28
28
  overflow: hidden;
29
29
 
30
30
  &:hover,
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import { mergePlaylinks } from '$lib/playlink'
3
+ import type { PlaylinkData } from '$lib/types/playlink'
4
+ import PlaylinkIcon from './PlaylinkIcon.svelte'
5
+
6
+ interface Props {
7
+ playlinks: PlaylinkData[]
8
+ size?: number
9
+ }
10
+
11
+ const { playlinks, size = 30 }: Props = $props()
12
+
13
+ const limitedPlaylinks = $derived(mergePlaylinks(playlinks).slice(0, 3))
14
+
15
+ function onPlaylinkClick(event: MouseEvent): void {
16
+ event.stopPropagation()
17
+ }
18
+ </script>
19
+
20
+ <div class="playlinks">
21
+ {#each limitedPlaylinks as playlink}
22
+ <PlaylinkIcon {playlink} onclick={onPlaylinkClick} {size} />
23
+ {/each}
24
+
25
+ {#if playlinks.length > limitedPlaylinks.length}
26
+ <span class="more">
27
+ +{playlinks.length - limitedPlaylinks.length}
28
+ </span>
29
+ {/if}
30
+
31
+ {#if !playlinks.length}
32
+ <div class="empty" data-testid="playlinks-empty">
33
+ Unavailable to stream
34
+ </div>
35
+ {/if}
36
+ </div>
37
+
38
+ <style lang="scss">
39
+ .playlinks {
40
+ display: flex;
41
+ flex-wrap: wrap;
42
+ gap: margin(0.25);
43
+ margin-top: auto;
44
+ }
45
+
46
+ .more {
47
+ display: flex;
48
+ align-items: center;
49
+ padding: 0 margin(0.125);
50
+ color: theme(list-item-more-text-color, text-color-alt);
51
+ font-size: theme(detail-font-size-small, font-size-small);
52
+ }
53
+
54
+ .empty {
55
+ margin-top: margin(0.25);
56
+ opacity: 0.65;
57
+ font-style: italic;
58
+ font-size: 0.85em;
59
+ white-space: initial;
60
+ color: theme(list-item-empty-text-color, text-color-alt);
61
+ }
62
+ </style>
@@ -9,6 +9,7 @@
9
9
  import IconShare from './Icons/IconShare.svelte'
10
10
  import IconLink from './Icons/IconLink.svelte'
11
11
  import IconEmail from './Icons/IconEmail.svelte'
12
+ import Button from './Button.svelte'
12
13
  import { onMount } from 'svelte'
13
14
 
14
15
  interface Props {
@@ -55,9 +56,9 @@
55
56
  <svelte:window onclick={() => showContextMenu = false} />
56
57
 
57
58
  <div class="share">
58
- <button class="button" onclick={toggle} aria-label={t('Share')}>
59
- <IconShare />
60
- </button>
59
+ <Button onclick={toggle}>
60
+ <IconShare /> Share
61
+ </Button>
61
62
 
62
63
  {#if showContextMenu}
63
64
  <div class="context-menu" transition:scale={{ duration: 50, start: 0.85 }}>
@@ -79,30 +80,11 @@
79
80
  position: relative;
80
81
  }
81
82
 
82
- .button {
83
- display: flex;
84
- align-items: center;
85
- justify-content: center;
86
- cursor: pointer;
87
- appearance: none;
88
- border: 0;
89
- border-radius: margin(3);
90
- aspect-ratio: 1 / 1;
91
- background: transparent;
92
- color: theme(detail-text-color-alt, text-color-alt);
93
-
94
- &:hover {
95
- color: theme(detail-text-color, text-color);
96
- background: theme(share-button-hover-background, content);
97
- box-shadow: 0 0 0 2px theme(share-button-hover-background, content);
98
- }
99
- }
100
-
101
83
  .context-menu {
102
84
  z-index: 10;
103
85
  position: absolute;
104
86
  bottom: calc(100% + margin(0.5));
105
- right: 0;
87
+ left: 0;
106
88
  max-width: margin(15);
107
89
  border-radius: $border-radius;
108
90
  background: theme(detail-background, lighter);
@@ -7,6 +7,7 @@
7
7
  import SimilarRail from './Rails/SimilarRail.svelte'
8
8
  import TitlePoster from './TitlePoster.svelte'
9
9
  import Share from './Share.svelte'
10
+ import Trailer from './Trailer.svelte'
10
11
  import { t } from '$lib/localization'
11
12
  import type { TitleData } from '$lib/types/title'
12
13
  import { heading } from '$lib/actions/heading'
@@ -36,26 +37,28 @@
36
37
 
37
38
  <div class="heading" use:heading={2} class:truncate={small} id="title">{title.title}</div>
38
39
 
39
- <div class="row">
40
- <div class="info">
41
- <div class="imdb">
42
- <IconIMDb />
43
- {title.imdb_score}
44
- </div>
40
+ <div class="info">
41
+ <div class="imdb">
42
+ <IconIMDb />
43
+ {title.imdb_score}
44
+ </div>
45
45
 
46
- <Genres genres={title.genres} />
46
+ <Genres genres={title.genres} />
47
47
 
48
- <div>{title.year}</div>
49
- <div class="capitalize">{t(`Type: ${title.type}`)}</div>
48
+ <div>{title.year}</div>
49
+ <div class="capitalize">{t(`Type: ${title.type}`)}</div>
50
50
 
51
- {#if !small && title.length}
52
- <div>{title.length} {t('Minutes')}</div>
53
- {/if}
54
- </div>
51
+ {#if !small && title.length}
52
+ <div>{title.length} {t('Minutes')}</div>
53
+ {/if}
54
+ </div>
55
55
 
56
- <div class="action">
56
+ <div class="actions">
57
+ <!-- !! Button is temporarily always visible while embeddable_url is not yet available -->
58
+ {#if true || title.embeddable_url}
59
+ <Trailer title={title} />
57
60
  <Share title={title.title} url={titleUrl(title)} />
58
- </div>
61
+ {/if}
59
62
  </div>
60
63
  </div>
61
64
 
@@ -146,11 +149,6 @@
146
149
  }
147
150
  }
148
151
 
149
- .row {
150
- display: flex;
151
- align-items: flex-start;
152
- }
153
-
154
152
  .info {
155
153
  display: flex;
156
154
  flex-wrap: wrap;
@@ -182,8 +180,10 @@
182
180
  }
183
181
  }
184
182
 
185
- .action {
186
- margin: margin(-0.125) 0 0 auto;
183
+ .actions {
184
+ display: flex;
185
+ gap: margin(0.5);
186
+ margin-top: margin(0.5);
187
187
  }
188
188
 
189
189
  .background {
@@ -8,6 +8,7 @@
8
8
  import Title from './Title.svelte'
9
9
  import TopScroll from './Ads/TopScroll.svelte'
10
10
  import Display from './Ads/Display.svelte'
11
+ import ExploreCallToAction from './Explore/ExploreCallToAction.svelte'
11
12
 
12
13
  interface Props {
13
14
  title: TitleData
@@ -40,6 +41,8 @@
40
41
  {#snippet bubble()}
41
42
  {#if topScrollAd}
42
43
  <TopScroll campaign={topScrollAd} />
44
+ {:else}
45
+ <ExploreCallToAction />
43
46
  {/if}
44
47
  {/snippet}
45
48
 
@@ -49,6 +52,6 @@
49
52
  {/if}
50
53
  {/snippet}
51
54
 
52
- <Modal {onscroll} {initialScrollPosition} prepend={displayAd ? prepend : null} bubble={topScrollAd ? bubble : null} tall>
55
+ <Modal {onscroll} {initialScrollPosition} {bubble} prepend={displayAd ? prepend : null} tall>
53
56
  <Title {title} />
54
57
  </Modal>
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import { t } from '$lib/localization'
3
+ import { openTrailerOverlay } from '$lib/trailer'
4
+ import type { TitleData } from '$lib/types/title'
5
+ import Button from './Button.svelte'
6
+ import IconPlay from './Icons/IconPlay.svelte'
7
+
8
+ interface Props {
9
+ title: TitleData
10
+ }
11
+
12
+ const { title }: Props = $props()
13
+ </script>
14
+
15
+ <Button onclick={() => openTrailerOverlay(title)}>
16
+ <IconPlay />
17
+ {t('Watch Trailer')}
18
+ </Button>
@@ -0,0 +1,96 @@
1
+ <script lang="ts">
2
+ import { fade } from 'svelte/transition'
3
+ import IconClose from './Icons/IconClose.svelte'
4
+
5
+ interface Props {
6
+ embeddable_url: string,
7
+ onclose: () => void
8
+ }
9
+
10
+ const { embeddable_url, onclose }: Props = $props()
11
+
12
+ const videoId = $derived(getVideoId(embeddable_url))
13
+
14
+ // Gets the YouTube ID from a url, can be a large number of differnet formats
15
+ // https://stackoverflow.com/a/54200105/1665157
16
+ function getVideoId(url: string): string | null {
17
+ const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/
18
+ const match = url.match(regExp)
19
+
20
+ return match?.[7] || null
21
+ }
22
+ </script>
23
+
24
+ <div class="overlay" transition:fade={{ duration: 100 }}>
25
+ {#if videoId}
26
+ <iframe width="600" height="338" src="https://www.youtube.com/embed/{videoId}?autoplay=true" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
27
+ {:else}
28
+ Something went wrong
29
+ {/if}
30
+
31
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
32
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
33
+ <div class="backdrop" onclick={onclose} data-testid="backdrop"></div>
34
+
35
+ <button class="close" onclick={onclose} aria-label="Close">
36
+ <IconClose size={24} />
37
+ </button>
38
+ </div>
39
+
40
+ <style lang="scss">
41
+ iframe {
42
+ z-index: 1;
43
+ position: relative;
44
+ display: block;
45
+ width: 95vmin;
46
+ height: auto;
47
+ aspect-ratio: 16/9;
48
+ box-shadow: 0 0 margin(4) rgba(255, 255, 255, 0.15);
49
+ background: black;
50
+ }
51
+
52
+ .overlay {
53
+ z-index: 2147483647; // As high as she goes
54
+ box-sizing: border-box;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ position: fixed;
59
+ top: 0;
60
+ right: 0;
61
+ bottom: 0;
62
+ left: 0;
63
+ background: theme(detail-backdrop, rgba(0, 0, 0, 0.95));
64
+ }
65
+
66
+ .backdrop {
67
+ z-index: 0;
68
+ position: fixed;
69
+ top: 0;
70
+ right: 0;
71
+ bottom: 0;
72
+ left: 0;
73
+ }
74
+
75
+ .close {
76
+ appearance: none;
77
+ z-index: 1;
78
+ position: fixed;
79
+ top: margin(2);
80
+ right: margin(2);
81
+ padding: 0;
82
+ margin: 0;
83
+ border: 0;
84
+ background: transparent;
85
+ color: white;
86
+ cursor: pointer;
87
+
88
+ &:hover {
89
+ transform: scale(1.1);
90
+ }
91
+
92
+ &:active {
93
+ transform: scale(1);
94
+ }
95
+ }
96
+ </style>
@@ -2,6 +2,7 @@
2
2
  import { browser } from '$app/environment'
3
3
  import { title, linkInjections, campaign, participants } from '$lib/fakeData'
4
4
  import { openModal } from '$lib/modal'
5
+ import { insertExplore, insertExploreIntoNavigation } from '$lib/explore'
5
6
  import Disclaimer from '../components/Ads/Disclaimer.svelte'
6
7
  import Display from '../components/Ads/Display.svelte'
7
8
  import TopScroll from '../components/Ads/TopScroll.svelte'
@@ -16,9 +17,22 @@
16
17
  import Title from '../components/Title.svelte'
17
18
  import TitlePopover from '../components/TitlePopover.svelte'
18
19
  import Tooltip from '../components/Tooltip.svelte'
20
+ import Explore from '../components/Explore/Explore.svelte'
21
+
22
+ if (browser) {
23
+ // @ts-ignore
24
+ window.PlayPilotLinkInjections = {
25
+ token: 'ZoAL14yqzevMyQiwckbvyetOkeIUeEDN',
26
+ config: {
27
+ explore_navigation_selector: 'nav a:last-child',
28
+ explore_navigation_label: 'Streaming guide',
29
+ explore_navigation_path: '/explore',
30
+ },
31
+ }
32
+ }
19
33
 
20
- // @ts-ignore
21
- if (browser) window.PlayPilotLinkInjections = {}
34
+ // Used to re-render some elements with a key
35
+ let renderKey = Math.random()
22
36
  </script>
23
37
 
24
38
  <h1>Elements</h1>
@@ -153,6 +167,29 @@
153
167
  </div>
154
168
  </div>
155
169
 
170
+ <h2>Explore</h2>
171
+
172
+ <div class="group">
173
+ <div class="item">
174
+ <button onclick={() => renderKey = Math.random()}>Rerender</button>
175
+
176
+ {#key renderKey}
177
+ <Explore />
178
+ {/key}
179
+ </div>
180
+
181
+ <div class="item">
182
+ <button onclick={() => openModal({ type: 'explore' })}>Show explore modal</button>
183
+ </div>
184
+
185
+ <div class="item">
186
+ <button onclick={insertExplore}>Insert explore component</button>
187
+ <button onclick={insertExploreIntoNavigation}>Insert explore into navigation</button>
188
+
189
+ <div data-playpilot-explore style="height: 20rem"></div>
190
+ </div>
191
+ </div>
192
+
156
193
  <style lang="scss">
157
194
  @import url('$lib/scss/global.scss');
158
195
 
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ /**
3
+ * This is an example page for inserting the explore component into a page. The script will be loaded on this page,
4
+ * but before the script is loaded some loading state needs to display. This will be supplied to the third party
5
+ * implementing the tag, and will be different for each third party.
6
+ */
7
+
8
+ import { browser } from '$app/environment'
9
+ import { insertExplore, insertExploreIntoNavigation } from '$lib/explore'
10
+
11
+ if (browser) {
12
+ // @ts-ignore
13
+ window.PlayPilotLinkInjections = {
14
+ token: 'ZoAL14yqzevMyQiwckbvyetOkeIUeEDN',
15
+ config: {
16
+ explore_navigation_selector: 'nav a:last-child',
17
+ explore_navigation_label: 'Streaming guide',
18
+ explore_navigation_path: '/explore',
19
+ },
20
+ }
21
+ }
22
+
23
+ insertExploreIntoNavigation()
24
+ // Pretend there is some loading time, as there would be on a real page
25
+ setTimeout(insertExplore, 1500)
26
+ </script>
27
+
28
+ <main>
29
+ <!--
30
+ This is an example of the sort of tag we'd give a partner to insert on their page.
31
+ This exists soley as a loading state while the script is has not loaded.
32
+ The exact styling and what not will be different per partner.
33
+ -->
34
+ <div data-playpilot-explore>
35
+ <div style="padding: 16px 32px; min-height: 100vh; color: white">
36
+ <div style="display: flex; justify-content: center; gap: 8px; margin: 16px 0; font-size: 14px;">
37
+ <strong>Site Name</strong>
38
+ <div style="width: 2px; height: 0.5lh; margin-top: 0.25lh; background: currentColor;"></div>
39
+ Streaming Guide
40
+ </div>
41
+
42
+ <div role="status" aria-live="polite" style="display: flex; flex-direction: column; align-items: center; margin: 0 auto;">
43
+ <svg stroke="currentColor" width="72" viewBox="0 0 24 24"><g><circle cx="12" cy="12" r="9.5" fill="none" stroke-width="3" stroke-linecap="round"><animate attributeName="stroke-dasharray" dur="1.5s" calcMode="spline" values="0 150;42 150;42 150;42 150" keyTimes="0;0.475;0.95;1" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" repeatCount="indefinite"/><animate attributeName="stroke-dashoffset" dur="1.5s" calcMode="spline" values="0;-16;-59;-59" keyTimes="0;0.475;0.95;1" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" repeatCount="indefinite"/></circle><animateTransform attributeName="transform" type="rotate" dur="2s" values="0 12 12;360 12 12" repeatCount="indefinite"/></g></svg>
44
+ <div style="margin: 12px 0;">Loading…</div>
45
+ <noscript>Sorry, this page requires JavaScript to be enabled.</noscript>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </main>
50
+
51
+ <style lang="scss">
52
+ @import url('$lib/scss/global.scss');
53
+
54
+ main {
55
+ margin: margin(2) margin(-2) margin(-2);
56
+ padding: 0;
57
+ background: theme(light);
58
+ }
59
+ </style>
60
+
@@ -10,7 +10,6 @@ vi.mock('$lib/api/api', () => ({
10
10
  api: vi.fn(),
11
11
  }))
12
12
 
13
-
14
13
  vi.mock('$lib/tracking', () => ({
15
14
  track: vi.fn(),
16
15
  }))
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ import { api } from '$lib/api/api'
4
+ import { fetchSimilarTitles, fetchTitles } from '$lib/api/titles'
5
+ import { title } from '$lib/fakeData'
6
+ import { getApiToken } from '$lib/token'
7
+
8
+ vi.mock('$lib/token', () => ({
9
+ getApiToken: vi.fn(),
10
+ }))
11
+
12
+ vi.mock('$lib/api/api', () => ({
13
+ api: vi.fn(),
14
+ }))
15
+
16
+ describe('$lib/api/ads', () => {
17
+ beforeEach(() => {
18
+ vi.resetAllMocks()
19
+ vi.mocked(getApiToken).mockReturnValue('some-token')
20
+ })
21
+
22
+ describe('fetchTitles', () => {
23
+ it('Should call api with given parameters and return response', async () => {
24
+ vi.mocked(api).mockResolvedValueOnce({ results: [title] })
25
+
26
+ const response = await fetchTitles({ some: 'thing' })
27
+
28
+ expect(api).toHaveBeenCalledWith('/titles/browse?api-token=some-token&some=thing')
29
+ expect(response).toEqual({ results: [title] })
30
+ })
31
+
32
+ it('Should throw when api returns error', async () => {
33
+ vi.mocked(api).mockRejectedValueOnce({ error: 'message' })
34
+
35
+ await expect(async () => await fetchTitles()).rejects.toThrow()
36
+ })
37
+ })
38
+
39
+ describe('fetchSimilarTitles', () => {
40
+ it('Should call api with with sid for given title and return array of titles', async () => {
41
+ vi.mocked(api).mockResolvedValueOnce({ results: [title] })
42
+
43
+ const response = await fetchSimilarTitles(title)
44
+
45
+ expect(api).toHaveBeenCalledWith(`/titles/browse?api-token=some-token&related_to_sid=${title.sid}`)
46
+ expect(response).toEqual([title])
47
+ })
48
+
49
+ it('Should throw when api returns error', async () => {
50
+ vi.mocked(api).mockRejectedValueOnce({ error: 'message' })
51
+
52
+ await expect(async () => await fetchSimilarTitles(title)).rejects.toThrow()
53
+ })
54
+ })
55
+ })