@playpilot/tpi 1.0.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/.github/workflows/tests.yml +22 -0
- package/.prettierignore +4 -0
- package/.prettierrc +16 -0
- package/README.md +38 -0
- package/dist/link-injections.js +7 -0
- package/eslint.config.js +33 -0
- package/index.html +11 -0
- package/jsconfig.json +19 -0
- package/package.json +35 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +12 -0
- package/src/demo.spec.js +7 -0
- package/src/lib/api.js +160 -0
- package/src/lib/array.js +15 -0
- package/src/lib/auth.js +84 -0
- package/src/lib/constants.js +2 -0
- package/src/lib/enums/TrackingEvent.js +15 -0
- package/src/lib/fakeData.js +140 -0
- package/src/lib/genres.json +420 -0
- package/src/lib/global.css +37 -0
- package/src/lib/hash.js +15 -0
- package/src/lib/html.js +21 -0
- package/src/lib/index.js +1 -0
- package/src/lib/linkInjection.js +275 -0
- package/src/lib/search.js +24 -0
- package/src/lib/text.js +61 -0
- package/src/lib/tracking.js +32 -0
- package/src/lib/variables.css +16 -0
- package/src/main.js +45 -0
- package/src/routes/+layout.svelte +54 -0
- package/src/routes/+page.svelte +96 -0
- package/src/routes/components/AfterArticlePlaylinks.svelte +90 -0
- package/src/routes/components/ContextMenu.svelte +67 -0
- package/src/routes/components/Description.svelte +47 -0
- package/src/routes/components/Editorial/Alert.svelte +18 -0
- package/src/routes/components/Editorial/DragHandle.svelte +134 -0
- package/src/routes/components/Editorial/Editor.svelte +277 -0
- package/src/routes/components/Editorial/EditorItem.svelte +260 -0
- package/src/routes/components/Editorial/ManualInjection.svelte +192 -0
- package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +132 -0
- package/src/routes/components/Editorial/Search/TitleSearch.svelte +176 -0
- package/src/routes/components/Editorial/Switch.svelte +76 -0
- package/src/routes/components/Editorial/TextInput.svelte +29 -0
- package/src/routes/components/Genres.svelte +41 -0
- package/src/routes/components/Icons/IconAlign.svelte +12 -0
- package/src/routes/components/Icons/IconBack.svelte +3 -0
- package/src/routes/components/Icons/IconBookmark.svelte +3 -0
- package/src/routes/components/Icons/IconChevron.svelte +18 -0
- package/src/routes/components/Icons/IconClose.svelte +3 -0
- package/src/routes/components/Icons/IconContinue.svelte +3 -0
- package/src/routes/components/Icons/IconDots.svelte +5 -0
- package/src/routes/components/Icons/IconEnlarge.svelte +12 -0
- package/src/routes/components/Icons/IconIMDb.svelte +3 -0
- package/src/routes/components/Icons/IconNewTab.svelte +3 -0
- package/src/routes/components/Modal.svelte +106 -0
- package/src/routes/components/Participants.svelte +44 -0
- package/src/routes/components/Playlinks.svelte +155 -0
- package/src/routes/components/Popover.svelte +95 -0
- package/src/routes/components/RoundButton.svelte +38 -0
- package/src/routes/components/SkeletonText.svelte +33 -0
- package/src/routes/components/Title.svelte +180 -0
- package/src/routes/components/TitleModal.svelte +24 -0
- package/src/routes/components/TitlePopover.svelte +17 -0
- package/src/tests/helpers.js +18 -0
- package/src/tests/lib/api.test.js +162 -0
- package/src/tests/lib/array.test.js +14 -0
- package/src/tests/lib/auth.test.js +115 -0
- package/src/tests/lib/hash.test.js +28 -0
- package/src/tests/lib/html.test.js +16 -0
- package/src/tests/lib/linkInjection.test.js +754 -0
- package/src/tests/lib/search.test.js +42 -0
- package/src/tests/lib/text.test.js +94 -0
- package/src/tests/lib/tracking.test.js +71 -0
- package/src/tests/routes/+page.test.js +109 -0
- package/src/tests/routes/components/AfterArticlePlaylinks.test.js +115 -0
- package/src/tests/routes/components/ContextMenu.test.js +37 -0
- package/src/tests/routes/components/Description.test.js +58 -0
- package/src/tests/routes/components/Editorial/Alert.test.js +17 -0
- package/src/tests/routes/components/Editorial/DragHandle.test.js +55 -0
- package/src/tests/routes/components/Editorial/Editor.test.js +64 -0
- package/src/tests/routes/components/Editorial/EditorItem.test.js +142 -0
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +114 -0
- package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +63 -0
- package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +58 -0
- package/src/tests/routes/components/Editorial/Switch.test.js +60 -0
- package/src/tests/routes/components/Editorial/TextInput.test.js +30 -0
- package/src/tests/routes/components/Genres.test.js +37 -0
- package/src/tests/routes/components/Modal.test.js +84 -0
- package/src/tests/routes/components/Participants.test.js +33 -0
- package/src/tests/routes/components/Playlinks.test.js +101 -0
- package/src/tests/routes/components/Popover.test.js +66 -0
- package/src/tests/routes/components/RoundButton.test.js +35 -0
- package/src/tests/routes/components/SkeletonText.test.js +12 -0
- package/src/tests/routes/components/Title.test.js +82 -0
- package/src/tests/routes/components/TitleModal.test.js +33 -0
- package/src/tests/routes/components/TitlePopover.test.js +23 -0
- package/src/tests/setup.js +53 -0
- package/src/typedefs.js +72 -0
- package/static/favicon.png +0 -0
- package/svelte.config.js +13 -0
- package/vite.config.js +61 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
2
|
+
import { searchTitles } from '$lib/search'
|
|
3
|
+
import { fakeFetch } from '../helpers'
|
|
4
|
+
import { title } from '$lib/fakeData'
|
|
5
|
+
|
|
6
|
+
describe('$lib/search', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.resetAllMocks()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('searchTitles', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
window.PlayPilotLinkInjections = { token: 'a' }
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('Should call fetch with api token and given query', async () => {
|
|
18
|
+
fakeFetch({ response: [title] })
|
|
19
|
+
|
|
20
|
+
const response = await searchTitles('Some query')
|
|
21
|
+
|
|
22
|
+
expect(response).toEqual([title])
|
|
23
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
24
|
+
expect.stringMatching(/api-token=[^&]+&query=[^&]+/),
|
|
25
|
+
expect.anything(),
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('Should throw when response is invalid', async () => {
|
|
30
|
+
fakeFetch({ response: null, ok: false })
|
|
31
|
+
|
|
32
|
+
expect(async () => await searchTitles('')).rejects.toThrowError()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('Should throw when no api token is given', async () => {
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
window.PlayPilotLinkInjections = null
|
|
38
|
+
|
|
39
|
+
expect(async () => await searchTitles('')).rejects.toThrowError('No token was provided')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { findTextNodeContaining, isNodeInLink, replaceStartingFrom } from '$lib/text'
|
|
3
|
+
|
|
4
|
+
describe('text.js', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
document.body.innerHTML = `
|
|
7
|
+
<section>
|
|
8
|
+
<p>Some sentence <strong>with words</strong> in it</p>
|
|
9
|
+
<a href="#">A link <span>inside</span></a>
|
|
10
|
+
<div>Another <span>text</span> node</div>
|
|
11
|
+
</section>
|
|
12
|
+
`
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('findTextNodeContaining', () => {
|
|
16
|
+
it('Should find the correct text node inside an element', () => {
|
|
17
|
+
document.body.innerHTML = '<section>Some sentence with several words in it.</section>'
|
|
18
|
+
|
|
19
|
+
const element = /** @type {HTMLElement} */ (document.querySelector('section'))
|
|
20
|
+
expect(findTextNodeContaining('words', element)?.parentNode?.nodeName).toBe('SECTION')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('Should find the correct text node inside a child element element', () => {
|
|
24
|
+
document.body.innerHTML = '<section>Some sentence with <strong>several words</strong> in it.</section>'
|
|
25
|
+
|
|
26
|
+
const element = /** @type {HTMLElement} */ (document.querySelector('section'))
|
|
27
|
+
expect(findTextNodeContaining('words', element)?.parentNode?.nodeName).toBe('STRONG')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('Should return null if the text is not found', () => {
|
|
31
|
+
document.body.innerHTML = '<section>Some sentence.</section>'
|
|
32
|
+
|
|
33
|
+
const element = /** @type {HTMLElement} */ (document.querySelector('section'))
|
|
34
|
+
expect(findTextNodeContaining('word', element)).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('Should not return a node if it is of a filtered parent type', () => {
|
|
38
|
+
document.body.innerHTML = '<section>Some sentence with <strong>several words</strong> in it.</section>'
|
|
39
|
+
|
|
40
|
+
const element = /** @type {HTMLElement} */ (document.querySelector('section'))
|
|
41
|
+
expect(findTextNodeContaining('words', element, ['STRONG'])).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('Should return the first occurance of a node that is not in the filtered parent type', () => {
|
|
45
|
+
document.body.innerHTML = '<section>Some sentence with <strong>several words</strong> in it and other words.</section>'
|
|
46
|
+
|
|
47
|
+
const element = /** @type {HTMLElement} */ (document.querySelector('section'))
|
|
48
|
+
expect(findTextNodeContaining('words', element, ['STRONG'])?.parentNode?.nodeName).toBe('SECTION')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('isNodeInLink', () => {
|
|
53
|
+
it('Should return true if the node is inside a link', () => {
|
|
54
|
+
document.body.innerHTML = '<section>Some sentence <a>with a link</a> in it.</section>'
|
|
55
|
+
|
|
56
|
+
const element = /** @type {HTMLElement} */ (document.querySelector('a')).childNodes[0]
|
|
57
|
+
expect(isNodeInLink(element)).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('Should return true if the node is a child inside a link', () => {
|
|
61
|
+
document.body.innerHTML = '<section>Some sentence <a>with a <span>link</span></a> in it.</section>'
|
|
62
|
+
|
|
63
|
+
const element = /** @type {HTMLElement} */ (document.querySelector('span')).childNodes[0]
|
|
64
|
+
expect(isNodeInLink(element)).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('Should return false if the node is not inside a link', () => {
|
|
68
|
+
document.body.innerHTML = '<section>Some sentence <span>link</span> in it.</section>'
|
|
69
|
+
|
|
70
|
+
const element = /** @type {HTMLElement} */ (document.querySelector('span')).childNodes[0]
|
|
71
|
+
expect(isNodeInLink(element)).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('replaceStartingFrom', () => {
|
|
76
|
+
it('Should replace a given string with a given replacement', () => {
|
|
77
|
+
expect(replaceStartingFrom('Some text', 'text', 'word', 0)).toBe('Some word')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('Should replace a given string with a given replacement starting from a given index', () => {
|
|
81
|
+
expect(replaceStartingFrom('Some text with more text', 'text', 'words', 10)).toBe('Some text with more words')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('Should return the original string if no replacement was found', () => {
|
|
85
|
+
expect(replaceStartingFrom('Some text', 'word', 'other', 0)).toBe('Some text')
|
|
86
|
+
expect(replaceStartingFrom('Some text', 'text', 'other', 20)).toBe('Some text')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('Should treat unencoded and encoded characters as the same', () => {
|
|
90
|
+
expect(replaceStartingFrom('Some text & more', 'text & more', 'word', 0)).toBe('Some word')
|
|
91
|
+
expect(replaceStartingFrom('Some text & more', 'text & more', 'word', 0)).toBe('Some word')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { track } from '$lib/tracking'
|
|
4
|
+
import { title } from '$lib/fakeData'
|
|
5
|
+
|
|
6
|
+
global.fetch = vi.fn()
|
|
7
|
+
|
|
8
|
+
describe('$lib/tracking', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.resetAllMocks()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('track', () => {
|
|
14
|
+
it('Should fetch with given event and expected payload', () => {
|
|
15
|
+
track('Some event')
|
|
16
|
+
|
|
17
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
18
|
+
expect.stringContaining('insights'),
|
|
19
|
+
expect.objectContaining({
|
|
20
|
+
body: expect.stringContaining('Some event'),
|
|
21
|
+
headers: expect.any(Object),
|
|
22
|
+
method: 'POST',
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('Should include current url in request', () => {
|
|
28
|
+
track('Some event')
|
|
29
|
+
|
|
30
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
31
|
+
expect.any(String),
|
|
32
|
+
expect.objectContaining({
|
|
33
|
+
body: expect.stringContaining(window.location.href),
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('Should include relevant title data when title is given', () => {
|
|
39
|
+
track('Some event', title)
|
|
40
|
+
|
|
41
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
42
|
+
expect.any(String),
|
|
43
|
+
expect.objectContaining({
|
|
44
|
+
body: expect.stringContaining(`"original_title":"${title.original_title}"`),
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('Should include given payload', () => {
|
|
50
|
+
track('Some event', null, { 'Some key': 'Some value' })
|
|
51
|
+
|
|
52
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
53
|
+
expect.any(String),
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
body: expect.stringContaining('"Some key":"Some value"'),
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('Should override given title data if same data is given in payload', () => {
|
|
61
|
+
track('Some event', title, { original_title: 'Some other title' })
|
|
62
|
+
|
|
63
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
64
|
+
expect.any(String),
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
body: expect.stringContaining('"original_title":"Some other title"'),
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import page from '../../routes/+page.svelte'
|
|
5
|
+
import { pollLinkInjections } from '$lib/api'
|
|
6
|
+
import { track } from '$lib/tracking'
|
|
7
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
8
|
+
import { authorize } from '$lib/auth'
|
|
9
|
+
|
|
10
|
+
vi.mock('$lib/api', () => ({
|
|
11
|
+
pollLinkInjections: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock('$lib/tracking', () => ({
|
|
15
|
+
track: vi.fn(),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
vi.mock(import('$lib/auth'), async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal()
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
authorize: vi.fn(),
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('$routes/+page.svelte', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.resetAllMocks()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
Object.defineProperty(window, 'location', {
|
|
33
|
+
writable: true,
|
|
34
|
+
value: { search: '' },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
window.PlayPiloLinkInjections = null
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('Should call pollLinkInjections on mount', () => {
|
|
42
|
+
render(page)
|
|
43
|
+
expect(pollLinkInjections).toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('Should use the given selector when present', () => {
|
|
47
|
+
document.body.innerHTML = '<div class="some-element"><p>Here</p></div> <p>Not here</p>'
|
|
48
|
+
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
window.PlayPilotLinkInjections = { selector: '.some-element' }
|
|
51
|
+
|
|
52
|
+
render(page)
|
|
53
|
+
expect(pollLinkInjections).toHaveBeenCalledWith(expect.anything(), '<p>Here</p>')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('Should render editor if correct url param is given', async () => {
|
|
57
|
+
vi.mocked(authorize).mockResolvedValueOnce(true)
|
|
58
|
+
window.location.search = '?playpilot-editorial-mode=true'
|
|
59
|
+
|
|
60
|
+
const { container } = render(page)
|
|
61
|
+
await new Promise(res => setTimeout(res)) // Await auth
|
|
62
|
+
|
|
63
|
+
expect(container.querySelector('.editor')).toBeTruthy()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('Should render editor if initialized with editorial_token', async () => {
|
|
67
|
+
vi.mocked(authorize).mockResolvedValueOnce(true)
|
|
68
|
+
// @ts-ignore
|
|
69
|
+
window.PlayPilotLinkInjections = { editorial_token: 'some-token' }
|
|
70
|
+
|
|
71
|
+
const { container } = render(page)
|
|
72
|
+
await new Promise(res => setTimeout(res)) // Await auth
|
|
73
|
+
|
|
74
|
+
expect(container.querySelector('.editor')).toBeTruthy()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('Should not render editor if correct url param is given but user is not authed', async () => {
|
|
78
|
+
vi.mocked(authorize).mockResolvedValueOnce(false)
|
|
79
|
+
window.location.search = '?playpilot-editorial-mode=true'
|
|
80
|
+
|
|
81
|
+
const { container } = render(page)
|
|
82
|
+
await new Promise(res => setTimeout(res)) // Await auth
|
|
83
|
+
|
|
84
|
+
expect(container.querySelector('.editor')).not.toBeTruthy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('Should not render editor if correct url param is not given', async () => {
|
|
88
|
+
window.location.search = '?playpilot-editorial-mode=false'
|
|
89
|
+
let { container } = render(page)
|
|
90
|
+
await new Promise(res => setTimeout(res)) // Await auth
|
|
91
|
+
expect(container.querySelector('.editor')).not.toBeTruthy()
|
|
92
|
+
|
|
93
|
+
window.location.search = '?other-param-mode=true'
|
|
94
|
+
;({ container } = render(page))
|
|
95
|
+
await new Promise(res => setTimeout(res)) // Await auth
|
|
96
|
+
expect(container.querySelector('.editor')).not.toBeTruthy()
|
|
97
|
+
|
|
98
|
+
window.location.search = ''
|
|
99
|
+
;({ container } = render(page))
|
|
100
|
+
await new Promise(res => setTimeout(res)) // Await auth
|
|
101
|
+
expect(container.querySelector('.editor')).not.toBeTruthy()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('Should track event when the page is loaded', () => {
|
|
105
|
+
render(page)
|
|
106
|
+
|
|
107
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.ArticlePageView)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import AfterArticlePlaylinks from '../../../routes/components/AfterArticlePlaylinks.svelte'
|
|
5
|
+
import { title } from '$lib/fakeData'
|
|
6
|
+
import { track } from '$lib/tracking'
|
|
7
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
8
|
+
|
|
9
|
+
vi.mock('$lib/tracking', () => ({
|
|
10
|
+
track: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
vi.mock('svelte', async (importActual) => ({
|
|
14
|
+
...(await importActual()),
|
|
15
|
+
getContext: vi.fn(),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
const linkInjections = [{
|
|
19
|
+
sid: '1',
|
|
20
|
+
title: 'an injection',
|
|
21
|
+
sentence: 'This is a sentence with an injection.',
|
|
22
|
+
playpilot_url: 'https://some-link.com/',
|
|
23
|
+
poster: 'some-poster',
|
|
24
|
+
key: 'some-key-1',
|
|
25
|
+
title_details: title,
|
|
26
|
+
}, {
|
|
27
|
+
sid: '2',
|
|
28
|
+
title: 'a sentence',
|
|
29
|
+
sentence: 'This is a sentence with an injection.',
|
|
30
|
+
playpilot_url: 'https://some-link.com/',
|
|
31
|
+
poster: 'some-poster',
|
|
32
|
+
key: 'some-key-1',
|
|
33
|
+
title_details: title,
|
|
34
|
+
}]
|
|
35
|
+
|
|
36
|
+
describe('AfterArticlePlaylinks.svelte', () => {
|
|
37
|
+
it('Should render each given injection', () => {
|
|
38
|
+
const { getByText } = render(AfterArticlePlaylinks, { linkInjections })
|
|
39
|
+
|
|
40
|
+
expect(getByText('an injection', { exact: false })).toBeTruthy()
|
|
41
|
+
expect(getByText('a sentence', { exact: false })).toBeTruthy()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('Should render the playlinks for a given injection', () => {
|
|
45
|
+
const { getByText } = render(AfterArticlePlaylinks, { linkInjections: [linkInjections[0]] })
|
|
46
|
+
|
|
47
|
+
const playlinks = linkInjections[0].title_details.providers
|
|
48
|
+
|
|
49
|
+
expect(/** @type {HTMLAnchorElement} */ (getByText(playlinks[0].name)).href).toBe(playlinks[0].url)
|
|
50
|
+
expect(/** @type {HTMLAnchorElement} */ (getByText(playlinks[1].name)).href).toBe(playlinks[1].url)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('Should not render a trailing comma and end in a period', () => {
|
|
54
|
+
const { container } = render(AfterArticlePlaylinks, { linkInjections })
|
|
55
|
+
|
|
56
|
+
const text = container.innerText.trim()
|
|
57
|
+
|
|
58
|
+
expect(text.charAt(text.length - 1)).toBe('.')
|
|
59
|
+
expect(text.charAt(text.length - 2)).not.toBe(',')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('Should render final playlink prepended with "and"', () => {
|
|
63
|
+
const { container } = render(AfterArticlePlaylinks, { linkInjections: [linkInjections[0]] })
|
|
64
|
+
|
|
65
|
+
const playlinks = linkInjections[0].title_details.providers
|
|
66
|
+
|
|
67
|
+
const text = container.innerText.trim()
|
|
68
|
+
expect(text).toBe(`"${linkInjections[0].title}" is available to stream on ${playlinks[0].name}, and ${playlinks[1].name}.`)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('Should not render "and" after final playlink when only 1 playlink is present', () => {
|
|
72
|
+
/** @type {LinkInjection} */
|
|
73
|
+
const injection = { ...linkInjections[0], title_details: { ...linkInjections[0].title_details, providers: [linkInjections[0].title_details.providers[0]] }}
|
|
74
|
+
const { container } = render(AfterArticlePlaylinks, { linkInjections: [injection] })
|
|
75
|
+
|
|
76
|
+
const playlinks = injection.title_details?.providers || []
|
|
77
|
+
|
|
78
|
+
const text = container.innerText.trim()
|
|
79
|
+
expect(text).toBe(`"${injection.title}" is available to stream on ${playlinks[0].name}.`)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('Should call track event when playlink is clicked', async () => {
|
|
83
|
+
const { getAllByRole } = render(AfterArticlePlaylinks, { linkInjections })
|
|
84
|
+
|
|
85
|
+
await fireEvent.click(getAllByRole('link')[0])
|
|
86
|
+
|
|
87
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.AfterArticlePlaylinkClick, expect.any(Object), { playlink: expect.any(String) })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('Should render titles without playlinks as empty state', () => {
|
|
91
|
+
const injection = { ...linkInjections[0], title_details: { ...linkInjections[0].title_details, providers: [] }}
|
|
92
|
+
const { getByText } = render(AfterArticlePlaylinks, { linkInjections: [injection] })
|
|
93
|
+
|
|
94
|
+
expect(getByText(`"${injection.title}" is not currently available to stream.`)).toBeTruthy()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('Should display as modal button if after_article_style is modal_button', () => {
|
|
98
|
+
const injection = { ...linkInjections[0], after_article_style: 'modal_button' }
|
|
99
|
+
const { getByText } = render(AfterArticlePlaylinks, { linkInjections: [injection] })
|
|
100
|
+
|
|
101
|
+
expect(getByText(`"${injection.title}" is available to stream.`)).toBeTruthy()
|
|
102
|
+
expect(getByText('View streaming options')).toBeTruthy()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('Should fire given onclickmodal function when display as modal_button', async () => {
|
|
106
|
+
const onclickmodal = vi.fn()
|
|
107
|
+
const injection = { ...linkInjections[0], after_article_style: 'modal_button' }
|
|
108
|
+
const { getByText } = render(AfterArticlePlaylinks, { linkInjections: [injection], onclickmodal })
|
|
109
|
+
|
|
110
|
+
await fireEvent.click(getByText('View streaming options'))
|
|
111
|
+
|
|
112
|
+
expect(onclickmodal).toHaveBeenCalled()
|
|
113
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.AfterArticlePlaylinkClick, expect.any(Object), { playlink: expect.any(String) })
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import ContextMenu from '../../../routes/components/ContextMenu.svelte'
|
|
5
|
+
import { createRawSnippet } from 'svelte'
|
|
6
|
+
|
|
7
|
+
describe('ContextMenu.svelte', () => {
|
|
8
|
+
const children = createRawSnippet(() => ({ render: () => '<p>Some snippet</p>' }))
|
|
9
|
+
|
|
10
|
+
it('Should open the context menu on click', async () => {
|
|
11
|
+
const { getByRole, queryByText } = render(ContextMenu, { ariaLabel: '', children })
|
|
12
|
+
|
|
13
|
+
expect(queryByText('Some snippet')).not.toBeTruthy()
|
|
14
|
+
|
|
15
|
+
await fireEvent.click(getByRole('button'))
|
|
16
|
+
|
|
17
|
+
expect(queryByText('Some snippet')).toBeTruthy()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('Should close the context menu on second click', async () => {
|
|
21
|
+
const { getByRole, queryByText } = render(ContextMenu, { ariaLabel: '', children })
|
|
22
|
+
|
|
23
|
+
await fireEvent.click(getByRole('button'))
|
|
24
|
+
await fireEvent.click(getByRole('button'))
|
|
25
|
+
|
|
26
|
+
expect(queryByText('Some snippet')).not.toBeTruthy()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('Should close the context menu when clicking outside of the button', async () => {
|
|
30
|
+
const { getByRole, queryByText } = render(ContextMenu, { ariaLabel: '', children })
|
|
31
|
+
|
|
32
|
+
await fireEvent.click(getByRole('button'))
|
|
33
|
+
await fireEvent.click(document.body)
|
|
34
|
+
|
|
35
|
+
expect(queryByText('Some snippet')).not.toBeTruthy()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import Description from '../../../routes/components/Description.svelte'
|
|
5
|
+
|
|
6
|
+
describe('Description.svelte', () => {
|
|
7
|
+
it('Should render given text', () => {
|
|
8
|
+
const { getByText } = render(Description, { text: 'Some test description' })
|
|
9
|
+
|
|
10
|
+
expect(getByText('Some test description')).toBeTruthy()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('Should limit given description to given limit', () => {
|
|
14
|
+
const { getByText } = render(Description, { text: 'Some test description', limit: 5 })
|
|
15
|
+
|
|
16
|
+
expect(getByText('Some ...')).toBeTruthy()
|
|
17
|
+
expect(getByText('Show more')).toBeTruthy()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('Should not render show more button if text is shorter than limit', () => {
|
|
21
|
+
const { queryByRole } = render(Description, { text: 'Some test description', limit: 50 })
|
|
22
|
+
|
|
23
|
+
expect(queryByRole('button')).not.toBeTruthy()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('Should expand description and remove show more button after clicking', async () => {
|
|
27
|
+
const { getByText, queryByRole } = render(Description, { text: 'Some test description', limit: 5 })
|
|
28
|
+
|
|
29
|
+
expect(getByText('Some ...')).toBeTruthy()
|
|
30
|
+
|
|
31
|
+
await fireEvent.click(/** @type {Node} **/(queryByRole('button')))
|
|
32
|
+
|
|
33
|
+
expect(getByText('Some test description')).toBeTruthy()
|
|
34
|
+
expect(queryByRole('button')).not.toBeTruthy()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('Should include blurb after expanding if given', async () => {
|
|
38
|
+
const { queryByText, queryByRole } = render(Description, { text: 'Some test description', limit: 5, blurb: 'Some blurb' })
|
|
39
|
+
|
|
40
|
+
expect(queryByText('Some blurb')).not.toBeTruthy()
|
|
41
|
+
|
|
42
|
+
await fireEvent.click(/** @type {Node} **/(queryByRole('button')))
|
|
43
|
+
|
|
44
|
+
expect(queryByText('Some blurb')).toBeTruthy()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('Should show "show more" button regardless of limit if blurb is given', async () => {
|
|
48
|
+
const { getByText } = render(Description, { text: 'Some test description', limit: 200, blurb: 'Some blurb' })
|
|
49
|
+
|
|
50
|
+
expect(getByText('Show more')).toBeTruthy()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('Should not show "show more" button if limit is passed and no blurb is given', async () => {
|
|
54
|
+
const { queryByText } = render(Description, { text: 'Some test description', limit: 200, blurb: '' })
|
|
55
|
+
|
|
56
|
+
expect(queryByText('Show more')).not.toBeTruthy()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import Alert from '../../../../routes/components/Editorial/Alert.svelte'
|
|
5
|
+
import { createRawSnippet } from 'svelte'
|
|
6
|
+
|
|
7
|
+
describe('Alert.svelte', () => {
|
|
8
|
+
it('Should render the given snippet', () => {
|
|
9
|
+
const children = createRawSnippet(() => ({
|
|
10
|
+
render: () => '<p>Some snippet</p>',
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
const { getByText } = render(Alert, { children })
|
|
14
|
+
|
|
15
|
+
expect(getByText('Some snippet')).toBeTruthy()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import DragHandle from '../../../../routes/components/Editorial/DragHandle.svelte'
|
|
5
|
+
|
|
6
|
+
describe('DragHandle.svelte', () => {
|
|
7
|
+
it('Should position the given element based on the given position', () => {
|
|
8
|
+
document.body.innerHTML = '<div></div>'
|
|
9
|
+
const element = document.querySelector('div')
|
|
10
|
+
render(DragHandle, { element, position: { x: 50, y: 30 } })
|
|
11
|
+
|
|
12
|
+
expect(element?.style.right).toBe('50px')
|
|
13
|
+
expect(element?.style.bottom).toBe('30px')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('Should limit the position to be visible on the screen', () => {
|
|
17
|
+
document.body.innerHTML = '<div></div>'
|
|
18
|
+
const element = document.querySelector('div')
|
|
19
|
+
render(DragHandle, { element, position: { x: 5000, y: 5000 } })
|
|
20
|
+
|
|
21
|
+
expect(element?.style.right).toBe(window.innerWidth + 'px')
|
|
22
|
+
expect(element?.style.bottom).toBe(window.innerHeight + 'px')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('Should limit the position to the given limit', () => {
|
|
26
|
+
document.body.innerHTML = '<div></div>'
|
|
27
|
+
const element = document.querySelector('div')
|
|
28
|
+
render(DragHandle, { element, position: { x: 0, y: 0 }, limit: { x: 10, y: 10 } })
|
|
29
|
+
|
|
30
|
+
expect(element?.style.right).toBe('10px')
|
|
31
|
+
expect(element?.style.bottom).toBe('10px')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('Should limit the position to both the screen size and the given limit', () => {
|
|
35
|
+
document.body.innerHTML = '<div></div>'
|
|
36
|
+
const element = document.querySelector('div')
|
|
37
|
+
render(DragHandle, { element, position: { x: 5000, y: 5000 }, limit: { x: 10, y: 10 } })
|
|
38
|
+
|
|
39
|
+
expect(element?.style.right).toBe(window.innerWidth - 10 + 'px')
|
|
40
|
+
expect(element?.style.bottom).toBe(window.innerHeight - 10 + 'px')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('Should fire given onchange function when position changes', () => {
|
|
44
|
+
document.body.innerHTML = '<div></div>'
|
|
45
|
+
|
|
46
|
+
const onchange = vi.fn()
|
|
47
|
+
const element = document.querySelector('div')
|
|
48
|
+
render(DragHandle, { element, position: { x: 5000, y: 5000 }, onchange })
|
|
49
|
+
|
|
50
|
+
expect(onchange).toHaveBeenCalledWith({
|
|
51
|
+
x: window.innerWidth,
|
|
52
|
+
y: window.innerHeight,
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import Editor from '../../../../routes/components/Editorial/Editor.svelte'
|
|
5
|
+
import { title } from '$lib/fakeData'
|
|
6
|
+
|
|
7
|
+
vi.mock('$lib/api', () => ({
|
|
8
|
+
saveLinkInjections: vi.fn(),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
describe('Editor.svelte', () => {
|
|
12
|
+
const linkInjections = [{
|
|
13
|
+
sid: '123',
|
|
14
|
+
key: '1',
|
|
15
|
+
title: 'an injection',
|
|
16
|
+
sentence: 'This is a sentence with an injection.',
|
|
17
|
+
playpilot_url: 'https://some-link.com/',
|
|
18
|
+
title_details: title,
|
|
19
|
+
}, {
|
|
20
|
+
sid: '456',
|
|
21
|
+
key: '2',
|
|
22
|
+
title: 'a sentence',
|
|
23
|
+
sentence: 'This is a sentence with an injection.',
|
|
24
|
+
playpilot_url: 'https://some-link.com/',
|
|
25
|
+
title_details: title,
|
|
26
|
+
}]
|
|
27
|
+
|
|
28
|
+
it('Should show the number of injections', () => {
|
|
29
|
+
const { getByText } = render(Editor, { linkInjections })
|
|
30
|
+
|
|
31
|
+
expect(getByText(linkInjections.length)).toBeTruthy()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('Should show injections', async () => {
|
|
35
|
+
const { getAllByText } = render(Editor, { linkInjections })
|
|
36
|
+
|
|
37
|
+
expect(getAllByText(title.title)).toHaveLength(2)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('Should display a loading state when loading is true', async () => {
|
|
41
|
+
const { getByText, container } = render(Editor, { linkInjections: [], loading: true })
|
|
42
|
+
|
|
43
|
+
expect(getByText('Loading...')).toBeTruthy()
|
|
44
|
+
expect(container.querySelector('.loading')).toBeTruthy()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('Should not display a loading state when loading is false', async () => {
|
|
48
|
+
const { queryByText } = render(Editor, { linkInjections: [], loading: false })
|
|
49
|
+
|
|
50
|
+
expect(queryByText('Loading...')).not.toBeTruthy()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('Should not show save button in enlarged view if no injections are present', async () => {
|
|
54
|
+
const { queryByText } = render(Editor, { linkInjections: [], loading: false })
|
|
55
|
+
|
|
56
|
+
expect(queryByText('Save links')).not.toBeTruthy()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('Should show save button in enlarged view if injections are present', async () => {
|
|
60
|
+
const { queryByText } = render(Editor, { linkInjections, loading: false })
|
|
61
|
+
|
|
62
|
+
expect(queryByText('Save links')).toBeTruthy()
|
|
63
|
+
})
|
|
64
|
+
})
|