@playpilot/tpi 8.29.11 → 8.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/editorial.mount.js +9 -9
  2. package/dist/link-injections.js +1 -1
  3. package/dist/mount.js +7 -7
  4. package/package.json +1 -1
  5. package/src/lib/api/externalPages.ts +1 -0
  6. package/src/lib/api/participants.ts +1 -1
  7. package/src/lib/api/providers.ts +1 -1
  8. package/src/lib/api/search.ts +9 -0
  9. package/src/lib/api/titles.ts +1 -1
  10. package/src/lib/injection.ts +479 -479
  11. package/src/lib/types/global.d.ts +1 -0
  12. package/src/routes/components/Editorial/Editor.svelte +0 -2
  13. package/src/routes/components/Editorial/EditorItem.svelte +2 -2
  14. package/src/routes/components/Editorial/ManualInjection.svelte +26 -19
  15. package/src/routes/components/Editorial/Search/ParticipantSearchItem.svelte +86 -0
  16. package/src/routes/components/Editorial/Search/TypeSearch.svelte +120 -0
  17. package/src/routes/components/Explore/Routes/ExploreModal.svelte +107 -107
  18. package/src/routes/components/Modals/TitlesReelModal.svelte +3 -3
  19. package/src/routes/components/Rails/TitlesRail.svelte +1 -1
  20. package/src/routes/components/Tabs.svelte +12 -23
  21. package/src/routes/components/YouTubeEmbed.svelte +61 -20
  22. package/src/routes/components/YouTubeEmbedBackground.svelte +1 -0
  23. package/src/routes/components/YouTubeEmbedOverlay.svelte +1 -1
  24. package/src/routes/elements/+page.svelte +12 -0
  25. package/src/tests/lib/api/externalPages.test.js +134 -1
  26. package/src/tests/routes/components/Editorial/EditorItem.test.js +1 -2
  27. package/src/tests/routes/components/Editorial/ManualInjection.test.js +43 -6
  28. package/src/tests/routes/components/Editorial/Search/{TitleSearch.test.js → TypeSearch.test.js} +21 -9
  29. package/src/tests/routes/components/YouTubeEmbed.test.js +109 -33
  30. package/src/tests/routes/components/YouTubeEmbedBackground.test.js +2 -3
  31. package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +2 -3
  32. package/src/routes/components/Editorial/Search/TitleSearch.svelte +0 -96
@@ -14,14 +14,14 @@
14
14
  fallbackStartTime?: number
15
15
  }
16
16
 
17
- const { embeddable_url = '', controls = [], muted = false, loop = false, captions = false, autoplay = true, showMuteControls = false, fallbackStartTime = 0 }: Props = $props()
17
+ let { embeddable_url = '', controls = [], muted = false, loop = false, captions = false, autoplay = true, showMuteControls = false, fallbackStartTime = 0 }: Props = $props()
18
18
 
19
19
  const videoId = $derived(getVideoId(embeddable_url))
20
20
  const startTime = $derived((window?.PlayPilotLinkInjections?.video_playtimes?.[videoId || ''] || (fallbackStartTime + 1)) - 1)
21
21
  const color = window?.getComputedStyle(document.body).getPropertyValue('--playpilot-primary')?.replace('#', '') || 'fa548a'
22
22
 
23
- let iframe: HTMLIFrameElement | null = $state(null)
24
- let isMuted = $state(muted)
23
+ let element: HTMLElement | null = $state(null)
24
+ let player: any
25
25
 
26
26
  onMount(() => {
27
27
  if (!videoId) return
@@ -37,37 +37,74 @@
37
37
  return () => clearInterval(interval)
38
38
  })
39
39
 
40
- export function toggleMute(state = !isMuted): void {
41
- if (isMuted != state) iframe?.contentWindow?.postMessage('mute', '*')
42
- isMuted = state
40
+ function initialize(): void {
41
+ if (typeof window.Plyr === 'undefined') return
42
+ if (!element) return
43
+
44
+ element.dataset.plyrEmbedId = videoId!
45
+ element.style.setProperty('--plyr-color-main', '#' + color)
46
+
47
+ player = new window.Plyr(element, {
48
+ controls,
49
+ playsinline: true,
50
+ autoplay,
51
+ muted,
52
+ clickToPlay: false,
53
+ youtube: {
54
+ noCookie: true,
55
+ cc_load_policy: captions ? 1 : 0,
56
+ cc_lang_pref: 'auto',
57
+ },
58
+ })
59
+
60
+ player.on('ready', () => {
61
+ player.currentTime = startTime
62
+ toggleMute(muted)
63
+ })
64
+
65
+ if (!loop) return
66
+
67
+ player.on('statechange', (event: { detail: { code: number } }) => {
68
+ if (event.detail.code !== 0) return
69
+
70
+ requestAnimationFrame(() => {
71
+ player.play()
72
+ toggleMute(muted)
73
+ })
74
+ })
75
+ }
76
+
77
+ export function toggleMute(state = !muted): void {
78
+ player.muted = state
79
+ player.volume = muted ? 0 : 1
80
+
81
+ muted = state
43
82
  }
44
83
 
45
84
  export function play(event: CustomEvent): void {
46
- if (event.detail === embeddable_url) iframe?.contentWindow?.postMessage('play', '*')
85
+ if (event.detail === embeddable_url) player?.play()
47
86
  }
48
87
 
49
88
  export function pause(event: CustomEvent): void {
50
- if (event.detail === embeddable_url) iframe?.contentWindow?.postMessage('pause', '*')
89
+ if (event.detail === embeddable_url) player?.pause()
51
90
  }
52
91
  </script>
53
92
 
54
93
  <svelte:window onplayembed={play} onpauseembed={pause} />
55
94
 
95
+ <svelte:head>
96
+ {#if videoId}
97
+ <script src="https://unpkg.com/plyr@3" onload={initialize}></script>
98
+ <link rel="stylesheet" href="https://unpkg.com/plyr@3/dist/plyr.css" />
99
+ {/if}
100
+ </svelte:head>
101
+
56
102
  {#if videoId}
57
- <iframe
58
- bind:this={iframe}
59
- width="600"
60
- height="338"
61
- src="https://video.playpilot.net/?video_id={videoId}&color={color}&muted={muted}&loop={loop}&captions={captions}&controls={controls.join(',')}&start_time={startTime}&autoplay={autoplay}&playsinline=true"
62
- title="YouTube video player"
63
- frameborder="0"
64
- referrerpolicy="strict-origin-when-cross-origin"
65
- allowfullscreen>
66
- </iframe>
103
+ <div bind:this={element} class="player" data-plyr-provider="youtube" data-plyr-embed-id data-testid={embeddable_url}></div>
67
104
 
68
105
  {#if showMuteControls}
69
106
  <button class="mute" onclick={() => toggleMute()} aria-label="Mute">
70
- <IconMute muted={isMuted} />
107
+ <IconMute {muted} />
71
108
  </button>
72
109
  {/if}
73
110
  {:else}
@@ -75,7 +112,11 @@
75
112
  {/if}
76
113
 
77
114
  <style lang="scss">
78
- iframe {
115
+ .player {
116
+ --plyr-control-icon-size: 24px;
117
+ --plyr-video-controls-background: linear-gradient(rgba(0, 0, 0, 0), black);
118
+ --plyr-font-size-time: 16px;
119
+ --plyr-font-family: 'Consolas', monospace;
79
120
  z-index: -1;
80
121
  position: relative;
81
122
  width: 100%;
@@ -22,6 +22,7 @@
22
22
  .video-background {
23
23
  background: black;
24
24
 
25
+ :global(.plyr),
25
26
  :global(iframe) {
26
27
  position: absolute;
27
28
  height: var(--height);
@@ -37,7 +37,7 @@
37
37
  left: 0;
38
38
  background: theme(detail-backdrop, rgba(0, 0, 0, 0.95));
39
39
 
40
- :global(iframe) {
40
+ :global(.plyr) {
41
41
  z-index: 1;
42
42
  position: relative;
43
43
  display: block;
@@ -22,6 +22,7 @@
22
22
  import TitlesRail from '../components/Rails/TitlesRail.svelte'
23
23
  import ParticipantsRail from '../components/Rails/ParticipantsRail.svelte'
24
24
  import InfiniteTitlesRail from '../components/Rails/InfiniteTitlesRail.svelte'
25
+ import YouTubeEmbed from '../components/YouTubeEmbed.svelte'
25
26
 
26
27
  if (browser) {
27
28
  // @ts-ignore
@@ -43,6 +44,12 @@
43
44
 
44
45
  <p>This page lays out various elements used in TPI. It does not currently include editor elements.</p>
45
46
 
47
+ <h2>Youtube Embed</h2>
48
+
49
+ <div class="video">
50
+ <YouTubeEmbed embeddable_url="https://www.youtube.com/watch?v=bBMajXwi6Cs" controls={['progress', 'play']} loop showMuteControls muted />
51
+ </div>
52
+
46
53
  <h2>Title</h2>
47
54
 
48
55
  <div class="group">
@@ -267,4 +274,9 @@
267
274
  padding: margin(2);
268
275
  margin-bottom: margin(1);
269
276
  }
277
+
278
+ .video {
279
+ position: relative;
280
+ max-width: 400px;
281
+ }
270
282
  </style>
@@ -1,10 +1,11 @@
1
1
  import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
2
2
  import { waitFor } from '@testing-library/svelte'
3
3
 
4
- import { fetchLinkInjections, pollLinkInjections, runAiBasedOnResponse } from '$lib/api/externalPages'
4
+ import { fetchLinkInjections, pollLinkInjections, runAiBasedOnResponse, saveLinkInjections } from '$lib/api/externalPages'
5
5
  import { authorize, isEditorialModeEnabled } from '$lib/api/auth'
6
6
  import { Language } from '$lib/enums/Language'
7
7
  import { api } from '$lib/api/api'
8
+ import { generateInjection } from '../../helpers'
8
9
 
9
10
  vi.mock('$lib/api/api', () => ({
10
11
  api: vi.fn(),
@@ -379,4 +380,136 @@ describe('$lib/api/externalPages', () => {
379
380
  expect(api).toHaveBeenCalled()
380
381
  })
381
382
  })
383
+
384
+ describe('saveLinkInjections', () => {
385
+ beforeEach(() => {
386
+ // @ts-ignore
387
+ window.PlayPilotLinkInjections = { token: 'a' }
388
+ })
389
+
390
+ const manualInjection = { ...generateInjection('Some injection', 'injection'), manual: true }
391
+ const aiInjection = generateInjection('Some other injection', 'other injection')
392
+
393
+ it('Should call api with POST method and only manual injections included', async () => {
394
+ vi.mocked(api).mockResolvedValueOnce({ ok: true })
395
+
396
+ // @ts-ignore
397
+ await saveLinkInjections([manualInjection, aiInjection])
398
+
399
+ expect(api).toHaveBeenCalledWith(
400
+ expect.stringContaining('api-token'),
401
+ expect.objectContaining({
402
+ method: 'POST',
403
+ body: expect.objectContaining({
404
+ manual_injections: [
405
+ {
406
+ sid: manualInjection.sid,
407
+ title: manualInjection.title,
408
+ sentence: manualInjection.sentence,
409
+ phrase_before: manualInjection.phrase_before,
410
+ phrase_after: manualInjection.phrase_after,
411
+ playpilot_url: manualInjection.playpilot_url,
412
+ after_article: false,
413
+ after_article_style: null,
414
+ in_text: true,
415
+ inactive: false,
416
+ removed: false,
417
+ type: manualInjection.type,
418
+ },
419
+ ],
420
+ }),
421
+ }),
422
+ )
423
+ })
424
+
425
+ it('Should apply given values for after_article, in_text, inactive and removed instead of defaults', async () => {
426
+ vi.mocked(api).mockResolvedValueOnce({ ok: true })
427
+
428
+ const injection = {
429
+ ...manualInjection,
430
+ after_article: true,
431
+ after_article_style: 'modal_button',
432
+ in_text: false,
433
+ inactive: true,
434
+ removed: true,
435
+ }
436
+
437
+ // @ts-ignore
438
+ await saveLinkInjections([injection])
439
+
440
+ expect(api).toHaveBeenCalledWith(
441
+ expect.any(String),
442
+ expect.objectContaining({
443
+ body: expect.objectContaining({
444
+ manual_injections: [
445
+ expect.objectContaining({
446
+ after_article: true,
447
+ after_article_style: 'modal_button',
448
+ in_text: false,
449
+ inactive: true,
450
+ removed: true,
451
+ }),
452
+ ],
453
+ }),
454
+ }),
455
+ )
456
+ })
457
+
458
+ it('Should save with given type', async () => {
459
+ vi.mocked(api).mockResolvedValueOnce({ ok: true })
460
+
461
+ // @ts-ignore
462
+ await saveLinkInjections([{ ...manualInjection, type: 'participant' }])
463
+
464
+ expect(api).toHaveBeenCalledWith(
465
+ expect.any(String),
466
+ expect.objectContaining({
467
+ body: expect.objectContaining({
468
+ manual_injections: [
469
+ expect.objectContaining({
470
+ type: 'participant',
471
+ }),
472
+ ],
473
+ }),
474
+ }),
475
+ )
476
+ })
477
+
478
+ it('Should include content_text_selector from window.PlayPilotLinkInjections.selector', async () => {
479
+ vi.mocked(api).mockResolvedValueOnce({ ok: true })
480
+
481
+ // @ts-ignore
482
+ window.PlayPilotLinkInjections = { token: 'a', selector: '.some-element' }
483
+
484
+ // @ts-ignore
485
+ await saveLinkInjections([manualInjection])
486
+
487
+ expect(api).toHaveBeenCalledWith(
488
+ expect.any(String),
489
+ expect.objectContaining({
490
+ body: expect.objectContaining({
491
+ content_text_selector: '.some-element',
492
+ }),
493
+ }),
494
+ )
495
+ })
496
+
497
+ it('Should return the original linkInjections array unchanged, including non-manual injections', async () => {
498
+ vi.mocked(api).mockResolvedValueOnce({ ok: true })
499
+
500
+ const linkInjections = [manualInjection, aiInjection]
501
+
502
+ // @ts-ignore
503
+ const result = await saveLinkInjections(linkInjections)
504
+
505
+ expect(result).toEqual(linkInjections)
506
+ })
507
+
508
+ it('Should throw when the response is faulty', async () => {
509
+ vi.mocked(api).mockResolvedValueOnce(undefined)
510
+
511
+ // @ts-ignore
512
+ await expect(async () => await saveLinkInjections([manualInjection])).rejects.toThrowError('saveLinkInjections response was faulty')
513
+ })
514
+ })
382
515
  })
@@ -207,7 +207,6 @@ describe('EditorItem.svelte', () => {
207
207
 
208
208
  expect(queryByLabelText('Expand')).not.toBeTruthy()
209
209
  expect(getByText(fakeLinkInjections[4].participant_details?.name || '-')).toBeTruthy()
210
- expect(container.querySelector('.poster')).not.toBeTruthy()
211
- expect(container.querySelector('.placeholder-image')).toBeTruthy()
210
+ expect(container.querySelector('.participant')).toBeTruthy()
212
211
  })
213
212
  })
@@ -2,9 +2,10 @@ import { fireEvent, render, waitFor } from '@testing-library/svelte'
2
2
  import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import ManualInjection from '../../../../routes/components/Editorial/ManualInjection.svelte'
5
- import { searchTitles } from '$lib/api/search'
6
- import { title } from '$lib/fakeData'
5
+ import { searchParticipants, searchTitles } from '$lib/api/search'
6
+ import { participants, title } from '$lib/fakeData'
7
7
  import { getIndexOfSelection } from '$lib/selection'
8
+ import { participantUrl, titleUrl } from '$lib/routes'
8
9
 
9
10
  function createSelectionMock() {
10
11
  const container = document.querySelector('div')
@@ -26,6 +27,7 @@ function createSelectionMock() {
26
27
 
27
28
  vi.mock('$lib/api/search', () => ({
28
29
  searchTitles: vi.fn(),
30
+ searchParticipants: vi.fn(),
29
31
  }))
30
32
 
31
33
  vi.mock('$lib/selection', () => ({
@@ -93,7 +95,7 @@ describe('ManualInjection.svelte', () => {
93
95
  sid: title.sid,
94
96
  title: 'text',
95
97
  sentence: 'Some text in a sentence',
96
- playpilot_url: `https://www.playpilot.com/show/${title.slug}`,
98
+ playpilot_url: titleUrl(title),
97
99
  key: expect.any(String),
98
100
  title_details: title,
99
101
  manual: true,
@@ -136,7 +138,7 @@ describe('ManualInjection.svelte', () => {
136
138
  sid: title.sid,
137
139
  title: 'text',
138
140
  sentence: 'Some text in a sentence',
139
- playpilot_url: `https://www.playpilot.com/show/${title.slug}`,
141
+ playpilot_url: titleUrl(title),
140
142
  key: expect.any(String),
141
143
  title_details: title,
142
144
  manual: true,
@@ -179,7 +181,7 @@ describe('ManualInjection.svelte', () => {
179
181
  sid: title.sid,
180
182
  title: 'Some title',
181
183
  sentence: 'Some title',
182
- playpilot_url: `https://www.playpilot.com/show/${title.slug}`,
184
+ playpilot_url: titleUrl(title),
183
185
  key: expect.any(String),
184
186
  title_details: title,
185
187
  manual: true,
@@ -222,7 +224,7 @@ describe('ManualInjection.svelte', () => {
222
224
  sid: title.sid,
223
225
  title: 'Some other paragraph',
224
226
  sentence: 'Some other paragraph',
225
- playpilot_url: `https://www.playpilot.com/show/${title.slug}`,
227
+ playpilot_url: titleUrl(title),
226
228
  key: expect.any(String),
227
229
  title_details: title,
228
230
  manual: true,
@@ -285,4 +287,39 @@ describe('ManualInjection.svelte', () => {
285
287
 
286
288
  await waitFor(() => expect(searchTitles).toHaveBeenCalled())
287
289
  })
290
+
291
+ it('Should save participants when selected', async () => {
292
+ createSelectionMock()
293
+
294
+ const participant = participants[0]
295
+
296
+ vi.mocked(searchParticipants).mockResolvedValueOnce([participant])
297
+ vi.mocked(getIndexOfSelection).mockReturnValueOnce({ start: 5, end: 10 })
298
+
299
+ const onsave = vi.fn()
300
+ const { getByText, getByLabelText } = render(ManualInjection, { onsave })
301
+
302
+ await fireEvent.mouseUp(window)
303
+ await fireEvent.click(getByText('Cast & Crew'))
304
+
305
+ await waitFor(async () => {
306
+ await fireEvent.input(getByLabelText('Search...'), { target: { value: 'test' } })
307
+ await fireEvent.click(getByText(participants[0].name))
308
+ })
309
+
310
+ await fireEvent.click(getByText('Add playlink'))
311
+
312
+ expect(onsave).toHaveBeenCalledWith({
313
+ sid: participants[0].sid,
314
+ title: 'text',
315
+ sentence: 'Some text in a sentence',
316
+ playpilot_url: participantUrl(participant),
317
+ key: expect.any(String),
318
+ participant_details: participant,
319
+ manual: true,
320
+ phrase_before: 'Some',
321
+ phrase_after: 'in a',
322
+ type: 'participant',
323
+ })
324
+ })
288
325
  })
@@ -1,21 +1,22 @@
1
1
  import { fireEvent, render, waitFor } from '@testing-library/svelte'
2
2
  import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
- import TitleSearch from '../../../../../routes/components/Editorial/Search/TitleSearch.svelte'
5
- import { searchTitles } from '$lib/api/search'
4
+ import TypeSearch from '../../../../../routes/components/Editorial/Search/TypeSearch.svelte'
5
+ import { searchTitles, searchParticipants } from '$lib/api/search'
6
6
  import { title } from '$lib/fakeData'
7
7
 
8
8
  vi.mock('$lib/api/search', () => ({
9
9
  searchTitles: vi.fn(),
10
+ searchParticipants: vi.fn(),
10
11
  }))
11
12
 
12
- describe('TitleSearch.svelte', () => {
13
+ describe('TypeSearch.svelte', () => {
13
14
  beforeEach(() => {
14
15
  vi.resetAllMocks()
15
16
  })
16
17
 
17
18
  it('Should search for titles when inputting text', async () => {
18
- const { getByRole } = render(TitleSearch)
19
+ const { getByRole } = render(TypeSearch)
19
20
 
20
21
  await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
21
22
 
@@ -25,7 +26,7 @@ describe('TitleSearch.svelte', () => {
25
26
  it('Should show empty state when no results are returned', async () => {
26
27
  vi.mocked(searchTitles).mockResolvedValueOnce([])
27
28
 
28
- const { getByRole, getByText } = render(TitleSearch)
29
+ const { getByRole, getByText } = render(TypeSearch)
29
30
 
30
31
  await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
31
32
 
@@ -37,7 +38,7 @@ describe('TitleSearch.svelte', () => {
37
38
  it('Should show results when given', async () => {
38
39
  vi.mocked(searchTitles).mockResolvedValueOnce([title])
39
40
 
40
- const { getByRole, getByText, getByTestId } = render(TitleSearch)
41
+ const { getByRole, getByText, getByTestId } = render(TypeSearch)
41
42
 
42
43
  await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
43
44
 
@@ -52,15 +53,26 @@ describe('TitleSearch.svelte', () => {
52
53
  vi.mocked(searchTitles).mockResolvedValueOnce([title])
53
54
 
54
55
  const onselect = vi.fn()
55
- const { getByRole, getByText, queryByTestId } = render(TitleSearch, { onselect, query: '' })
56
+ const { getByRole, getByText, queryByTestId } = render(TypeSearch, { onselect, query: '' })
56
57
 
57
58
  await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
58
59
 
59
60
  await waitFor(async () => {
60
- await fireEvent.click(getByRole('button'))
61
- expect(onselect).toHaveBeenCalledWith(title)
61
+ await fireEvent.click(getByText(title.title))
62
+
63
+ expect(onselect).toHaveBeenCalledWith(title, 'title')
62
64
  expect(queryByTestId('search-results')).not.toBeTruthy()
63
65
  expect(getByText(title.title)).toBeTruthy()
64
66
  })
65
67
  })
68
+
69
+ it('Should search for participants after changing tab when inputting text', async () => {
70
+ const { getByRole, getByText } = render(TypeSearch)
71
+
72
+ await fireEvent.click(getByText('Cast & Crew'))
73
+ await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
74
+
75
+ expect(searchTitles).not.toHaveBeenCalled()
76
+ expect(searchParticipants).toHaveBeenCalled()
77
+ })
66
78
  })
@@ -1,72 +1,148 @@
1
1
  import { render } from '@testing-library/svelte'
2
- import { describe, expect, it } from 'vitest'
3
-
2
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
4
3
  import YouTubeEmbed from '../../../routes/components/YouTubeEmbed.svelte'
5
- import { beforeEach } from 'node:test'
6
4
  import { fakeFetch } from '../../helpers'
7
5
 
6
+ function loadPlyr() {
7
+ const script = document.head.querySelector('script[src*="plyr"]')
8
+ script?.dispatchEvent(new Event('load'))
9
+ }
10
+
8
11
  describe('YouTubeEmbed.svelte', () => {
12
+ /** @type {any} */ let Plyr
13
+ /** @type {any} */ let player
14
+ /** @type {any} */ let onready
15
+ /** @type {any} */ let statechange
16
+
9
17
  beforeEach(() => {
10
18
  fakeFetch()
19
+ onready = undefined
20
+ statechange = undefined
21
+
22
+ player = {
23
+ on: vi.fn((eventName, callback) => {
24
+ if (eventName === 'ready') onready = callback
25
+ if (eventName === 'statechange') statechange = callback
26
+ }),
27
+ play: vi.fn(),
28
+ pause: vi.fn(),
29
+ muted: false,
30
+ volume: 1,
31
+ currentTime: 0,
32
+ }
33
+
34
+ Plyr = vi.fn(() => player)
35
+ window.Plyr = Plyr
11
36
  })
12
37
 
13
- it('Should render embed iframe with given video url and default options', () => {
14
- const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc' })
38
+ afterEach(() => {
39
+ delete window.Plyr
40
+ document.head.innerHTML = ''
41
+ })
15
42
 
16
- expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toBe('https://video.playpilot.net/?video_id=abc&color=fa548a&muted=false&loop=false&captions=false&controls=&start_time=0&autoplay=true&playsinline=true')
43
+ it('Should render player element and initialize Plyr with default options', () => {
44
+ const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc' })
45
+ loadPlyr()
46
+
47
+ /** @type {HTMLElement | null} */
48
+ const playerElement = container.querySelector('.player')
49
+
50
+ expect(playerElement).toBeTruthy()
51
+ expect(playerElement?.dataset.plyrEmbedId).toBe('abc')
52
+ expect(playerElement?.dataset.plyrProvider).toBe('youtube')
53
+
54
+ expect(Plyr).toHaveBeenCalledWith(
55
+ playerElement,
56
+ expect.objectContaining({
57
+ controls: [],
58
+ autoplay: true,
59
+ muted: false,
60
+ clickToPlay: false,
61
+ playsinline: true,
62
+ youtube: expect.objectContaining({ noCookie: true, cc_load_policy: 0, cc_lang_pref: 'auto' }),
63
+ }),
64
+ )
17
65
  })
18
66
 
19
- it('Should render embed iframe with given controls', () => {
20
- const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', controls: ['fullscreen', 'mute'] })
67
+ it('Should initialize Plyr with given controls', () => {
68
+ const { container } = render(YouTubeEmbed, {
69
+ embeddable_url: 'youtube.com/watch?v=abc',
70
+ controls: ['fullscreen', 'mute'],
71
+ })
21
72
 
22
- expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&controls=fullscreen,mute')
73
+ loadPlyr()
74
+
75
+ expect(Plyr).toHaveBeenCalledWith(
76
+ container.querySelector('.player'),
77
+ expect.objectContaining({ controls: ['fullscreen', 'mute'] }),
78
+ )
23
79
  })
24
80
 
25
- it('Should render embed iframe with muted if given', () => {
81
+ it('Should initialize Plyr muted if given', () => {
26
82
  const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', muted: true })
27
83
 
28
- expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&muted=true')
84
+ loadPlyr()
85
+
86
+ expect(Plyr).toHaveBeenCalledWith(
87
+ container.querySelector('.player'),
88
+ expect.objectContaining({ muted: true }),
89
+ )
90
+ })
91
+
92
+ it('Should set current time on ready to given fallbackStartTime', () => {
93
+ render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', fallbackStartTime: 8 })
94
+
95
+ loadPlyr()
96
+ onready?.()
97
+
98
+ expect(player.currentTime).toBe(8)
29
99
  })
30
100
 
31
- it('Should render embed iframe with loop if given', () => {
32
- const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', loop: true })
101
+ it('Should use stored playtime for video id, minus 1 second', () => {
102
+ // @ts-ignore
103
+ window.PlayPilotLinkInjections = {
104
+ video_playtimes: { abc: 5 },
105
+ }
106
+
107
+ render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc' })
108
+
109
+ loadPlyr()
110
+ onready?.()
33
111
 
34
- expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&loop=true')
112
+ expect(player.currentTime).toBe(4)
35
113
  })
36
114
 
37
- it('Should render embed iframe with captions if given', () => {
115
+ it('Should enable captions in youtube options if given', () => {
38
116
  const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', captions: true })
117
+ loadPlyr()
39
118
 
40
- expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&captions=true')
119
+ expect(Plyr).toHaveBeenCalledWith(container.querySelector('.player'), expect.objectContaining({ youtube: expect.objectContaining({ cc_load_policy: 1 }) }))
41
120
  })
42
121
 
43
- it('Should render embed iframe with autoplay set to false if given', () => {
122
+ it('Should initialize Plyr with autoplay set to false if given', () => {
44
123
  const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', autoplay: false })
45
124
 
46
- expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&autoplay=false')
125
+ loadPlyr()
126
+
127
+ expect(Plyr).toHaveBeenCalledWith(container.querySelector('.player'), expect.objectContaining({ autoplay: false }))
47
128
  })
48
129
 
49
- it('Should start at given fallbackStartTime', () => {
50
- const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', fallbackStartTime: 8 })
130
+ it('Should replay and re-mute on loop when video ends', () => {
131
+ render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', loop: true, muted: true })
132
+
133
+ loadPlyr()
134
+ global.requestAnimationFrame = (callback) => (callback(0), 0)
51
135
 
52
- expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&start_time=8')
136
+ statechange?.({ detail: { code: 0 } })
137
+
138
+ expect(player.play).toHaveBeenCalled()
139
+ expect(player.muted).toBe(true)
53
140
  })
54
141
 
55
142
  it('Should render error message if embeddable_url is invalid', () => {
56
143
  const { container, getByText } = render(YouTubeEmbed, { embeddable_url: '-' })
57
144
 
58
- expect(container.querySelector('iframe')).not.toBeTruthy()
145
+ expect(container.querySelector('.player')).not.toBeTruthy()
59
146
  expect(getByText('Something went wrong')).toBeTruthy()
60
147
  })
61
-
62
- it('Should include start_time for given video id if present, minus 1 second', () => {
63
- // @ts-ignore
64
- window.PlayPilotLinkInjections = {
65
- video_playtimes: { abc: 5 },
66
- }
67
-
68
- const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', captions: true })
69
-
70
- expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&start_time=4')
71
- })
72
148
  })