@playpilot/tpi 5.15.0 → 5.16.2
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/dist/link-injections.js +11 -10
- package/package.json +2 -1
- package/release.js +3 -1
- package/src/lib/enums/SplitTest.ts +4 -0
- package/src/lib/fakeData.ts +70 -0
- package/src/lib/linkInjection.ts +11 -29
- package/src/lib/modal.ts +97 -0
- package/src/lib/playlink.ts +4 -1
- package/src/lib/types/participant.d.ts +14 -0
- package/src/lib/types/title.d.ts +3 -1
- package/src/routes/components/Description.svelte +1 -0
- package/src/routes/components/Icons/IconArrow.svelte +22 -0
- package/src/routes/components/Icons/IconClose.svelte +1 -1
- package/src/routes/components/Icons/IconIMDb.svelte +9 -1
- package/src/routes/components/ListTitle.svelte +204 -0
- package/src/routes/components/Modal.svelte +63 -13
- package/src/routes/components/Participant.svelte +92 -0
- package/src/routes/components/ParticipantModal.svelte +31 -0
- package/src/routes/components/PlaylinkIcon.svelte +41 -0
- package/src/routes/components/PlaylinkLabel.svelte +37 -0
- package/src/routes/components/Playlinks.svelte +1 -3
- package/src/routes/components/Rails/ParticipantsRail.svelte +56 -0
- package/src/routes/components/Rails/Rail.svelte +91 -0
- package/src/routes/components/Rails/SimilarRail.svelte +16 -0
- package/src/routes/components/Rails/TitlesRail.svelte +95 -0
- package/src/routes/components/Tabs.svelte +47 -0
- package/src/routes/components/Title.svelte +20 -17
- package/src/routes/components/TitleModal.svelte +3 -3
- package/src/routes/components/TitlePoster.svelte +30 -0
- package/src/tests/lib/linkInjection.test.js +10 -22
- package/src/tests/lib/modal.test.js +148 -0
- package/src/tests/lib/playlink.test.js +25 -10
- package/src/tests/routes/components/ListTitle.test.js +84 -0
- package/src/tests/routes/components/Modal.test.js +51 -19
- package/src/tests/routes/components/PlaylinkIcon.test.js +27 -0
- package/src/tests/routes/components/PlaylinkLabel.test.js +19 -0
- package/src/tests/routes/components/Rails/ParticipantsRail.test.js +41 -0
- package/src/tests/routes/components/Rails/TitleRail.test.js +38 -0
- package/src/tests/routes/components/Title.test.js +6 -0
- package/src/tests/routes/components/TitlePoster.test.js +20 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Option {
|
|
3
|
+
label: string
|
|
4
|
+
value: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
options: Option[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { options }: Props = $props()
|
|
12
|
+
|
|
13
|
+
let active = $state(options[0]?.value)
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="tabs">
|
|
17
|
+
{#each options as { label, value }}
|
|
18
|
+
<button class="tab" class:active={value === active} onclick={() => active = value}>{label}</button>
|
|
19
|
+
{/each}
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<style lang="scss">
|
|
23
|
+
.tabs {
|
|
24
|
+
display: flex;
|
|
25
|
+
gap: margin(0.5);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.tab {
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
appearance: none;
|
|
31
|
+
background: transparent;
|
|
32
|
+
padding: margin(0.25) margin(0.5);
|
|
33
|
+
border: 1px solid currentColor;
|
|
34
|
+
border-radius: var(--playpilot-tabs-border-radius, margin(0.25));
|
|
35
|
+
transition: opacity 100ms;
|
|
36
|
+
font-family: inherit;
|
|
37
|
+
font-weight: var(--playpilot-tabs-font-weight, 500);
|
|
38
|
+
color: var(--playpilot-tabs-text-color, var(--playpilot-detail-text-color, var(--playpilot-text-color-alt)));
|
|
39
|
+
font-size: inherit;
|
|
40
|
+
|
|
41
|
+
&.active,
|
|
42
|
+
&:hover {
|
|
43
|
+
background: var(--playpilot-tabs-background-active, var(--playpilot-content));
|
|
44
|
+
color: var(--playpilot-tabs-text-color-active, var(--playpilot-detail-text-color, var(--playpilot-text-color)));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
import Playlinks from './Playlinks.svelte'
|
|
4
4
|
import Description from './Description.svelte'
|
|
5
5
|
import IconIMDb from './Icons/IconIMDb.svelte'
|
|
6
|
+
import ParticipantsRail from './Rails/ParticipantsRail.svelte'
|
|
7
|
+
import SimilarRail from './Rails/SimilarRail.svelte'
|
|
8
|
+
import TitlePoster from './TitlePoster.svelte'
|
|
6
9
|
import { t } from '$lib/localization'
|
|
7
10
|
import type { TitleData } from '$lib/types/title'
|
|
8
11
|
import { heading } from '$lib/actions/heading'
|
|
9
12
|
import { removeImageUrlPrefix } from '$lib/image'
|
|
13
|
+
import { participants } from '$lib/fakeData'
|
|
10
14
|
|
|
11
15
|
interface Props {
|
|
12
16
|
title: TitleData
|
|
@@ -22,13 +26,8 @@
|
|
|
22
26
|
|
|
23
27
|
<div class="content" class:small data-playpilot-link-injections-title>
|
|
24
28
|
<div class="header">
|
|
25
|
-
<div class="
|
|
26
|
-
<
|
|
27
|
-
class="poster"
|
|
28
|
-
class:loaded={posterLoaded}
|
|
29
|
-
src={removeImageUrlPrefix(title.standing_poster)}
|
|
30
|
-
alt="Movie poster for '{title.title}'"
|
|
31
|
-
onload={() => posterLoaded = true} />
|
|
29
|
+
<div class="poster" class:loaded={posterLoaded}>
|
|
30
|
+
<TitlePoster {title} onload={() => posterLoaded = true} />
|
|
32
31
|
</div>
|
|
33
32
|
|
|
34
33
|
<div class="heading" use:heading={2} class:truncate={small} id="title">{title.title}</div>
|
|
@@ -53,9 +52,18 @@
|
|
|
53
52
|
<div class="main">
|
|
54
53
|
<Playlinks playlinks={title.providers} {title} />
|
|
55
54
|
|
|
56
|
-
{#if !small}
|
|
55
|
+
{#if !small && title.description}
|
|
57
56
|
<Description text={title.description} blurb={title.blurb} />
|
|
58
57
|
{/if}
|
|
58
|
+
|
|
59
|
+
<!-- Temporarily not available on production as there is not yet an API endpoint for either -->
|
|
60
|
+
{#if process.env.NODE_ENV !== 'production'}
|
|
61
|
+
{#if true || title.participants?.length}
|
|
62
|
+
<ParticipantsRail participants={participants} />
|
|
63
|
+
{/if}
|
|
64
|
+
|
|
65
|
+
<SimilarRail />
|
|
66
|
+
{/if}
|
|
59
67
|
</div>
|
|
60
68
|
</div>
|
|
61
69
|
|
|
@@ -103,14 +111,14 @@
|
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
&.small {
|
|
106
|
-
font-size: var(--playpilot-detail-font-size, 12px);
|
|
114
|
+
font-size: var(--playpilot-detail-font-size-small, 12px);
|
|
107
115
|
line-height: 1.45;
|
|
108
|
-
padding-bottom: margin(
|
|
116
|
+
padding-bottom: margin(1);
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
.header {
|
|
113
|
-
padding: margin(
|
|
121
|
+
padding: margin(3) 0 margin(1);
|
|
114
122
|
background: transparent;
|
|
115
123
|
|
|
116
124
|
.small & {
|
|
@@ -118,12 +126,6 @@
|
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
.top {
|
|
122
|
-
display: flex;
|
|
123
|
-
justify-content: space-between;
|
|
124
|
-
align-items: flex-end;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
129
|
.poster {
|
|
128
130
|
display: block;
|
|
129
131
|
width: margin(4.5);
|
|
@@ -132,6 +134,7 @@
|
|
|
132
134
|
border-radius: var(--playpilot-detail-image-border-radius, margin(0.5));
|
|
133
135
|
background: var(--playpilot-detail-image-background, var(--playpilot-content));
|
|
134
136
|
box-shadow: var(--playpilot-detail-image-shadow, var(--playpilot-shadow));
|
|
137
|
+
overflow: hidden;
|
|
135
138
|
opacity: 0;
|
|
136
139
|
|
|
137
140
|
&.loaded {
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
import { getFirstAdOfType } from '$lib/ads'
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
|
-
onclose: () => void,
|
|
14
13
|
title: TitleData
|
|
14
|
+
initialScrollPosition?: number
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const {
|
|
17
|
+
const { title, initialScrollPosition = 0 }: Props = $props()
|
|
18
18
|
|
|
19
19
|
const topScrollAd = getFirstAdOfType('top_scroll')
|
|
20
20
|
const displayAd = getFirstAdOfType('card')
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
{/if}
|
|
50
50
|
{/snippet}
|
|
51
51
|
|
|
52
|
-
<Modal {
|
|
52
|
+
<Modal {onscroll} {initialScrollPosition} prepend={displayAd ? prepend : null} bubble={topScrollAd ? bubble : null} tall>
|
|
53
53
|
<Title {title} />
|
|
54
54
|
</Modal>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { imagePlaceholderDataUrl } from '$lib/constants'
|
|
3
|
+
import { removeImageUrlPrefix } from '$lib/image'
|
|
4
|
+
import type { TitleData } from '$lib/types/title'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
title: TitleData
|
|
8
|
+
width?: number
|
|
9
|
+
height?: number
|
|
10
|
+
onload?: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { title, width = 96, height = 144, onload = () => null }: Props = $props()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<img
|
|
17
|
+
src={removeImageUrlPrefix(title.standing_poster)}
|
|
18
|
+
alt="Movie poster for '{title.title}'"
|
|
19
|
+
{width}
|
|
20
|
+
{height}
|
|
21
|
+
onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl}
|
|
22
|
+
{onload} />
|
|
23
|
+
|
|
24
|
+
<style lang="scss">
|
|
25
|
+
img {
|
|
26
|
+
display: block;
|
|
27
|
+
width: 100%;
|
|
28
|
+
height: auto;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
|
4
4
|
import { injectLinksInDocument, clearLinkInjections, clearLinkInjection, getLinkInjectionElements, insertAfterArticlePlaylinks, getLinkInjectionsParentElement, isAvailableAsManualInjection, filterRemovedAndInactiveInjections, isEquivalentInjection, filterInvalidInTextInjections, filterInvalidAfterArticleInjections, isValidInjection, isValidPlaylinkType, removePlayPilotTitleLinks, trackLinkIntersection } from '$lib/linkInjection'
|
|
5
5
|
import { mount, unmount } from 'svelte'
|
|
6
6
|
import { fakeFetch, generateInjection } from '../helpers'
|
|
7
|
+
import { openModal } from '$lib/modal'
|
|
7
8
|
import { track } from '$lib/tracking'
|
|
8
9
|
|
|
9
10
|
vi.mock('svelte', () => ({
|
|
@@ -11,6 +12,13 @@ vi.mock('svelte', () => ({
|
|
|
11
12
|
unmount: vi.fn(),
|
|
12
13
|
}))
|
|
13
14
|
|
|
15
|
+
vi.mock('$lib/modal', () => ({
|
|
16
|
+
openModal: vi.fn(),
|
|
17
|
+
destroyAllModals: vi.fn(),
|
|
18
|
+
getPreviousModal: vi.fn(),
|
|
19
|
+
goBackToPreviousModal: vi.fn(),
|
|
20
|
+
}))
|
|
21
|
+
|
|
14
22
|
vi.mock('$lib/tracking', () => ({
|
|
15
23
|
track: vi.fn(),
|
|
16
24
|
}))
|
|
@@ -311,7 +319,7 @@ describe('linkInjection.js', () => {
|
|
|
311
319
|
expect(() => injectLinksInDocument(elements, { aiInjections: linkInjections, manualInjections: [] })).not.toThrow()
|
|
312
320
|
})
|
|
313
321
|
|
|
314
|
-
it('Should
|
|
322
|
+
it('Should open modal when link is clicked', async () => {
|
|
315
323
|
document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
|
|
316
324
|
|
|
317
325
|
const elements = Array.from(document.body.querySelectorAll('p'))
|
|
@@ -322,27 +330,7 @@ describe('linkInjection.js', () => {
|
|
|
322
330
|
const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
|
|
323
331
|
await fireEvent.click(link)
|
|
324
332
|
|
|
325
|
-
expect(
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
it('Should not mount modal multiple times if modal is already open', async () => {
|
|
329
|
-
const sentence = 'This is a sentence with an injection.'
|
|
330
|
-
document.body.innerHTML = `<p>${sentence}</p>`
|
|
331
|
-
|
|
332
|
-
const elements = Array.from(document.body.querySelectorAll('p'))
|
|
333
|
-
|
|
334
|
-
const linkInjections = [
|
|
335
|
-
generateInjection(sentence, 'a sentence'),
|
|
336
|
-
generateInjection(sentence, 'an injection'),
|
|
337
|
-
]
|
|
338
|
-
|
|
339
|
-
injectLinksInDocument(elements, { aiInjections: linkInjections, manualInjections: [] })
|
|
340
|
-
|
|
341
|
-
const links = document.querySelectorAll('a')
|
|
342
|
-
await fireEvent.click(links[0])
|
|
343
|
-
await fireEvent.click(links[1])
|
|
344
|
-
|
|
345
|
-
expect(mount).toHaveBeenCalledTimes(1)
|
|
333
|
+
expect(openModal).toHaveBeenCalled()
|
|
346
334
|
})
|
|
347
335
|
|
|
348
336
|
it('Should not fire given onclick function when clicked with modifier keys or not left click', async () => {
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
|
|
8
|
+
vi.mock('svelte', () => ({
|
|
9
|
+
mount: vi.fn(),
|
|
10
|
+
unmount: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
const modal = {
|
|
14
|
+
injection: linkInjections[0],
|
|
15
|
+
data: title,
|
|
16
|
+
type: 'title',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('modal.js', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
destroyAllModals()
|
|
22
|
+
vi.resetAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('openModal', () => {
|
|
26
|
+
it('Should mount modal', () => {
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
openModal(modal)
|
|
29
|
+
|
|
30
|
+
expect(mount).toHaveBeenCalledWith(TitleModal, expect.any(Object))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('Should mount modal for participants', () => {
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
openModal({ ...modal, type: 'participant' })
|
|
36
|
+
|
|
37
|
+
expect(mount).toHaveBeenCalledWith(ParticipantModal, expect.any(Object))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('Should not mount modal if modifier key was present in event', () => {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
openModal({ ...modal, event: { ctrlKey: true } })
|
|
43
|
+
|
|
44
|
+
expect(mount).not.toHaveBeenCalled()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('closeCurrentModal', () => {
|
|
49
|
+
it('Should unmount modal without removing it from list', () => {
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
openModal(modal)
|
|
52
|
+
closeCurrentModal()
|
|
53
|
+
|
|
54
|
+
expect(unmount).toHaveBeenCalled()
|
|
55
|
+
expect(getAllModals()).toHaveLength(1)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('destroyCurrentModal', () => {
|
|
60
|
+
it('Should unmount modal and remove lastest entry from list', () => {
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
openModal(modal)
|
|
63
|
+
destroyCurrentModal()
|
|
64
|
+
|
|
65
|
+
expect(unmount).toHaveBeenCalled()
|
|
66
|
+
expect(getAllModals()).toHaveLength(0)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('destroyAllModals', () => {
|
|
71
|
+
it('Should unmount current modal and remove all modals from list', () => {
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
openModal(modal)
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
openModal(modal)
|
|
76
|
+
// @ts-ignore
|
|
77
|
+
openModal(modal)
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
openModal(modal)
|
|
80
|
+
|
|
81
|
+
destroyAllModals()
|
|
82
|
+
|
|
83
|
+
expect(unmount).toHaveBeenCalled()
|
|
84
|
+
expect(getAllModals()).toHaveLength(0)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('getPreviousModal', () => {
|
|
89
|
+
it('Should return the modal before the last', () => {
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
openModal({ ...modal, type: 'participant' })
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
openModal({ ...modal, type: 'title' })
|
|
94
|
+
|
|
95
|
+
// @ts-ignore
|
|
96
|
+
expect(getPreviousModal().type).toBe('participant')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('Should return null if there is no previous modal', () => {
|
|
100
|
+
// @ts-ignore
|
|
101
|
+
openModal({ ...modal, type: 'participant' })
|
|
102
|
+
|
|
103
|
+
// @ts-ignore
|
|
104
|
+
expect(getPreviousModal()).toBeNull()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('Should return null if there are no modals at all', () => {
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
expect(getPreviousModal()).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('goBackToPreviousModal', () => {
|
|
114
|
+
it('Should destroy the current modal and mount the previous modal', () => {
|
|
115
|
+
// @ts-ignore
|
|
116
|
+
openModal({ ...modal, type: 'participant', data: { ...title, title: 'Previous modal' } })
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
openModal({ ...modal, type: 'title' })
|
|
119
|
+
|
|
120
|
+
vi.resetAllMocks()
|
|
121
|
+
|
|
122
|
+
goBackToPreviousModal()
|
|
123
|
+
|
|
124
|
+
expect(unmount).toHaveBeenCalled()
|
|
125
|
+
expect(mount).toHaveBeenCalledWith(
|
|
126
|
+
ParticipantModal,
|
|
127
|
+
expect.objectContaining({
|
|
128
|
+
props: expect.objectContaining({
|
|
129
|
+
participant: expect.objectContaining({ title: 'Previous modal' }),
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('getAllModals', () => {
|
|
137
|
+
it('Should return all current modals', () => {
|
|
138
|
+
// @ts-ignore
|
|
139
|
+
openModal(modal)
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
openModal(modal)
|
|
142
|
+
// @ts-ignore
|
|
143
|
+
openModal(modal)
|
|
144
|
+
|
|
145
|
+
expect(getAllModals()).toHaveLength(3)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -4,12 +4,12 @@ import { mergePlaylinks } from '$lib/playlink'
|
|
|
4
4
|
describe('$lib/playlink', () => {
|
|
5
5
|
describe('mergePlaylinks', () => {
|
|
6
6
|
it('Should return an array with RENT and BUY categories of the same provider as one entry', () => {
|
|
7
|
-
/** @type {PlaylinkData[]} */
|
|
7
|
+
/** @type {import('$lib/types/playlink').PlaylinkData[]} */
|
|
8
8
|
const playlinks = [{
|
|
9
9
|
sid: '1',
|
|
10
10
|
name: 'Netflix',
|
|
11
11
|
url: '',
|
|
12
|
-
logo_url: '',
|
|
12
|
+
logo_url: 'a',
|
|
13
13
|
extra_info: {
|
|
14
14
|
category: 'RENT',
|
|
15
15
|
},
|
|
@@ -17,7 +17,7 @@ describe('$lib/playlink', () => {
|
|
|
17
17
|
sid: '2',
|
|
18
18
|
name: 'Netflix',
|
|
19
19
|
url: '',
|
|
20
|
-
logo_url: '',
|
|
20
|
+
logo_url: 'a',
|
|
21
21
|
extra_info: {
|
|
22
22
|
category: 'BUY',
|
|
23
23
|
},
|
|
@@ -28,12 +28,12 @@ describe('$lib/playlink', () => {
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
it('Should not merge playlinks if they do not share the same name', () => {
|
|
31
|
-
/** @type {PlaylinkData[]} */
|
|
31
|
+
/** @type {import('$lib/types/playlink').PlaylinkData[]} */
|
|
32
32
|
const playlinks = [{
|
|
33
33
|
sid: '1',
|
|
34
34
|
name: 'Netflix',
|
|
35
35
|
url: '',
|
|
36
|
-
logo_url: '',
|
|
36
|
+
logo_url: 'a',
|
|
37
37
|
extra_info: {
|
|
38
38
|
category: 'RENT',
|
|
39
39
|
},
|
|
@@ -41,7 +41,7 @@ describe('$lib/playlink', () => {
|
|
|
41
41
|
sid: '2',
|
|
42
42
|
name: 'Apple',
|
|
43
43
|
url: '',
|
|
44
|
-
logo_url: '',
|
|
44
|
+
logo_url: 'a',
|
|
45
45
|
extra_info: {
|
|
46
46
|
category: 'BUY',
|
|
47
47
|
},
|
|
@@ -51,12 +51,12 @@ describe('$lib/playlink', () => {
|
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
it('Should not merge playlinks if one of the categories is not RENT or BUY', () => {
|
|
54
|
-
/** @type {PlaylinkData[]} */
|
|
54
|
+
/** @type {import('$lib/types/playlink').PlaylinkData[]} */
|
|
55
55
|
const playlinks = [{
|
|
56
56
|
sid: '1',
|
|
57
57
|
name: 'Netflix',
|
|
58
58
|
url: '',
|
|
59
|
-
logo_url: '',
|
|
59
|
+
logo_url: 'a',
|
|
60
60
|
extra_info: {
|
|
61
61
|
category: 'RENT',
|
|
62
62
|
},
|
|
@@ -64,7 +64,7 @@ describe('$lib/playlink', () => {
|
|
|
64
64
|
sid: '2',
|
|
65
65
|
name: 'Netflix',
|
|
66
66
|
url: '',
|
|
67
|
-
logo_url: '',
|
|
67
|
+
logo_url: 'a',
|
|
68
68
|
extra_info: {
|
|
69
69
|
category: 'SVOD',
|
|
70
70
|
},
|
|
@@ -73,8 +73,23 @@ describe('$lib/playlink', () => {
|
|
|
73
73
|
expect(mergePlaylinks(playlinks)).toHaveLength(2)
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
+
it('Should filter out playlinks without logos', () => {
|
|
77
|
+
/** @type {import('$lib/types/playlink').PlaylinkData[]} */
|
|
78
|
+
const playlinks = [{
|
|
79
|
+
sid: '1',
|
|
80
|
+
name: 'Netflix',
|
|
81
|
+
url: '',
|
|
82
|
+
logo_url: '',
|
|
83
|
+
extra_info: {
|
|
84
|
+
category: 'RENT',
|
|
85
|
+
},
|
|
86
|
+
}]
|
|
87
|
+
|
|
88
|
+
expect(mergePlaylinks(playlinks)).toHaveLength(0)
|
|
89
|
+
})
|
|
90
|
+
|
|
76
91
|
it('Should return an empty array if given', () => {
|
|
77
|
-
/** @type {PlaylinkData[]} */
|
|
92
|
+
/** @type {import('$lib/types/playlink').PlaylinkData[]} */
|
|
78
93
|
const playlinks = []
|
|
79
94
|
expect(mergePlaylinks(playlinks)).toEqual([])
|
|
80
95
|
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import ListTitle from '../../../routes/components/ListTitle.svelte'
|
|
5
|
+
import { title } from '$lib/fakeData'
|
|
6
|
+
|
|
7
|
+
vi.mock('$lib/tracking', () => ({
|
|
8
|
+
track: vi.fn(),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
vi.mock('svelte', async (importActual) => ({
|
|
12
|
+
...(await importActual()),
|
|
13
|
+
getContext: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('ListTitle.svelte', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('Should render the given image', () => {
|
|
22
|
+
const { getByAltText } = render(ListTitle, { title })
|
|
23
|
+
|
|
24
|
+
expect(getByAltText(`Movie poster for '${title.title}'`)).toBeTruthy()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('Should render the given info', () => {
|
|
28
|
+
const { getByText } = render(ListTitle, { title })
|
|
29
|
+
|
|
30
|
+
expect(getByText(title.title)).toBeTruthy()
|
|
31
|
+
expect(getByText(title.imdb_score)).toBeTruthy()
|
|
32
|
+
expect(getByText(title.year)).toBeTruthy()
|
|
33
|
+
expect(getByText(title.description)).toBeTruthy()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('Should not have small class by default', () => {
|
|
37
|
+
const { container } = render(ListTitle, { title })
|
|
38
|
+
|
|
39
|
+
expect(container.querySelector('.small')).not.toBeTruthy()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('Should fire the given onclick function when clicked', async () => {
|
|
43
|
+
const onclick = vi.fn()
|
|
44
|
+
const { getByText } = render(ListTitle, { title, onclick })
|
|
45
|
+
|
|
46
|
+
await fireEvent.click(getByText(title.title))
|
|
47
|
+
|
|
48
|
+
expect(onclick).toHaveBeenCalled()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('Playlinks', () => {
|
|
52
|
+
const playlinks = [
|
|
53
|
+
{ name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
|
|
54
|
+
{ name: 'Some other playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
it('Should render playlinks', () => {
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
const { queryByAltText, queryByText } = render(ListTitle, { title: { ...title, description: 'Some description', providers: playlinks } })
|
|
60
|
+
|
|
61
|
+
expect(queryByAltText('Some playlink') || queryByText('Some playlink')).toBeTruthy()
|
|
62
|
+
expect(queryByAltText('Some other playlink') || queryByText('Some other playlink')).toBeTruthy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('Should limit the number of shown playlinks', () => {
|
|
66
|
+
// @ts-ignore
|
|
67
|
+
const { container, getByText } = render(ListTitle, { title: { ...title, description: 'Some description', providers: [...playlinks, ...playlinks] } })
|
|
68
|
+
|
|
69
|
+
expect(container.querySelectorAll('.playlink')).toHaveLength(3)
|
|
70
|
+
expect(getByText('+1')).toBeTruthy()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('Should not fire the given onclick function when playlink is clicked', async () => {
|
|
74
|
+
const onclick = vi.fn()
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
const { container } = render(ListTitle, { title: { ...title, providers: playlinks }, onclick })
|
|
77
|
+
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
await fireEvent.click(container.querySelector('.playlink'))
|
|
80
|
+
|
|
81
|
+
expect(onclick).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -1,57 +1,63 @@
|
|
|
1
1
|
import { render, fireEvent } from '@testing-library/svelte'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import Modal from '../../../routes/components/Modal.svelte'
|
|
5
5
|
import { createRawSnippet } from 'svelte'
|
|
6
|
+
import { destroyAllModals, getPreviousModal, goBackToPreviousModal } from '$lib/modal'
|
|
7
|
+
|
|
8
|
+
vi.mock('$lib/modal', () => ({
|
|
9
|
+
destroyAllModals: vi.fn(),
|
|
10
|
+
getPreviousModal: vi.fn(),
|
|
11
|
+
goBackToPreviousModal: vi.fn(),
|
|
12
|
+
}))
|
|
6
13
|
|
|
7
14
|
describe('Modal.svelte', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.resetAllMocks()
|
|
17
|
+
})
|
|
18
|
+
|
|
8
19
|
const children = createRawSnippet(() => ({ render: () => '<div></div>' }))
|
|
9
20
|
|
|
10
|
-
it('Should fire given
|
|
11
|
-
const
|
|
12
|
-
const { container } = render(Modal, { children, onclose })
|
|
21
|
+
it('Should fire given destroyAllModals function when backdrop is clicked', async () => {
|
|
22
|
+
const { container } = render(Modal, { children })
|
|
13
23
|
|
|
14
24
|
await fireEvent.click(/** @type {Node} */ (container.querySelector('.backdrop')))
|
|
15
25
|
|
|
16
|
-
expect(
|
|
26
|
+
expect(destroyAllModals).toHaveBeenCalled()
|
|
17
27
|
})
|
|
18
28
|
|
|
19
|
-
it('Should fire
|
|
20
|
-
const
|
|
21
|
-
const { container } = render(Modal, { children, onclose })
|
|
29
|
+
it('Should fire destroyAllModals function when close button is clicked', async () => {
|
|
30
|
+
const { container } = render(Modal, { children })
|
|
22
31
|
|
|
23
32
|
await fireEvent.click(/** @type {Node} */ (container.querySelector('button')))
|
|
24
33
|
|
|
25
|
-
expect(
|
|
34
|
+
expect(destroyAllModals).toHaveBeenCalled()
|
|
26
35
|
})
|
|
27
36
|
|
|
28
37
|
it('Should not fire given onclose function when dialog is clicked', async () => {
|
|
29
|
-
const
|
|
30
|
-
const { container } = render(Modal, { children, onclose })
|
|
38
|
+
const { container } = render(Modal, { children })
|
|
31
39
|
|
|
32
40
|
await fireEvent.click(/** @type {Node} */ (container.querySelector('.dialog')))
|
|
33
41
|
|
|
34
|
-
expect(
|
|
42
|
+
expect(destroyAllModals).not.toHaveBeenCalled()
|
|
35
43
|
})
|
|
36
44
|
|
|
37
|
-
it('Should fire given
|
|
38
|
-
|
|
39
|
-
render(Modal, { children, onclose })
|
|
45
|
+
it('Should fire given destroyAllModals function when escape key is pressed', async () => {
|
|
46
|
+
render(Modal, { children })
|
|
40
47
|
|
|
41
48
|
fireEvent.keyDown(window, { key: 'Escape' })
|
|
42
49
|
|
|
43
|
-
expect(
|
|
50
|
+
expect(destroyAllModals).toHaveBeenCalled()
|
|
44
51
|
})
|
|
45
52
|
|
|
46
53
|
it('Should not fire given onclose function when key other than escape key is pressed', async () => {
|
|
47
|
-
|
|
48
|
-
render(Modal, { children, onclose })
|
|
54
|
+
render(Modal, { children })
|
|
49
55
|
|
|
50
56
|
fireEvent.keyDown(window, { key: 'a' })
|
|
51
57
|
fireEvent.keyDown(window, { key: 'Enter' })
|
|
52
58
|
fireEvent.keyDown(window, { key: 'Space' })
|
|
53
59
|
|
|
54
|
-
expect(
|
|
60
|
+
expect(destroyAllModals).not.toHaveBeenCalled()
|
|
55
61
|
})
|
|
56
62
|
|
|
57
63
|
it('Should fire given onscroll function when dialog is scrolled', async () => {
|
|
@@ -96,4 +102,30 @@ describe('Modal.svelte', () => {
|
|
|
96
102
|
|
|
97
103
|
expect(getByRole('dialog').classList).toContain('tall')
|
|
98
104
|
})
|
|
105
|
+
|
|
106
|
+
it('Should not show back button by default', () => {
|
|
107
|
+
const { queryByLabelText } = render(Modal, { children })
|
|
108
|
+
|
|
109
|
+
expect(queryByLabelText('Back')).not.toBeTruthy()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('Should show back button if previous modal was opened', () => {
|
|
113
|
+
// @ts-ignore
|
|
114
|
+
vi.mocked(getPreviousModal).mockReturnValueOnce(true)
|
|
115
|
+
|
|
116
|
+
const { getByLabelText } = render(Modal, { children })
|
|
117
|
+
|
|
118
|
+
expect(getByLabelText('Back')).toBeTruthy()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('Should fire goBackToPreviousModal when back button is clicked', async () => {
|
|
122
|
+
// @ts-ignore
|
|
123
|
+
vi.mocked(getPreviousModal).mockReturnValueOnce(true)
|
|
124
|
+
|
|
125
|
+
const { getByLabelText } = render(Modal, { children })
|
|
126
|
+
|
|
127
|
+
await fireEvent.click(getByLabelText('Back'))
|
|
128
|
+
|
|
129
|
+
expect(goBackToPreviousModal).toHaveBeenCalled()
|
|
130
|
+
})
|
|
99
131
|
})
|