@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.
Files changed (101) hide show
  1. package/.github/workflows/tests.yml +22 -0
  2. package/.prettierignore +4 -0
  3. package/.prettierrc +16 -0
  4. package/README.md +38 -0
  5. package/dist/link-injections.js +7 -0
  6. package/eslint.config.js +33 -0
  7. package/index.html +11 -0
  8. package/jsconfig.json +19 -0
  9. package/package.json +35 -0
  10. package/src/app.d.ts +13 -0
  11. package/src/app.html +12 -0
  12. package/src/demo.spec.js +7 -0
  13. package/src/lib/api.js +160 -0
  14. package/src/lib/array.js +15 -0
  15. package/src/lib/auth.js +84 -0
  16. package/src/lib/constants.js +2 -0
  17. package/src/lib/enums/TrackingEvent.js +15 -0
  18. package/src/lib/fakeData.js +140 -0
  19. package/src/lib/genres.json +420 -0
  20. package/src/lib/global.css +37 -0
  21. package/src/lib/hash.js +15 -0
  22. package/src/lib/html.js +21 -0
  23. package/src/lib/index.js +1 -0
  24. package/src/lib/linkInjection.js +275 -0
  25. package/src/lib/search.js +24 -0
  26. package/src/lib/text.js +61 -0
  27. package/src/lib/tracking.js +32 -0
  28. package/src/lib/variables.css +16 -0
  29. package/src/main.js +45 -0
  30. package/src/routes/+layout.svelte +54 -0
  31. package/src/routes/+page.svelte +96 -0
  32. package/src/routes/components/AfterArticlePlaylinks.svelte +90 -0
  33. package/src/routes/components/ContextMenu.svelte +67 -0
  34. package/src/routes/components/Description.svelte +47 -0
  35. package/src/routes/components/Editorial/Alert.svelte +18 -0
  36. package/src/routes/components/Editorial/DragHandle.svelte +134 -0
  37. package/src/routes/components/Editorial/Editor.svelte +277 -0
  38. package/src/routes/components/Editorial/EditorItem.svelte +260 -0
  39. package/src/routes/components/Editorial/ManualInjection.svelte +192 -0
  40. package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +132 -0
  41. package/src/routes/components/Editorial/Search/TitleSearch.svelte +176 -0
  42. package/src/routes/components/Editorial/Switch.svelte +76 -0
  43. package/src/routes/components/Editorial/TextInput.svelte +29 -0
  44. package/src/routes/components/Genres.svelte +41 -0
  45. package/src/routes/components/Icons/IconAlign.svelte +12 -0
  46. package/src/routes/components/Icons/IconBack.svelte +3 -0
  47. package/src/routes/components/Icons/IconBookmark.svelte +3 -0
  48. package/src/routes/components/Icons/IconChevron.svelte +18 -0
  49. package/src/routes/components/Icons/IconClose.svelte +3 -0
  50. package/src/routes/components/Icons/IconContinue.svelte +3 -0
  51. package/src/routes/components/Icons/IconDots.svelte +5 -0
  52. package/src/routes/components/Icons/IconEnlarge.svelte +12 -0
  53. package/src/routes/components/Icons/IconIMDb.svelte +3 -0
  54. package/src/routes/components/Icons/IconNewTab.svelte +3 -0
  55. package/src/routes/components/Modal.svelte +106 -0
  56. package/src/routes/components/Participants.svelte +44 -0
  57. package/src/routes/components/Playlinks.svelte +155 -0
  58. package/src/routes/components/Popover.svelte +95 -0
  59. package/src/routes/components/RoundButton.svelte +38 -0
  60. package/src/routes/components/SkeletonText.svelte +33 -0
  61. package/src/routes/components/Title.svelte +180 -0
  62. package/src/routes/components/TitleModal.svelte +24 -0
  63. package/src/routes/components/TitlePopover.svelte +17 -0
  64. package/src/tests/helpers.js +18 -0
  65. package/src/tests/lib/api.test.js +162 -0
  66. package/src/tests/lib/array.test.js +14 -0
  67. package/src/tests/lib/auth.test.js +115 -0
  68. package/src/tests/lib/hash.test.js +28 -0
  69. package/src/tests/lib/html.test.js +16 -0
  70. package/src/tests/lib/linkInjection.test.js +754 -0
  71. package/src/tests/lib/search.test.js +42 -0
  72. package/src/tests/lib/text.test.js +94 -0
  73. package/src/tests/lib/tracking.test.js +71 -0
  74. package/src/tests/routes/+page.test.js +109 -0
  75. package/src/tests/routes/components/AfterArticlePlaylinks.test.js +115 -0
  76. package/src/tests/routes/components/ContextMenu.test.js +37 -0
  77. package/src/tests/routes/components/Description.test.js +58 -0
  78. package/src/tests/routes/components/Editorial/Alert.test.js +17 -0
  79. package/src/tests/routes/components/Editorial/DragHandle.test.js +55 -0
  80. package/src/tests/routes/components/Editorial/Editor.test.js +64 -0
  81. package/src/tests/routes/components/Editorial/EditorItem.test.js +142 -0
  82. package/src/tests/routes/components/Editorial/ManualInjection.test.js +114 -0
  83. package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +63 -0
  84. package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +58 -0
  85. package/src/tests/routes/components/Editorial/Switch.test.js +60 -0
  86. package/src/tests/routes/components/Editorial/TextInput.test.js +30 -0
  87. package/src/tests/routes/components/Genres.test.js +37 -0
  88. package/src/tests/routes/components/Modal.test.js +84 -0
  89. package/src/tests/routes/components/Participants.test.js +33 -0
  90. package/src/tests/routes/components/Playlinks.test.js +101 -0
  91. package/src/tests/routes/components/Popover.test.js +66 -0
  92. package/src/tests/routes/components/RoundButton.test.js +35 -0
  93. package/src/tests/routes/components/SkeletonText.test.js +12 -0
  94. package/src/tests/routes/components/Title.test.js +82 -0
  95. package/src/tests/routes/components/TitleModal.test.js +33 -0
  96. package/src/tests/routes/components/TitlePopover.test.js +23 -0
  97. package/src/tests/setup.js +53 -0
  98. package/src/typedefs.js +72 -0
  99. package/static/favicon.png +0 -0
  100. package/svelte.config.js +13 -0
  101. 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
+ })