@playpilot/tpi 5.32.0 → 5.33.0-beta.explore.1

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 (43) hide show
  1. package/dist/link-injections.js +10 -11
  2. package/package.json +1 -1
  3. package/src/lib/api/titles.ts +13 -1
  4. package/src/lib/data/translations.ts +5 -0
  5. package/src/lib/explore.ts +26 -0
  6. package/src/lib/fakeData.ts +1 -0
  7. package/src/lib/images/titles-list.webp +0 -0
  8. package/src/lib/modal.ts +7 -6
  9. package/src/lib/trailer.ts +22 -0
  10. package/src/lib/types/api.d.ts +6 -0
  11. package/src/lib/types/title.d.ts +4 -1
  12. package/src/routes/+page.svelte +5 -1
  13. package/src/routes/components/Ads/TopScroll.svelte +4 -18
  14. package/src/routes/components/Button.svelte +73 -0
  15. package/src/routes/components/Explore/Explore.svelte +178 -0
  16. package/src/routes/components/Explore/ExploreCallToAction.svelte +58 -0
  17. package/src/routes/components/Explore/ExploreModal.svelte +15 -0
  18. package/src/routes/components/Explore/Filter.svelte +3 -0
  19. package/src/routes/components/Explore/Search.svelte +54 -0
  20. package/src/routes/components/Icons/IconClose.svelte +9 -1
  21. package/src/routes/components/Icons/IconPlay.svelte +3 -0
  22. package/src/routes/components/Icons/IconSearch.svelte +3 -0
  23. package/src/routes/components/ListTitle.svelte +3 -5
  24. package/src/routes/components/ListTitleSkeleton.svelte +42 -0
  25. package/src/routes/components/Modal.svelte +5 -23
  26. package/src/routes/components/ParticipantModal.svelte +1 -11
  27. package/src/routes/components/Share.svelte +5 -23
  28. package/src/routes/components/Title.svelte +22 -22
  29. package/src/routes/components/TitleModal.svelte +4 -1
  30. package/src/routes/components/Trailer.svelte +18 -0
  31. package/src/routes/components/YouTubeEmbedOverlay.svelte +96 -0
  32. package/src/routes/elements/+page.svelte +28 -1
  33. package/src/tests/lib/api/ads.test.js +0 -1
  34. package/src/tests/lib/api/titles.test.js +55 -0
  35. package/src/tests/lib/explore.test.js +49 -0
  36. package/src/tests/lib/trailer.test.js +56 -0
  37. package/src/tests/routes/components/Button.test.js +28 -0
  38. package/src/tests/routes/components/Explore/Explore.test.js +133 -0
  39. package/src/tests/routes/components/Explore/Search.test.js +26 -0
  40. package/src/tests/routes/components/Share.test.js +12 -12
  41. package/src/tests/routes/components/Title.test.js +13 -0
  42. package/src/tests/routes/components/Trailer.test.js +20 -0
  43. package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +31 -0
@@ -0,0 +1,3 @@
1
+ <svg width="18" height="18" viewBox="0 0 19 19">
2
+ <path fill="currentColor" d="M16.593,2.407a8.155,8.155,0,0,0-11.569,0A8.166,8.166,0,0,0,4.18,12.983L.38,16.8a1.3,1.3,0,0,0,0,1.837A1.372,1.372,0,0,0,1.288,19a1.3,1.3,0,0,0,.908-.38L6.017,14.8A8.172,8.172,0,0,0,16.593,2.407ZM15.327,12.688A6.4,6.4,0,1,1,17.184,8.17,6.324,6.324,0,0,1,15.327,12.688Z" />
3
+ </svg>
@@ -90,7 +90,7 @@
90
90
  align-items: center;
91
91
  width: 100%;
92
92
  background: theme(list-item-background, lighter);
93
- padding: margin(0.5);
93
+ padding: theme(list-item-padding, margin(0.5));
94
94
  border: 0;
95
95
  border-radius: theme(list-item-border-radius, border-radius);
96
96
  text-decoration: none;
@@ -98,10 +98,8 @@
98
98
 
99
99
  &:hover {
100
100
  filter: brightness(theme(list-item-hover-filter-brightness, hover-filter-brightness));
101
- }
102
-
103
- &:last-child {
104
- border-bottom: 0;
101
+ background: theme(list-item-hover-background, lighter);
102
+ box-shadow: theme(list-item-hover-shadow, none);
105
103
  }
106
104
  }
107
105
 
@@ -0,0 +1,42 @@
1
+ <div class="skeleton" data-testid="skeleton">
2
+ <div class="poster"></div>
3
+
4
+ <div class="lines">
5
+ <div class="line" style:width="40%"></div>
6
+ <div class="line" style:width="60%"></div>
7
+ <div class="line" style:width="80%"></div>
8
+ </div>
9
+ </div>
10
+
11
+ <style lang="scss">
12
+ .skeleton {
13
+ display: flex;
14
+ align-items: center;
15
+ width: 100%;
16
+ gap: margin(1);
17
+ }
18
+
19
+ .poster {
20
+ flex: 0 0 auto;
21
+ height: auto;
22
+ align-self: start;
23
+ width: margin(4.125);
24
+ aspect-ratio: 2/3;
25
+ border-radius: theme(detail-image-border-radius, border-radius);
26
+ background: theme(content);
27
+ }
28
+
29
+ .lines {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: margin(0.5);
33
+ width: 100%;
34
+ }
35
+
36
+ .line {
37
+ width: 60%;
38
+ height: theme(font-size-base);
39
+ background: theme(content);
40
+ border-radius: 5rem;
41
+ }
42
+ </style>
@@ -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)
@@ -95,7 +92,7 @@
95
92
  onhashchange={close}
96
93
  bind:innerWidth={windowWidth} />
97
94
 
98
- <div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} use:focustrap class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
95
+ <div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} use:focustrap class:has-bubble={!!bubble} class:has-prepend={!!prepend}>
99
96
  {#if prepend}
100
97
  <div class="prepend" transition:scaleOrFly|global={{ y: -10 }} data-view-transition-new="playpilot-title-extra">
101
98
  {@render prepend()}
@@ -103,7 +100,7 @@
103
100
  {/if}
104
101
 
105
102
  {#if bubble}
106
- <div class="bubble" class:inline={inlineBubble} transition:scaleOrFly|global={{ y: inlineBubble ? window.innerHeight : -10 }} data-view-transition-new="playpilot-title-extra">
103
+ <div class="bubble" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
107
104
  {@render bubble()}
108
105
  </div>
109
106
  {/if}
@@ -285,28 +282,13 @@
285
282
  .bubble {
286
283
  z-index: 1;
287
284
  position: relative;
288
- width: calc(100% - margin(1));
285
+ width: 100%;
289
286
  max-width: $max-width;
290
- margin: margin(0.5);
287
+ margin: auto 0 0;
291
288
 
292
289
  @include desktop() {
293
290
  width: 100%;
294
- margin: 0 0 margin(0.5);
295
- }
296
-
297
- &.inline {
298
- width: 100%;
299
- margin: auto 0 0;
300
-
301
- @include desktop() {
302
- margin-top: 0;
303
- }
304
- }
305
-
306
- .prepend + & {
307
- &:not(.inline) {
308
- margin-top: 0;
309
- }
291
+ margin-top: 0;
310
292
  }
311
293
  }
312
294
  </style>
@@ -12,17 +12,7 @@
12
12
  initialScrollPosition?: number
13
13
  }
14
14
 
15
- const { participant = {
16
- sid: 'pr5C5W',
17
- name: 'James Franco',
18
- birth_date: '1978-04-19',
19
- death_date: null,
20
- jobs: ['actor'],
21
- image: 'https://hips.hearstapps.com/hmg-prod/images/gettyimages-161098947-square.jpg',
22
- image_uuid: '08ed2fac357011eb87470aff12c0f5c9',
23
- gender: 'Male',
24
- bio: 'Aenean feugiat nec odio at venenatis. Integer porta neque metus, a sollicitudin dolor dapibus et. In sollicitudin nulla eget ultricies porttitor. Nulla facilisi. Sed turpis orci, facilisis efficitur neque in, ultrices ultricies purus. Mauris nec augue a nisi imperdiet semper ut nec tellus. Donec at tristique odio. Etiam luctus eget metus non mattis. Integer imperdiet in elit eu varius. Donec ornare, nibh vitae accumsan consequat, lacus nulla elementum sapien, a scelerisque tellus augue ac erat. Aenean finibus fringilla magna, ac laoreet nisl convallis vel. Proin laoreet ex ac augue maximus, nec gravida tortor pharetra.\nCurabitur maximus dui sed risus placerat pharetra vitae ut orci. Proin sodales enim a elit euismod, a varius sem suscipit. Vivamus eu magna cursus, fringilla est in, mollis nunc. Mauris fringilla eleifend nibh, eget auctor lectus bibendum non. Praesent sed elit ipsum. Donec nunc dolor, sagittis hendrerit gravida et, lacinia sed metus. Morbi tempus mi massa. In hac habitasse platea dictumst. Suspendisse aliquet tincidunt lectus ut elementum.',
25
- }, initialScrollPosition = 0 }: Props = $props()
15
+ const { participant, initialScrollPosition = 0 }: Props = $props()
26
16
 
27
17
  let windowWidth = $state(0)
28
18
 
@@ -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 } 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,13 @@
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'
19
21
 
20
22
  // @ts-ignore
21
- if (browser) window.PlayPilotLinkInjections = {}
23
+ if (browser) window.PlayPilotLinkInjections = { token: 'ZoAL14yqzevMyQiwckbvyetOkeIUeEDN' }
24
+
25
+ // Used to re-render some elements with a key
26
+ let renderKey = Math.random()
22
27
  </script>
23
28
 
24
29
  <h1>Elements</h1>
@@ -153,6 +158,28 @@
153
158
  </div>
154
159
  </div>
155
160
 
161
+ <h2>Explore</h2>
162
+
163
+ <div class="group">
164
+ <div class="item">
165
+ <button onclick={() => renderKey = Math.random()}>Rerender</button>
166
+
167
+ {#key renderKey}
168
+ <Explore />
169
+ {/key}
170
+ </div>
171
+
172
+ <div class="item">
173
+ <button onclick={() => openModal({ type: 'explore' })}>Show explore modal</button>
174
+ </div>
175
+
176
+ <div class="item">
177
+ <button onclick={insertExplore}>Insert explore component</button>
178
+
179
+ <div data-playpilot-explore></div>
180
+ </div>
181
+ </div>
182
+
156
183
  <style lang="scss">
157
184
  @import url('$lib/scss/global.scss');
158
185
 
@@ -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
+ })
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { destroyExplore, insertExplore } from '$lib/explore'
3
+ import { mount, unmount } from 'svelte'
4
+ import { waitFor } from '@testing-library/svelte'
5
+
6
+ vi.mock('svelte', () => ({
7
+ mount: vi.fn(() => true),
8
+ unmount: vi.fn(),
9
+ }))
10
+
11
+ describe('explore.js', () => {
12
+ beforeEach(() => {
13
+ destroyExplore()
14
+
15
+ vi.resetAllMocks()
16
+ vi.mocked(mount).mockReturnValue({})
17
+ })
18
+
19
+ describe('insertExplore', () => {
20
+ it('Should not call mount when no target element is found', () => {
21
+ insertExplore()
22
+
23
+ expect(mount).not.toHaveBeenCalled()
24
+ })
25
+
26
+ it('Should call mount when target is found', () => {
27
+ document.body.innerHTML = '<div data-playpilot-explore></div>'
28
+
29
+ insertExplore()
30
+
31
+ expect(mount).toHaveBeenCalled()
32
+ })
33
+ })
34
+
35
+ describe('destroyExplore', () => {
36
+ it('Should not call unmount if component was not previously mounted', () => {
37
+ destroyExplore()
38
+
39
+ expect(unmount).not.toHaveBeenCalled()
40
+ })
41
+
42
+ it('Should call unmount if component was previously mounted', () => {
43
+ insertExplore()
44
+ destroyExplore()
45
+
46
+ expect(unmount).toHaveBeenCalled()
47
+ })
48
+ })
49
+ })
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { closeCurrentModal, destroyAllModals, destroyCurrentModal, getAllModals, getPreviousModal, goBackToPreviousModal, openModal } from '$lib/modal'
3
+ import { linkInjections, title } from '$lib/fakeData'
4
+ import { mount, unmount } from 'svelte'
5
+ import ParticipantModal from '../../routes/components/ParticipantModal.svelte'
6
+ import TitleModal from '../../routes/components/TitleModal.svelte'
7
+ import { closeTrailerOverlay, openTrailerOverlay } from '$lib/trailer'
8
+
9
+ vi.mock('svelte', () => ({
10
+ mount: vi.fn(),
11
+ unmount: vi.fn(),
12
+ }))
13
+
14
+ const titleWithTrailer = { ...title, embeddable_url: 'abc' }
15
+
16
+ describe('modal.js', () => {
17
+ beforeEach(() => {
18
+ vi.resetAllMocks()
19
+ })
20
+
21
+ describe('openTrailerOverlay', () => {
22
+ it('Should call mount with given title embeddable_url', () => {
23
+ openTrailerOverlay(titleWithTrailer)
24
+
25
+ expect(mount).toHaveBeenCalledWith(
26
+ expect.any(Function),
27
+ expect.objectContaining({
28
+ target: expect.anything(),
29
+ props: expect.objectContaining({
30
+ onclose: expect.any(Function),
31
+ embeddable_url: titleWithTrailer.embeddable_url,
32
+ }),
33
+ }),
34
+ )
35
+ })
36
+ })
37
+
38
+ describe('closeTrailerOverlay', () => {
39
+ it('Should call unmount if component was previously mounted', () => {
40
+ vi.mocked(mount).mockReturnValueOnce({})
41
+
42
+ openTrailerOverlay(titleWithTrailer)
43
+ closeTrailerOverlay()
44
+
45
+ expect(unmount).toHaveBeenCalled()
46
+ })
47
+
48
+ it('Should not call unmount if component was not previously mounted', () => {
49
+ vi.mocked(mount).mockReturnValueOnce({})
50
+
51
+ closeTrailerOverlay()
52
+
53
+ expect(unmount).not.toHaveBeenCalled()
54
+ })
55
+ })
56
+ })
@@ -0,0 +1,28 @@
1
+ import { render, fireEvent } from '@testing-library/svelte'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import Button from '../../../routes/components/Button.svelte'
5
+
6
+ describe('Button.svelte', () => {
7
+ it('Should use filled class by default', () => {
8
+ const { getByRole } = render(Button)
9
+
10
+ expect(getByRole('button').classList).toContain('filled')
11
+ })
12
+
13
+ it('Should use border class when border variant is given', () => {
14
+ const { getByRole } = render(Button, { variant: 'border' })
15
+
16
+ expect(getByRole('button').classList).not.toContain('filled')
17
+ expect(getByRole('button').classList).toContain('border')
18
+ })
19
+
20
+ it('Should fire given onclick function on click', async () => {
21
+ const onclick = vi.fn()
22
+ const { getByRole } = render(Button, { onclick })
23
+
24
+ await fireEvent.click(getByRole('button'))
25
+
26
+ expect(onclick).toHaveBeenCalled()
27
+ })
28
+ })