@playpilot/tpi 5.34.3 → 5.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.34.3",
3
+ "version": "5.35.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -126,6 +126,11 @@ export const translations = {
126
126
  [Language.Swedish]: 'Liknande filmer och serier',
127
127
  [Language.Danish]: 'Lignende film og serier',
128
128
  },
129
+ 'Watch Trailer': {
130
+ [Language.English]: 'Watch trailer',
131
+ [Language.Swedish]: 'Se trailer',
132
+ [Language.Danish]: 'Se trailer',
133
+ },
129
134
 
130
135
  // Genres
131
136
  'All': {
@@ -53,6 +53,7 @@ export const TrackingEvent = Object.freeze({
53
53
  SplitTestView: 'ali_split_test_view',
54
54
  SplitTestAction: 'ali_split_test_action',
55
55
 
56
- // Share
57
- ShareTitle: 'ali_share_title'
56
+ // Various
57
+ ShareTitle: 'ali_share_title',
58
+ TrailerClick: 'ali_trailer_button_click'
58
59
  })
@@ -38,6 +38,7 @@ export const title: TitleData = {
38
38
  standing_poster: 'https://img.playpilot.tech/6239ee86a58f11efb0b50a58a9feac02/src/img?optimizer=image&class=2by3x18',
39
39
  title: 'Dune: Prophecy',
40
40
  original_title: 'Dune: Prophecy',
41
+ embeddable_url: null,
41
42
  }
42
43
 
43
44
  export const linkInjections: LinkInjection[] = [{
@@ -0,0 +1,21 @@
1
+ import { mount, unmount } from "svelte"
2
+ import { getPlayPilotWrapperElement } from "./injection"
3
+ import type { TitleData } from "./types/title"
4
+ import YouTubeEmbedOverlay from "../routes/components/YouTubeEmbedOverlay.svelte"
5
+
6
+ let currentTrailerComponent: object | null = {}
7
+
8
+ export function openTrailerOverlay(title: TitleData) {
9
+ const target = getPlayPilotWrapperElement()
10
+ const props = { onclose: closeTrailerOverlay, embeddable_url: title.embeddable_url || '' }
11
+
12
+ currentTrailerComponent = mount(YouTubeEmbedOverlay, { target, props })
13
+ }
14
+
15
+ export function closeTrailerOverlay(): void {
16
+ if (!currentTrailerComponent) return
17
+
18
+ unmount(currentTrailerComponent, { outro: true })
19
+
20
+ currentTrailerComponent = null
21
+ }
@@ -17,6 +17,7 @@ export type TitleData = {
17
17
  standing_poster: string
18
18
  title: string
19
19
  original_title: string
20
+ embeddable_url: string | null
20
21
  length?: number
21
22
  blurb?: string
22
23
  participants?: ParticipantData[]
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+
4
+ interface Props {
5
+ variant?: 'filled' | 'border'
6
+ // eslint-disable-next-line no-unused-vars
7
+ onclick?: (event: MouseEvent) => void
8
+ children?: Snippet
9
+ }
10
+
11
+ const { variant = 'filled', onclick, children }: Props = $props()
12
+ </script>
13
+
14
+ <button class="button {variant}" {onclick}>
15
+ {@render children?.()}
16
+ </button>
17
+
18
+ <style lang="scss">
19
+ .button {
20
+ appearance: none;
21
+ display: flex;
22
+ height: 100%;
23
+ align-items: center;
24
+ gap: margin(0.25);
25
+ border: 0;
26
+ padding: 0.25em 0.5em;
27
+ background: transparent;
28
+ border-radius: theme(button-border-radius, border-radius);
29
+ color: theme(button-text-color, text-color-alt);
30
+ font-size: inherit;
31
+ font-family: inherit;
32
+ font-weight: theme(button-font-weight, normal);
33
+ cursor: pointer;
34
+
35
+ :global(svg) {
36
+ width: 1.5em;
37
+ height: 1.5em;
38
+ opacity: 0.75;
39
+ }
40
+ }
41
+
42
+ .filled {
43
+ background: theme(button-filled-background, content);
44
+
45
+ &:hover,
46
+ &:active {
47
+ background: theme(button-filled-hover-background, content-light);
48
+ color: theme(button-filled-hover-text-color, text-color);
49
+ }
50
+ }
51
+
52
+ .border {
53
+ box-shadow: inset 0 0 0 1px theme(button-border-color, content-light);
54
+
55
+ &:hover,
56
+ &:active {
57
+ background: theme(button-border-color, content);
58
+ color: theme(button-hover-text-color, text-color);
59
+ }
60
+ }
61
+ </style>
62
+
@@ -1,3 +1,11 @@
1
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
1
+ <script lang="ts">
2
+ interface Props {
3
+ size?: number
4
+ }
5
+
6
+ const { size = 16 }: Props = $props()
7
+ </script>
8
+
9
+ <svg width={size} height={size} viewBox="0 0 16 16" fill="none">
2
10
  <path d="M14 2L2 14M2 2L14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
3
11
  </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="24px" height="24px" viewBox="0 -960 960 960">
2
+ <path fill="currentColor" d="m380-300 280-180-280-180v360ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
3
+ </svg>
@@ -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,27 @@
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
+ {#if title.embeddable_url}
58
+ <Trailer title={title} />
57
59
  <Share title={title.title} url={titleUrl(title)} />
58
- </div>
60
+ {/if}
59
61
  </div>
60
62
  </div>
61
63
 
@@ -146,11 +148,6 @@
146
148
  }
147
149
  }
148
150
 
149
- .row {
150
- display: flex;
151
- align-items: flex-start;
152
- }
153
-
154
151
  .info {
155
152
  display: flex;
156
153
  flex-wrap: wrap;
@@ -182,8 +179,10 @@
182
179
  }
183
180
  }
184
181
 
185
- .action {
186
- margin: margin(-0.125) 0 0 auto;
182
+ .actions {
183
+ display: flex;
184
+ gap: margin(0.5);
185
+ margin-top: margin(0.5);
187
186
  }
188
187
 
189
188
  .background {
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
+ import { t } from '$lib/localization'
4
+ import { track } from '$lib/tracking'
5
+ import { openTrailerOverlay } from '$lib/trailer'
6
+ import type { TitleData } from '$lib/types/title'
7
+ import Button from './Button.svelte'
8
+ import IconPlay from './Icons/IconPlay.svelte'
9
+
10
+ interface Props {
11
+ title: TitleData
12
+ }
13
+
14
+ const { title }: Props = $props()
15
+
16
+ function onclick() {
17
+ openTrailerOverlay(title)
18
+ track(TrackingEvent.TrailerClick, title)
19
+ }
20
+ </script>
21
+
22
+ <Button {onclick}>
23
+ <IconPlay />
24
+ {t('Watch Trailer')}
25
+ </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 different 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>
@@ -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
+ })
@@ -16,57 +16,57 @@ vi.mock('$lib/tracking', () => ({
16
16
 
17
17
  describe('Share.svelte', () => {
18
18
  it('Should open context menu on click', async () => {
19
- const { getByLabelText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
19
+ const { getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
20
20
 
21
21
  expect(queryByText('Copy URL')).not.toBeTruthy()
22
22
  expect(queryByText('Email')).not.toBeTruthy()
23
23
 
24
- await fireEvent.click(getByLabelText('Share'))
24
+ await fireEvent.click(getByText('Share'))
25
25
 
26
26
  expect(queryByText('Copy URL')).toBeTruthy()
27
27
  expect(queryByText('Email')).toBeTruthy()
28
28
  })
29
29
 
30
30
  it('Should close context menu on click of items', async () => {
31
- const { getByLabelText, getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
31
+ const { getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
32
32
 
33
- await fireEvent.click(getByLabelText('Share'))
33
+ await fireEvent.click(getByText('Share'))
34
34
  await fireEvent.click(getByText('Copy URL'))
35
35
 
36
36
  expect(queryByText('Copy URL')).not.toBeTruthy()
37
37
  })
38
38
 
39
39
  it('Should close context menu on click of body', async () => {
40
- const { getByLabelText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
40
+ const { getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
41
41
 
42
- await fireEvent.click(getByLabelText('Share'))
42
+ await fireEvent.click(getByText('Share'))
43
43
  await fireEvent.click(document.body)
44
44
 
45
45
  expect(queryByText('Copy URL')).not.toBeTruthy()
46
46
  })
47
47
 
48
48
  it('Should fire copyToClipboard on click of button', async () => {
49
- const { getByLabelText, getByText } = render(Share, { title: 'Some title', url: 'some-url' })
49
+ const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
50
50
 
51
- await fireEvent.click(getByLabelText('Share'))
51
+ await fireEvent.click(getByText('Share'))
52
52
  await fireEvent.click(getByText('Copy URL'))
53
53
 
54
54
  expect(copyToClipboard).toHaveBeenCalledWith('some-url?utm_source=tpi')
55
55
  })
56
56
 
57
57
  it('Should fire track function on click of copy URL button', async () => {
58
- const { getByLabelText, getByText } = render(Share, { title: 'Some title', url: 'some-url' })
58
+ const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
59
59
 
60
- await fireEvent.click(getByLabelText('Share'))
60
+ await fireEvent.click(getByText('Share'))
61
61
  await fireEvent.click(getByText('Copy URL'))
62
62
 
63
63
  expect(track).toHaveBeenCalledWith(TrackingEvent.ShareTitle, null, { title: 'Some title', url: 'http://localhost:3000/', method: 'copy' })
64
64
  })
65
65
 
66
66
  it('Should fire track function on click of email button', async () => {
67
- const { getByLabelText, getByText } = render(Share, { title: 'Some title', url: 'some-url' })
67
+ const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
68
68
 
69
- await fireEvent.click(getByLabelText('Share'))
69
+ await fireEvent.click(getByText('Share'))
70
70
  await fireEvent.click(getByText('Email'))
71
71
 
72
72
  expect(track).toHaveBeenCalledWith(TrackingEvent.ShareTitle, null, { title: 'Some title', url: 'http://localhost:3000/', method: 'email' })
@@ -91,4 +91,16 @@ describe('Title.svelte', () => {
91
91
  expect(fetchParticipantsForTitle).toHaveBeenCalled()
92
92
  expect(fetchSimilarTitles).toHaveBeenCalled()
93
93
  })
94
+
95
+ it('Should show trailer button when embeddable_url is given', () => {
96
+ const { getByText } = render(Title, { title: { ...title, embeddable_url: 'some-url' } })
97
+
98
+ expect(getByText('Watch trailer')).toBeTruthy()
99
+ })
100
+
101
+ it('Should not show trailer button when embeddable_url is not given', () => {
102
+ const { queryByText } = render(Title, { title })
103
+
104
+ expect(queryByText('Watch Trailer')).not.toBeTruthy()
105
+ })
94
106
  })
@@ -0,0 +1,34 @@
1
+ import { render, fireEvent } from '@testing-library/svelte'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import Trailer from '../../../routes/components/Trailer.svelte'
5
+ import { title } from '$lib/fakeData'
6
+ import { openTrailerOverlay } from '$lib/trailer'
7
+ import { track } from '$lib/tracking'
8
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
+
10
+ vi.mock('$lib/trailer', () => ({
11
+ openTrailerOverlay: vi.fn(),
12
+ }))
13
+
14
+ vi.mock('$lib/tracking', () => ({
15
+ track: vi.fn(),
16
+ }))
17
+
18
+ describe('Trailer.svelte', () => {
19
+ it('Should fire openTrailerOverlay with given title on click', async () => {
20
+ const { getByRole } = render(Trailer, { title })
21
+
22
+ await fireEvent.click(getByRole('button'))
23
+
24
+ expect(openTrailerOverlay).toHaveBeenCalledWith(title)
25
+ })
26
+
27
+ it('Should fire tracking event on click', async () => {
28
+ const { getByRole } = render(Trailer, { title })
29
+
30
+ await fireEvent.click(getByRole('button'))
31
+
32
+ expect(track).toHaveBeenCalledWith(TrackingEvent.TrailerClick, title)
33
+ })
34
+ })
@@ -0,0 +1,31 @@
1
+ import { fireEvent, render } from '@testing-library/svelte'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import YouTubeEmbedOverlay from '../../../routes/components/YouTubeEmbedOverlay.svelte'
5
+
6
+ describe('YouTubeEmbedOverlay.svelte', () => {
7
+ it('Should render embed iframe with given video url', () => {
8
+ const { container } = render(YouTubeEmbedOverlay, { embeddable_url: 'youtube.com/watch?v=abc', onclose: () => null })
9
+
10
+ // @ts-ignore
11
+ expect(container.querySelector('iframe').src).toBe('https://www.youtube.com/embed/abc?autoplay=true')
12
+ })
13
+
14
+ it('Should render error message if embeddable_url is invalid', () => {
15
+ const { container, getByText } = render(YouTubeEmbedOverlay, { embeddable_url: '-', onclose: () => null })
16
+
17
+ expect(container.querySelector('iframe')).not.toBeTruthy()
18
+ expect(getByText('Something went wrong')).toBeTruthy()
19
+ })
20
+
21
+ it('Should fire given onclose function on click of close button and backdrop', async () => {
22
+ const onclose = vi.fn()
23
+ const { getByLabelText, getByTestId } = render(YouTubeEmbedOverlay, { embeddable_url: '-', onclose })
24
+
25
+ await fireEvent.click(getByLabelText('Close'))
26
+ expect(onclose).toHaveBeenCalledTimes(1)
27
+
28
+ await fireEvent.click(getByTestId('backdrop'))
29
+ expect(onclose).toHaveBeenCalledTimes(2)
30
+ })
31
+ })