@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,162 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
2
|
+
import { fetchLinkInjections, pollLinkInjections } from '$lib/api'
|
|
3
|
+
import { fakeFetch } from '../helpers'
|
|
4
|
+
import { authorize, isEditorialModeEnabled } from '$lib/auth'
|
|
5
|
+
|
|
6
|
+
vi.mock('$lib/auth', async importActual => {
|
|
7
|
+
const actual = await importActual()
|
|
8
|
+
return {
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
...actual,
|
|
11
|
+
authorize: vi.fn(() => false),
|
|
12
|
+
isEditorialModeEnabled: vi.fn(() => false),
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('$lib/api', () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.resetAllMocks()
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
window.PlayPilotLinkInjections = null
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('fetchLinkInjections', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
window.PlayPilotLinkInjections = { token: 'a' }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('Should call fetch with given url and body', async () => {
|
|
30
|
+
fakeFetch({ response: 'Some response' })
|
|
31
|
+
|
|
32
|
+
const response = await fetchLinkInjections('https://some-url', 'some-html', { hash: 'some-hash' })
|
|
33
|
+
|
|
34
|
+
expect(response).toBe('Some response')
|
|
35
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
36
|
+
expect.stringContaining('api-token'),
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
hash: 'some-hash',
|
|
40
|
+
url: 'https://some-url',
|
|
41
|
+
page_text: 'some-html',
|
|
42
|
+
}),
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: expect.any(Object),
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('Should throw when no api token was given', async () => {
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
window.PlayPilotLinkInjections = null
|
|
52
|
+
|
|
53
|
+
await expect(async () => await fetchLinkInjections('https://some-url', 'some-html', { hash: 'some-hash' })).rejects.toThrowError('No token was provided')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('Should throw when response was incorrect', async () => {
|
|
57
|
+
fakeFetch({ ok: false })
|
|
58
|
+
|
|
59
|
+
await expect(async () => await fetchLinkInjections('https://some-url', 'some-html', { hash: 'some-hash' })).rejects.toThrowError()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('Should use given api token', async () => {
|
|
63
|
+
fakeFetch({ response: 'Some response' })
|
|
64
|
+
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
window.PlayPilotLinkInjections = { token: 'token' }
|
|
67
|
+
|
|
68
|
+
await fetchLinkInjections('https://some-url', 'some-html', { hash: 'some-hash' })
|
|
69
|
+
|
|
70
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
71
|
+
expect.stringContaining('api-token=token'),
|
|
72
|
+
expect.any(Object),
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('Should throw when no api token was given', async () => {
|
|
77
|
+
fakeFetch({ response: 'Some response' })
|
|
78
|
+
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
window.PlayPilotLinkInjections = null
|
|
81
|
+
|
|
82
|
+
await expect(async () => await fetchLinkInjections('https://some-url', 'some-html', { hash: 'some-hash' })).rejects.toThrowError()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('Should include editorial_mode_enabled=true in url if editorial mode is true', async () => {
|
|
86
|
+
fakeFetch({ response: 'Some response' })
|
|
87
|
+
|
|
88
|
+
vi.mocked(authorize).mockResolvedValueOnce(true)
|
|
89
|
+
vi.mocked(isEditorialModeEnabled).mockResolvedValueOnce(true)
|
|
90
|
+
|
|
91
|
+
const response = await fetchLinkInjections('https://some-url', 'some-html', { hash: 'some-hash' })
|
|
92
|
+
|
|
93
|
+
expect(response).toBe('Some response')
|
|
94
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
95
|
+
expect.stringContaining('editorial_mode_enabled=true'),
|
|
96
|
+
expect.any(Object),
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('pollLinkInjections', () => {
|
|
102
|
+
it('Should poll endpoint while results are not yet ready', async () => {
|
|
103
|
+
fakeFetch({ response: { injections_ready: false } })
|
|
104
|
+
|
|
105
|
+
pollLinkInjections('https://some-url', 'some-html', 500)
|
|
106
|
+
|
|
107
|
+
expect(global.fetch).toHaveBeenCalled()
|
|
108
|
+
|
|
109
|
+
await new Promise(res => setTimeout(res, 600)) // Wait for a polling
|
|
110
|
+
expect(global.fetch).toHaveBeenCalledTimes(2)
|
|
111
|
+
|
|
112
|
+
await new Promise(res => setTimeout(res, 600)) // Wait for a polling
|
|
113
|
+
expect(global.fetch).toHaveBeenCalledTimes(3)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('Should stop polling if replacements are ready', async () => {
|
|
117
|
+
fakeFetch({ response: { injections_ready: true, link_injections: [{ title: 'value' }] } })
|
|
118
|
+
|
|
119
|
+
const result = await pollLinkInjections('https://some-url', 'some-html', 500)
|
|
120
|
+
|
|
121
|
+
expect(global.fetch).toHaveBeenCalled()
|
|
122
|
+
expect(result).toEqual([{ title: 'value', key: expect.any(String) }])
|
|
123
|
+
|
|
124
|
+
await new Promise(res => setTimeout(res, 1000)) // Wait for a potential poll
|
|
125
|
+
expect(global.fetch).toHaveBeenCalledTimes(1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('Should stop polling if max limit was exceeded', async () => {
|
|
129
|
+
fakeFetch({ response: { injections_ready: false } })
|
|
130
|
+
|
|
131
|
+
await pollLinkInjections('https://some-url', 'some-html', 200, 2)
|
|
132
|
+
|
|
133
|
+
await new Promise(res => setTimeout(res, 1000)) // Wait for a potential poll
|
|
134
|
+
expect(global.fetch).toHaveBeenCalledTimes(2)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('Should continue polling if endpoint throws error', async () => {
|
|
138
|
+
fakeFetch({ ok: false })
|
|
139
|
+
|
|
140
|
+
pollLinkInjections('https://some-url', 'some-html', 500)
|
|
141
|
+
|
|
142
|
+
expect(global.fetch).toHaveBeenCalled()
|
|
143
|
+
|
|
144
|
+
await new Promise(res => setTimeout(res, 600)) // Wait for a polling
|
|
145
|
+
expect(global.fetch).toHaveBeenCalledTimes(2)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('Should return empty array if link_injections are null', async () => {
|
|
149
|
+
fakeFetch({ response: { injections_ready: true, link_injections: null } })
|
|
150
|
+
|
|
151
|
+
const result = await pollLinkInjections('https://some-url', 'some-html')
|
|
152
|
+
expect(result).toEqual([])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('Should return empty array despite actual results if injections_enabled is false', async () => {
|
|
156
|
+
fakeFetch({ response: { injections_enabled: false, link_injections: [{ title: 'value' }] } })
|
|
157
|
+
|
|
158
|
+
const result = await pollLinkInjections('https://some-url', 'some-html')
|
|
159
|
+
expect(result).toEqual([])
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getLargestValueInArray } from '$lib/array'
|
|
3
|
+
|
|
4
|
+
describe('array.js', () => {
|
|
5
|
+
describe('getLargestValueInArray', () => {
|
|
6
|
+
it('Should return the large given value in an array', () => {
|
|
7
|
+
expect(getLargestValueInArray([0, 12, 6, 8])).toBe(12)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('Should return 0 when array has no length', () => {
|
|
11
|
+
expect(getLargestValueInArray([])).toBe(0)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { authorize, getAuthToken, isEditorialModeEnabled } from '$lib/auth'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {Object} [options]
|
|
6
|
+
* @param {any} [options.response]
|
|
7
|
+
* @param {number} [options.status]
|
|
8
|
+
* @param {boolean} [options.ok]
|
|
9
|
+
*/
|
|
10
|
+
function fakeFetch({ response = '', status = 200, ok = true } = {}) {
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
global.fetch = vi.fn(() =>
|
|
13
|
+
Promise.resolve({
|
|
14
|
+
ok,
|
|
15
|
+
status,
|
|
16
|
+
json: () => response,
|
|
17
|
+
}),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('$lib/auth', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.resetAllMocks()
|
|
24
|
+
fakeFetch()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('authorize', () => {
|
|
28
|
+
it('Should authorize correctly with url token', async () => {
|
|
29
|
+
const authorized = await authorize('https://example.com/some-path?articleReplacementEditToken=some-token')
|
|
30
|
+
|
|
31
|
+
expect(authorized).toBeTruthy()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('Should authorize correctly with editorial token', async () => {
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
window.PlayPilotLinkInjections = { token: 'some-api-token', editorial_token: 'some-token' }
|
|
37
|
+
const authorized = await authorize('https://example.com/some-path')
|
|
38
|
+
|
|
39
|
+
expect(authorized).toBeTruthy()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('Should not authorize if required url param is not present', async () => {
|
|
43
|
+
const authorized = await authorize('https://example.com/some-path')
|
|
44
|
+
|
|
45
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
46
|
+
expect(authorized).not.toBeTruthy()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('Should authorize with cookie if previously authorized with url param', async () => {
|
|
50
|
+
let authorized = await authorize('https://example.com/some-path?articleReplacementEditToken=some-token')
|
|
51
|
+
expect(authorized).toBeTruthy()
|
|
52
|
+
|
|
53
|
+
authorized = await authorize('https://example.com/some-path')
|
|
54
|
+
expect(authorized).toBeTruthy()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('Should authorize with cookie if previously authorized with url param and no url is given', async () => {
|
|
58
|
+
let authorized = await authorize('https://example.com/some-path?articleReplacementEditToken=some-token')
|
|
59
|
+
expect(authorized).toBeTruthy()
|
|
60
|
+
|
|
61
|
+
authorized = await authorize()
|
|
62
|
+
expect(authorized).toBeTruthy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('Should not authorize is fetch response was negative', async () => {
|
|
66
|
+
fakeFetch({ response: '', ok: false, status: 403 })
|
|
67
|
+
let authorized = await authorize('https://example.com/some-path?articleReplacementEditToken=some-token')
|
|
68
|
+
|
|
69
|
+
expect(authorized).not.toBeTruthy()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('getAuthToken', () => {
|
|
74
|
+
it('Should return true if token is set in window object', () => {
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
window.PlayPilotLinkInjections = { editorial_token: 'some-token' }
|
|
77
|
+
|
|
78
|
+
expect(getAuthToken('https://some-url.com/')).toBeTruthy()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('Should return true if token is set in url', () => {
|
|
82
|
+
expect(getAuthToken('https://some-url.com/?articleReplacementEditToken=some-token')).toBeTruthy()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('Should return true if token is set as cookie', () => {
|
|
86
|
+
document.cookie = 'EncryptedToken=some-token'
|
|
87
|
+
expect(getAuthToken('https://some-url.com/?articleReplacementEditToken=some-token')).toBeTruthy()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('Should return false if no valid token is given', () => {
|
|
91
|
+
expect(getAuthToken('https://some-url.com/')).not.toBeTruthy()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('isEditorialModeEnabled', () => {
|
|
96
|
+
it('Should return true if both search token and window token are given', async () => {
|
|
97
|
+
window.location.search = '?playpilot-editorial-mode=false'
|
|
98
|
+
expect(isEditorialModeEnabled()).toBe(false)
|
|
99
|
+
|
|
100
|
+
window.location.search = '?other-param-mode=true'
|
|
101
|
+
expect(isEditorialModeEnabled()).toBe(false)
|
|
102
|
+
|
|
103
|
+
window.location.search = '?playpilot-editorial-mode=true'
|
|
104
|
+
expect(isEditorialModeEnabled()).toBe(true)
|
|
105
|
+
|
|
106
|
+
window.location.search = ''
|
|
107
|
+
expect(isEditorialModeEnabled()).toBe(false)
|
|
108
|
+
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
window.PlayPilotLinkInjections = { editorial_token: 'some-token' }
|
|
111
|
+
window.location.search = ''
|
|
112
|
+
expect(isEditorialModeEnabled()).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { stringToHash } from '$lib/hash'
|
|
3
|
+
|
|
4
|
+
describe('$lib/hash', () => {
|
|
5
|
+
describe('stringToHash', () => {
|
|
6
|
+
it('Should return the same hash for the same string', () => {
|
|
7
|
+
expect(stringToHash('Some string')).toBe(stringToHash('Some string'))
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('Should return different hashes for different strings', () => {
|
|
11
|
+
expect(stringToHash('Some')).not.toBe(stringToHash('String'))
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('Should handle an empty string correctly', () => {
|
|
15
|
+
const result = stringToHash('')
|
|
16
|
+
expect(result).toBe('0')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('Should be case-sensitive', () => {
|
|
20
|
+
expect(stringToHash('Some string')).not.toBe(stringToHash('some string'))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('Should handle long strings without throwing errors', () => {
|
|
24
|
+
const longString = 'a'.repeat(10000)
|
|
25
|
+
expect(() => stringToHash(longString)).not.toThrow()
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { encodeHtmlEntities, decodeHtmlEntities } from '$lib/html'
|
|
3
|
+
|
|
4
|
+
describe('html.js', () => {
|
|
5
|
+
describe('encodeHtmlEntities', () => {
|
|
6
|
+
it('Should encode given characters', () => {
|
|
7
|
+
expect(encodeHtmlEntities('Some & word')).toBe('Some & word')
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('decodeHtmlEntities', () => {
|
|
12
|
+
it('Should deencode given characters', () => {
|
|
13
|
+
expect(decodeHtmlEntities('Some & word')).toBe('Some & word')
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
})
|