@playpilot/tpi 5.32.0-beta.3 → 5.32.0-beta.youtube.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.
- package/dist/link-injections.js +11 -11
- package/package.json +1 -1
- package/src/lib/data/translations.ts +5 -5
- package/src/lib/enums/SplitTest.ts +0 -5
- package/src/lib/fakeData.ts +1 -0
- package/src/lib/injection.ts +4 -35
- package/src/lib/scss/global.scss +0 -39
- package/src/lib/text.ts +2 -1
- package/src/lib/trailer.ts +22 -0
- package/src/lib/types/title.d.ts +1 -0
- package/src/routes/+page.svelte +2 -17
- package/src/routes/components/Button.svelte +61 -0
- package/src/routes/components/Debugger.svelte +0 -8
- package/src/routes/components/Icons/IconClose.svelte +9 -1
- package/src/routes/components/Icons/IconPlay.svelte +3 -0
- package/src/routes/components/Playlinks/PlaylinkIcon.svelte +4 -1
- package/src/routes/components/RoundButton.svelte +4 -5
- package/src/routes/components/Share.svelte +5 -23
- package/src/routes/components/Title.svelte +22 -22
- package/src/routes/components/Trailer.svelte +18 -0
- package/src/routes/components/YouTubeEmbedOverlay.svelte +96 -0
- package/src/tests/lib/injections.test.js +11 -0
- package/src/tests/lib/text.test.js +10 -0
- package/src/tests/lib/trailer.test.js +56 -0
- package/src/tests/routes/+page.test.js +0 -2
- package/src/tests/routes/components/Button.test.js +28 -0
- package/src/tests/routes/components/Share.test.js +12 -12
- package/src/tests/routes/components/Title.test.js +13 -0
- package/src/tests/routes/components/Trailer.test.js +20 -0
- package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +31 -0
- package/src/routes/components/HighlightedInjection.svelte +0 -230
- package/src/tests/routes/components/HighlightedInjection.test.js +0 -98
|
@@ -714,6 +714,17 @@ describe('linkInjection.js', () => {
|
|
|
714
714
|
expect(document.querySelectorAll('a')).toHaveLength(1)
|
|
715
715
|
})
|
|
716
716
|
|
|
717
|
+
it('Should not inject injections into already existing links when multiple injections are present for the same phrase in the same sentence that only contain the phrase once and the phrase is broken up by HTML', () => {
|
|
718
|
+
document.body.innerHTML = '<p>This is a <strong>phr</strong>ase.</p>'
|
|
719
|
+
|
|
720
|
+
const elements = Array.from(document.querySelectorAll('p'))
|
|
721
|
+
const injection = generateInjection('This is a phrase.', 'phrase')
|
|
722
|
+
|
|
723
|
+
injectLinksInDocument(elements, { aiInjections: [injection, injection], manualInjections: [] })
|
|
724
|
+
|
|
725
|
+
expect(document.querySelectorAll('a')).toHaveLength(1)
|
|
726
|
+
})
|
|
727
|
+
|
|
717
728
|
it('Should not mount popover if user uses touch', async () => {
|
|
718
729
|
mockMatchMedia(true)
|
|
719
730
|
|
|
@@ -364,6 +364,16 @@ describe('text.js', () => {
|
|
|
364
364
|
after: 'matched',
|
|
365
365
|
})
|
|
366
366
|
})
|
|
367
|
+
|
|
368
|
+
it('Should return after phrase properly if element contains odd spacing in elements', () => {
|
|
369
|
+
document.body.innerHTML = '<p>Text with a <em>phrase </em><em>that</em> can be matched</p>'
|
|
370
|
+
const node = /** @type {Element} */ (document.querySelector('body')).childNodes[0]
|
|
371
|
+
|
|
372
|
+
expect(findSurroundingPhrases(node, 12, 23)).toEqual({
|
|
373
|
+
before: 'with a',
|
|
374
|
+
after: 'can be',
|
|
375
|
+
})
|
|
376
|
+
})
|
|
367
377
|
})
|
|
368
378
|
|
|
369
379
|
describe('reverseString', () => {
|
|
@@ -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
|
+
})
|
|
@@ -13,7 +13,6 @@ import { getFullUrlPath } from '$lib/url'
|
|
|
13
13
|
import { fetchAds } from '$lib/api/ads'
|
|
14
14
|
import { fetchConfig } from '$lib/api/config'
|
|
15
15
|
import { hasConsentedTo } from '$lib/consent'
|
|
16
|
-
import { getSplitTestVariantName } from '$lib/splitTest'
|
|
17
16
|
|
|
18
17
|
vi.mock('$lib/api/externalPages', () => ({
|
|
19
18
|
fetchLinkInjections: vi.fn(() => {}),
|
|
@@ -67,7 +66,6 @@ vi.mock('$lib/url', () => ({
|
|
|
67
66
|
|
|
68
67
|
vi.mock('$lib/splitTest', () => ({
|
|
69
68
|
trackSplitTestView: vi.fn(),
|
|
70
|
-
getSplitTestVariantName: vi.fn(),
|
|
71
69
|
}))
|
|
72
70
|
|
|
73
71
|
vi.mock('$lib/api/ads', () => ({
|
|
@@ -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 {
|
|
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(
|
|
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 {
|
|
31
|
+
const { getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
32
32
|
|
|
33
|
-
await fireEvent.click(
|
|
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 {
|
|
40
|
+
const { getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
41
41
|
|
|
42
|
-
await fireEvent.click(
|
|
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 {
|
|
49
|
+
const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
50
50
|
|
|
51
|
-
await fireEvent.click(
|
|
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 {
|
|
58
|
+
const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
59
59
|
|
|
60
|
-
await fireEvent.click(
|
|
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 {
|
|
67
|
+
const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
68
68
|
|
|
69
|
-
await fireEvent.click(
|
|
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,17 @@ 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
|
+
// Temporarily disabled while button is always visible
|
|
102
|
+
// it('Should not show trailer button when embeddable_url is not given', () => {
|
|
103
|
+
// const { queryByText } = render(Title, { title })
|
|
104
|
+
|
|
105
|
+
// expect(queryByText('Watch Trailer')).not.toBeTruthy()
|
|
106
|
+
// })
|
|
94
107
|
})
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
|
|
8
|
+
vi.mock('$lib/trailer', () => ({
|
|
9
|
+
openTrailerOverlay: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
describe('Trailer.svelte', () => {
|
|
13
|
+
it('Should fire given onclick function with given title on click', async () => {
|
|
14
|
+
const { getByRole } = render(Trailer, { title })
|
|
15
|
+
|
|
16
|
+
await fireEvent.click(getByRole('button'))
|
|
17
|
+
|
|
18
|
+
expect(openTrailerOverlay).toHaveBeenCalledWith(title)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { filterRemovedAndInactiveInjections, sortInjections } from '$lib/injection'
|
|
3
|
-
import { t } from '$lib/localization'
|
|
4
|
-
import { titleUrl } from '$lib/routes'
|
|
5
|
-
import { cleanPhrase } from '$lib/text'
|
|
6
|
-
import type { LinkInjection } from '$lib/types/injection'
|
|
7
|
-
import { openModal } from '$lib/modal'
|
|
8
|
-
import { trackSplitTestAction } from '$lib/splitTest'
|
|
9
|
-
import { SplitTest } from '$lib/enums/SplitTest'
|
|
10
|
-
import { scale } from 'svelte/transition'
|
|
11
|
-
import IconIMDb from './Icons/IconIMDb.svelte'
|
|
12
|
-
import TitlePoster from './TitlePoster.svelte'
|
|
13
|
-
import RoundButton from './RoundButton.svelte'
|
|
14
|
-
import IconClose from './Icons/IconClose.svelte'
|
|
15
|
-
|
|
16
|
-
interface Props {
|
|
17
|
-
linkInjections: LinkInjection[]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const { linkInjections }: Props = $props()
|
|
21
|
-
|
|
22
|
-
const scrollThreshold = Math.min(window.innerHeight, 800)
|
|
23
|
-
|
|
24
|
-
const filteredInjections = $derived(filterRemovedAndInactiveInjections(linkInjections))
|
|
25
|
-
const sortedInjections: LinkInjection[] = $derived(sortInjections(filteredInjections))
|
|
26
|
-
|
|
27
|
-
// Get title injection that might relate the most the overall article.
|
|
28
|
-
// If any is found it is used as the primary display, highlighting that injection over others.
|
|
29
|
-
// If none are found we simply use the first active injection and hope for the best.
|
|
30
|
-
const primaryInjection = $derived.by(() => {
|
|
31
|
-
const pageTitle = cleanPhrase(document.querySelector('h1')?.innerText || document.title || '')
|
|
32
|
-
return sortedInjections.find(injection => pageTitle.includes(cleanPhrase(injection.title))) || filteredInjections[0]
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
let shown = $state(false)
|
|
36
|
-
let closed = $state(false)
|
|
37
|
-
let offsetBottom = $state(0)
|
|
38
|
-
|
|
39
|
-
// Only show the element once the user has scroll past a threshold. From here we check the offset from the bottom of
|
|
40
|
-
// the page to make sure we don't overlay the element on top of other elements on the page, such as ads or menus
|
|
41
|
-
function onscroll(): void {
|
|
42
|
-
if (shown || closed) return
|
|
43
|
-
if (window.scrollY < scrollThreshold) return
|
|
44
|
-
|
|
45
|
-
const bottomFixedElementOffsets = getBottomFixedElementOffsets()
|
|
46
|
-
const largestOffset = Math.max(...bottomFixedElementOffsets, 0)
|
|
47
|
-
|
|
48
|
-
offsetBottom = largestOffset + parseFloat(getComputedStyle(document.documentElement).fontSize) // 1 rem
|
|
49
|
-
shown = true
|
|
50
|
-
|
|
51
|
-
trackSplitTestAction(SplitTest.InTextEngagement, 'shown')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Get all elements that are seemingly fixed to the bottom. This is used to determine the position of the element to try
|
|
55
|
-
// and make sure it does not overlap ads or other elements at the bottom of the screen.
|
|
56
|
-
// This is determined by them being fixed (duh) and being close to the bottom of the screen. This might turn out to be unreliable.
|
|
57
|
-
// It does not account for elements that are added after this function is called, which is often the case for ads.
|
|
58
|
-
function getBottomFixedElementOffsets(): number[] {
|
|
59
|
-
const fixedElements = Array.from(document.querySelectorAll('*')).filter((element) => {
|
|
60
|
-
const style = getComputedStyle(element)
|
|
61
|
-
const isFixed = style.position === 'fixed'
|
|
62
|
-
const isVisible = style.display !== 'none' && style.visibility !== 'hidden'
|
|
63
|
-
|
|
64
|
-
const rect = element.getBoundingClientRect()
|
|
65
|
-
const isCloseToBottom = rect.bottom < 50
|
|
66
|
-
const hasSize = rect.width > 0 && rect.height > 0
|
|
67
|
-
|
|
68
|
-
return isFixed && isVisible && isCloseToBottom && hasSize
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
return fixedElements.map((element) => element.clientHeight + (window.innerHeight - element.getBoundingClientRect().bottom))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function onclick(event: MouseEvent): void {
|
|
75
|
-
event.preventDefault()
|
|
76
|
-
|
|
77
|
-
closed = true
|
|
78
|
-
|
|
79
|
-
openModal({ event, injection: primaryInjection, data: primaryInjection.title_details })
|
|
80
|
-
trackSplitTestAction(SplitTest.InTextEngagement, 'click-highlight')
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function close(event: MouseEvent): void {
|
|
84
|
-
event.preventDefault()
|
|
85
|
-
|
|
86
|
-
closed = true
|
|
87
|
-
|
|
88
|
-
trackSplitTestAction(SplitTest.InTextEngagement, 'close')
|
|
89
|
-
}
|
|
90
|
-
</script>
|
|
91
|
-
|
|
92
|
-
<svelte:window {onscroll} />
|
|
93
|
-
|
|
94
|
-
{#if primaryInjection && shown && !closed}
|
|
95
|
-
{@const title = primaryInjection.title_details!}
|
|
96
|
-
|
|
97
|
-
<div class="highlighted-injection" style:bottom="{offsetBottom}px" in:scale={{ start: 0.85, duration: 100 }}>
|
|
98
|
-
<a {onclick} class="link" href={titleUrl(title)} target="_blank" aria-label="{title.title} (opens in a new tab)">
|
|
99
|
-
<div class="poster">
|
|
100
|
-
<TitlePoster {title} width={30} height={43} />
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<div class="content">
|
|
104
|
-
<div class="details">
|
|
105
|
-
<div class="heading">{title.title}</div>
|
|
106
|
-
|
|
107
|
-
<div class="meta">
|
|
108
|
-
<div class="imdb">
|
|
109
|
-
<IconIMDb />
|
|
110
|
-
{title.imdb_score || '-'}
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<div>{title.year}</div>
|
|
114
|
-
|
|
115
|
-
<div class="capitalize">{t(`Type: ${title.type}`)}</div>
|
|
116
|
-
|
|
117
|
-
{#if title.length}
|
|
118
|
-
<div>{title.length} {t('Minutes Short')}</div>
|
|
119
|
-
{/if}
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<div class="action">
|
|
124
|
-
{t('Watch')}
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
</a>
|
|
128
|
-
|
|
129
|
-
<div class="close">
|
|
130
|
-
<RoundButton onclick={close} aria-label="Close">
|
|
131
|
-
<IconClose />
|
|
132
|
-
</RoundButton>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
{/if}
|
|
136
|
-
|
|
137
|
-
<style lang="scss">
|
|
138
|
-
.highlighted-injection {
|
|
139
|
-
position: fixed;
|
|
140
|
-
bottom: margin(1);
|
|
141
|
-
left: margin(1);
|
|
142
|
-
width: calc(100% - margin(2));
|
|
143
|
-
|
|
144
|
-
@include desktop {
|
|
145
|
-
max-width: 400px;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.link {
|
|
150
|
-
display: flex;
|
|
151
|
-
gap: margin(1);
|
|
152
|
-
align-items: flex-start;
|
|
153
|
-
border-radius: theme(border-radius);
|
|
154
|
-
padding: margin(0.5) margin(1) margin(0.5) margin(0.5);
|
|
155
|
-
background: theme(dark);
|
|
156
|
-
box-shadow: theme(shadow-large);
|
|
157
|
-
text-decoration: none;
|
|
158
|
-
|
|
159
|
-
&:hover,
|
|
160
|
-
&:active {
|
|
161
|
-
transform: scale(1.025);
|
|
162
|
-
transition: transform 50ms;
|
|
163
|
-
filter: brightness(theme(hover-filter-brightness));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
&:active {
|
|
167
|
-
transform: scale(0.975);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.poster {
|
|
172
|
-
flex: 0 0 margin(2);
|
|
173
|
-
width: margin(2);
|
|
174
|
-
box-shadow: 0 0 2px 1px theme(content);
|
|
175
|
-
border-radius: theme(border-radius-small);
|
|
176
|
-
background: theme(content);
|
|
177
|
-
overflow: hidden;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.content {
|
|
181
|
-
display: flex;
|
|
182
|
-
align-items: center;
|
|
183
|
-
gap: margin(0.5);
|
|
184
|
-
width: 100%;
|
|
185
|
-
margin: auto 0;
|
|
186
|
-
color: theme(text-color-alt);
|
|
187
|
-
font-family: theme(font-family);
|
|
188
|
-
font-size: 12px;
|
|
189
|
-
line-height: 1.5;
|
|
190
|
-
font-style: normal;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.heading {
|
|
194
|
-
margin-bottom: margin(0.25);
|
|
195
|
-
font-size: theme(font-size-base);
|
|
196
|
-
font-weight: theme(font-bold);
|
|
197
|
-
color: theme(text-color);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.meta {
|
|
201
|
-
display: flex;
|
|
202
|
-
flex-wrap: wrap;
|
|
203
|
-
gap: 0 margin(0.5);
|
|
204
|
-
white-space: nowrap;
|
|
205
|
-
color: theme(text-color-alt);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
.imdb {
|
|
209
|
-
display: flex;
|
|
210
|
-
align-items: center;
|
|
211
|
-
gap: margin(0.25);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
.action {
|
|
215
|
-
grid-area: action;
|
|
216
|
-
margin-left: auto;
|
|
217
|
-
padding: margin(0.5);
|
|
218
|
-
border: theme(playlinks-action-border, 1px solid currentColor);
|
|
219
|
-
border-radius: theme(playlinks-action-border-radius, margin(2));
|
|
220
|
-
font-weight: theme(playlinks-action-font-weight, 500);
|
|
221
|
-
color: theme(playlinks-action-text-color, text-color);
|
|
222
|
-
line-height: 1;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
.close {
|
|
226
|
-
position: absolute;
|
|
227
|
-
top: margin(-0.75);
|
|
228
|
-
right: margin(-0.75);
|
|
229
|
-
}
|
|
230
|
-
</style>
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { render, fireEvent, waitFor } from '@testing-library/svelte'
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import HighlightedInjection from '../../../routes/components/HighlightedInjection.svelte'
|
|
5
|
-
import { generateInjection } from '../../helpers'
|
|
6
|
-
import { title } from '$lib/fakeData'
|
|
7
|
-
import { openModal } from '$lib/modal'
|
|
8
|
-
|
|
9
|
-
vi.mock('$lib/modal', () => ({
|
|
10
|
-
openModal: vi.fn(),
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {number} value
|
|
15
|
-
*/
|
|
16
|
-
async function mockScroll(value) {
|
|
17
|
-
Object.defineProperty(window, 'scrollY', { value, configurable: true })
|
|
18
|
-
window.dispatchEvent(new Event('scroll'))
|
|
19
|
-
|
|
20
|
-
return new Promise(res => setTimeout(res))
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('HighlightedInjection.svelte', () => {
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
vi.resetAllMocks()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const linkInjections = [
|
|
29
|
-
generateInjection('Some sentence', 'Some title'),
|
|
30
|
-
{ ...generateInjection('Some sentence', 'Some title'), title_details: { ...title, title: 'Some second injection' } },
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
it('Should show after scrolling the required distance', async () => {
|
|
34
|
-
const { queryByRole } = render(HighlightedInjection, { linkInjections })
|
|
35
|
-
|
|
36
|
-
await mockScroll(0)
|
|
37
|
-
expect(queryByRole('link')).not.toBeTruthy()
|
|
38
|
-
|
|
39
|
-
await mockScroll(200)
|
|
40
|
-
expect(queryByRole('link')).not.toBeTruthy()
|
|
41
|
-
|
|
42
|
-
await mockScroll(1000)
|
|
43
|
-
expect(queryByRole('link')).toBeTruthy()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('Should not hide after being shown by scrolling', async () => {
|
|
47
|
-
const { queryByRole } = render(HighlightedInjection, { linkInjections })
|
|
48
|
-
|
|
49
|
-
await mockScroll(1000)
|
|
50
|
-
expect(queryByRole('link')).toBeTruthy()
|
|
51
|
-
|
|
52
|
-
await mockScroll(0)
|
|
53
|
-
expect(queryByRole('link')).toBeTruthy()
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('Should render the first injection in the list', async () => {
|
|
57
|
-
const { getByText } = render(HighlightedInjection, { linkInjections })
|
|
58
|
-
|
|
59
|
-
await mockScroll(1000)
|
|
60
|
-
|
|
61
|
-
// @ts-ignore
|
|
62
|
-
expect(getByText(linkInjections[0].title_details.title)).toBeTruthy()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('Should render the injection most relevant to the document title', async () => {
|
|
66
|
-
// @ts-ignore
|
|
67
|
-
document.body.innerHTML = `<h1>${linkInjections[1].title_details.title}</h1>`
|
|
68
|
-
|
|
69
|
-
const { getByText } = render(HighlightedInjection, { linkInjections })
|
|
70
|
-
|
|
71
|
-
await mockScroll(1000)
|
|
72
|
-
|
|
73
|
-
// @ts-ignore
|
|
74
|
-
expect(getByText(linkInjections[1].title_details.title)).toBeTruthy()
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('Should open modal on click', async () => {
|
|
78
|
-
const { getByRole } = render(HighlightedInjection, { linkInjections })
|
|
79
|
-
|
|
80
|
-
await mockScroll(1000)
|
|
81
|
-
await fireEvent.click(getByRole('link'))
|
|
82
|
-
|
|
83
|
-
expect(openModal).toHaveBeenCalled()
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('Should close elements when close button is closed', async () => {
|
|
87
|
-
const { getByLabelText, queryByRole } = render(HighlightedInjection, { linkInjections })
|
|
88
|
-
|
|
89
|
-
await mockScroll(1000)
|
|
90
|
-
await fireEvent.click(getByLabelText('Close'))
|
|
91
|
-
|
|
92
|
-
await waitFor(() => {
|
|
93
|
-
expect(queryByRole('link')).not.toBeTruthy()
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
expect(openModal).not.toHaveBeenCalled()
|
|
97
|
-
})
|
|
98
|
-
})
|