@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,754 @@
1
+ import { fireEvent } from '@testing-library/svelte'
2
+ import { describe, expect, it, vi, beforeEach } from 'vitest'
3
+
4
+ import { injectLinksInDocument, clearLinkInjections, clearLinkInjection, getLinkInjectionElements, insertAfterArticlePlaylinks, getLinkInjectionsParentElement, sortLinkInjectionsByRange } from '$lib/linkInjection'
5
+ import { title } from '$lib/fakeData'
6
+ import { mount, unmount } from 'svelte'
7
+
8
+ vi.mock('svelte', () => ({
9
+ mount: vi.fn(),
10
+ unmount: vi.fn(),
11
+ }))
12
+
13
+ function mockMatchMedia(matches = false) {
14
+ Object.defineProperty(window, 'matchMedia', {
15
+ writable: true,
16
+ value: vi.fn().mockImplementation((query) => ({
17
+ matches,
18
+ media: query,
19
+ })),
20
+ })
21
+ }
22
+
23
+ describe('linkInjection.js', () => {
24
+ beforeEach(() => {
25
+ vi.resetAllMocks()
26
+ mockMatchMedia()
27
+
28
+ // @ts-ignore
29
+ window.PlayPilotLinkInjections = {}
30
+ })
31
+
32
+ describe('injectLinksInDocument', () => {
33
+ it('Should replace given words with injections', () => {
34
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
35
+
36
+ const elements = Array.from(document.querySelectorAll('p'))
37
+
38
+ const linkInjections = [{
39
+ sid: '1',
40
+ title: 'an injection',
41
+ sentence: 'This is a sentence with an injection.',
42
+ playpilot_url: 'https://some-link.com/',
43
+ poster: 'some-poster',
44
+ key: 'some-key',
45
+ }]
46
+
47
+ injectLinksInDocument(elements, linkInjections, () => null)
48
+
49
+ const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
50
+
51
+ expect(link.innerText).toBe('an injection')
52
+ expect(link.href).toBe('https://some-link.com/')
53
+ })
54
+
55
+ it('Should replace given words as expected when more than 1 injection per sentence is present', () => {
56
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
57
+
58
+ const elements = Array.from(document.body.querySelectorAll('p'))
59
+
60
+ const linkInjections = [{
61
+ sid: '1',
62
+ title: 'a sentence',
63
+ sentence: 'This is a sentence with an injection.',
64
+ playpilot_url: 'https://some-link.com/',
65
+ poster: 'some-poster',
66
+ key: 'some-key-1',
67
+ title_details: title,
68
+ }, {
69
+ sid: '2',
70
+ title: 'an injection',
71
+ sentence: 'This is a sentence with an injection.',
72
+ playpilot_url: 'https://some-link.com/',
73
+ poster: 'some-poster',
74
+ key: 'some-key-1',
75
+ title_details: title,
76
+ }]
77
+
78
+ injectLinksInDocument(elements, linkInjections, () => null)
79
+
80
+ const links = /** @type {HTMLAnchorElement[]} */ (Array.from(document.querySelectorAll('a')))
81
+
82
+ expect(links[0].innerText).toBe('a sentence')
83
+ expect(links[0].href).toBe('https://some-link.com/')
84
+ expect(links[0].dataset.playpilotOriginalTitle).toBe(title.original_title)
85
+
86
+ expect(links[1].innerText).toBe('an injection')
87
+ expect(links[1].href).toBe('https://some-link.com/')
88
+ expect(links[1].dataset.playpilotOriginalTitle).toBe(title.original_title)
89
+ })
90
+
91
+ it('Should ignore injections that are marked as inactive', () => {
92
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
93
+
94
+ const elements = Array.from(document.body.querySelectorAll('p'))
95
+
96
+ const linkInjections = [{
97
+ sid: '1',
98
+ title: 'an injection',
99
+ sentence: 'This is a sentence with an injection.',
100
+ playpilot_url: '',
101
+ poster: 'some-poster',
102
+ key: 'some-key',
103
+ inactive: true,
104
+ }]
105
+
106
+ injectLinksInDocument(elements, linkInjections, () => null)
107
+
108
+ expect(document.body.innerHTML).toBe('<p>This is a sentence with an injection.</p>')
109
+ })
110
+
111
+ it('Should keep existing HTML elements intact', () => {
112
+ document.body.innerHTML = '<p>This is a <button>sentence</button> with an injection.</p>'
113
+
114
+ const elements = Array.from(document.body.querySelectorAll('p'))
115
+
116
+ const linkInjections = [{
117
+ sid: '1',
118
+ title: 'an injection',
119
+ sentence: 'This is a sentence with an injection.',
120
+ playpilot_url: '',
121
+ poster: 'some-poster',
122
+ key: 'some-key',
123
+ }]
124
+
125
+ injectLinksInDocument(elements, linkInjections, () => null)
126
+
127
+ const button = /** @type {HTMLButtonElement} */ (document.querySelector('button'))
128
+
129
+ expect(button.innerText).toBe('sentence')
130
+ })
131
+
132
+ it('Should ignore elements that were not selected', () => {
133
+ document.body.innerHTML = '<h1>I am a title</h1><p>This is a sentence with an injection.</p>'
134
+
135
+ const elements = Array.from(document.body.querySelectorAll('p'))
136
+
137
+ const linkInjections = [{
138
+ sid: '1',
139
+ title: 'a title',
140
+ sentence: 'I am a title',
141
+ playpilot_url: '',
142
+ poster: 'some-poster',
143
+ key: 'some-key',
144
+ }]
145
+
146
+ injectLinksInDocument(elements, linkInjections, () => null)
147
+
148
+ expect(document.body.innerHTML.includes('I am a title')).toBeTruthy()
149
+ })
150
+
151
+ it('Should ignore partial matches due to interfering elements', () => {
152
+ document.body.innerHTML = '<p>This is a sentence with <strong>an</strong> injection.</p>'
153
+
154
+ const elements = Array.from(document.body.querySelectorAll('p'))
155
+
156
+ const linkInjections = [{
157
+ sid: '1',
158
+ title: 'an injection',
159
+ sentence: 'This is a sentence with an injection.',
160
+ playpilot_url: '',
161
+ poster: 'some-poster',
162
+ key: 'some-key',
163
+ }]
164
+
165
+ injectLinksInDocument(elements, linkInjections, () => null)
166
+
167
+ expect(document.body.innerHTML.includes('an injection')).not.toBeTruthy()
168
+ expect(document.body.innerHTML.includes('an')).toBeTruthy()
169
+ })
170
+
171
+ it('Should ignore matches due to parent links', () => {
172
+ document.body.innerHTML = '<p>This is <a>a sentence</a> with an injection.</p>'
173
+
174
+ const elements = Array.from(document.body.querySelectorAll('p'))
175
+
176
+ const linkInjections = [{
177
+ sid: '1',
178
+ title: 'a sentence',
179
+ sentence: 'This is a sentence with an injection.',
180
+ playpilot_url: 'https://some-link.com/',
181
+ poster: 'some-poster',
182
+ key: 'some-key-1',
183
+ }]
184
+
185
+ injectLinksInDocument(elements, linkInjections, () => null)
186
+
187
+ expect(document.querySelector('[data-playpilot-injection-key]')).not.toBeTruthy()
188
+ })
189
+
190
+ it('Should ignore matches inside of attributes inside of links', () => {
191
+ document.body.innerHTML = '<p>This is <a data-thing="some word">a sentence</a> with an injection.</p>'
192
+
193
+ const elements = Array.from(document.body.querySelectorAll('p'))
194
+
195
+ const linkInjections = [{
196
+ sid: '1',
197
+ title: 'some word',
198
+ sentence: 'This is a sentence with an injection.',
199
+ playpilot_url: 'https://some-link.com/',
200
+ poster: 'some-poster',
201
+ key: 'some-key-1',
202
+ }]
203
+
204
+ injectLinksInDocument(elements, linkInjections, () => null)
205
+
206
+ expect(document.querySelector('[data-playpilot-injection-key]')).not.toBeTruthy()
207
+ })
208
+
209
+ it('Should skip links and replace other words instead', () => {
210
+ document.body.innerHTML = '<p>This is <a>a word</a> with a word.</p>'
211
+
212
+ const elements = Array.from(document.body.querySelectorAll('p'))
213
+
214
+ const linkInjections = [{
215
+ sid: '1',
216
+ title: 'a word',
217
+ sentence: 'This is a word with a word.',
218
+ playpilot_url: 'https://some-link.com/',
219
+ poster: 'some-poster',
220
+ key: 'some-key-1',
221
+ }]
222
+
223
+ injectLinksInDocument(elements, linkInjections, () => null)
224
+
225
+ expect(document.querySelectorAll('a')).toHaveLength(2)
226
+ })
227
+
228
+ it('Should inject properly when sentence contains element that are not part of the match', () => {
229
+ document.body.innerHTML = '<p>This is <a>a sentence</a> with an injection.</p>'
230
+
231
+ const elements = Array.from(document.body.querySelectorAll('p'))
232
+
233
+ const linkInjections = [{
234
+ sid: '1',
235
+ title: 'an injection',
236
+ sentence: 'This is a sentence with an injection.',
237
+ playpilot_url: 'https://some-link.com/',
238
+ poster: 'some-poster',
239
+ key: 'some-key-1',
240
+ }]
241
+
242
+ injectLinksInDocument(elements, linkInjections, () => null)
243
+
244
+ expect(document.querySelector('[data-playpilot-injection-key]')).toBeTruthy()
245
+ })
246
+
247
+ it('Should leave the text intact if no injections were found', () => {
248
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
249
+
250
+ const elements = Array.from(document.body.querySelectorAll('p'))
251
+
252
+ /** @type {LinkInjection[]} */
253
+ const linkInjections = []
254
+
255
+ injectLinksInDocument(elements, linkInjections, () => null)
256
+
257
+ expect(document.body.innerHTML.includes('This is a sentence with an injection.')).toBeTruthy()
258
+ })
259
+
260
+ it('Should handle not receiving elements or injections', () => {
261
+ /** @type {HTMLElement[]} */
262
+ const elements = []
263
+
264
+ /** @type {LinkInjection[]} */
265
+ const linkInjections = []
266
+
267
+ expect(() => injectLinksInDocument(elements, linkInjections, () => null)).not.toThrow()
268
+ })
269
+
270
+ it('Should fire given onclick function when clicked', async () => {
271
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
272
+
273
+ const elements = Array.from(document.body.querySelectorAll('p'))
274
+
275
+ const linkInjections = [{
276
+ sid: '1',
277
+ title: 'an injection',
278
+ sentence: 'This is a sentence with an injection.',
279
+ playpilot_url: 'https://some-link.com/',
280
+ poster: 'some-poster',
281
+ key: 'some-key',
282
+ }]
283
+
284
+ const mock = vi.fn()
285
+ injectLinksInDocument(elements, linkInjections, mock)
286
+
287
+ const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
288
+ await fireEvent.click(link)
289
+
290
+ expect(mock).toHaveBeenCalledWith(linkInjections[0])
291
+ })
292
+
293
+ it('Should fire given onclick function when clicked for each link in a sentence if more than 1 injection is present', async () => {
294
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
295
+
296
+ const elements = Array.from(document.body.querySelectorAll('p'))
297
+
298
+ const linkInjections = [{
299
+ sid: '1',
300
+ title: 'a sentence',
301
+ sentence: 'This is a sentence with an injection.',
302
+ playpilot_url: 'https://some-link.com/',
303
+ poster: 'some-poster',
304
+ key: 'some-key-1',
305
+ }, {
306
+ sid: '2',
307
+ title: 'an injection',
308
+ sentence: 'This is a sentence with an injection.',
309
+ playpilot_url: 'https://some-link.com/',
310
+ poster: 'some-poster',
311
+ key: 'some-key-2',
312
+ }]
313
+
314
+ const mock = vi.fn()
315
+ injectLinksInDocument(elements, linkInjections, mock)
316
+
317
+ const links = document.querySelectorAll('a')
318
+ await fireEvent.click(links[0])
319
+ await fireEvent.click(links[1])
320
+
321
+ expect(mock).toHaveBeenCalledTimes(2)
322
+ })
323
+
324
+ it('Should not fire given onclick function when clicked with modifier keys or not left click', async () => {
325
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
326
+
327
+ const elements = Array.from(document.body.querySelectorAll('p'))
328
+
329
+ const linkInjections = [{
330
+ sid: '1',
331
+ title: 'an injection',
332
+ sentence: 'This is a sentence with an injection.',
333
+ playpilot_url: 'https://some-link.com/',
334
+ poster: 'some-poster',
335
+ key: 'some-key',
336
+ }]
337
+
338
+ const mock = vi.fn()
339
+ injectLinksInDocument(elements, linkInjections, mock)
340
+
341
+ const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
342
+
343
+ await fireEvent.click(link, { ctrlKey: true })
344
+ await fireEvent.click(link, { metaKey: true })
345
+ await fireEvent.click(link, { button: 1 })
346
+ await fireEvent.click(link, { button: 2 })
347
+
348
+ expect(mock).not.toHaveBeenCalled()
349
+ })
350
+
351
+ it('Should return array with failed injections marked as such', () => {
352
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
353
+
354
+ const elements = Array.from(document.body.querySelectorAll('p'))
355
+
356
+ const linkInjections = [{
357
+ sid: '1',
358
+ title: 'a sentence',
359
+ sentence: 'This is a sentence with an injection.',
360
+ playpilot_url: 'https://some-link.com/',
361
+ poster: 'some-poster',
362
+ key: 'some-key-1',
363
+ }, {
364
+ sid: '2',
365
+ title: 'not an injection',
366
+ sentence: 'This is a sentence with an injection.',
367
+ playpilot_url: 'https://some-link.com/',
368
+ poster: 'some-poster',
369
+ key: 'some-key-2',
370
+ }]
371
+
372
+ const results = injectLinksInDocument(elements, linkInjections, () => null)
373
+
374
+ expect(results[1].failed).toBe(true)
375
+ expect(results[0].failed).toBe(false)
376
+ })
377
+
378
+ it('Should mount popover component when link is hovered and unmount when link is blurred', async () => {
379
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
380
+
381
+ const elements = Array.from(document.body.querySelectorAll('p'))
382
+
383
+ const linkInjections = [{
384
+ sid: '1',
385
+ title: 'an injection',
386
+ sentence: 'This is a sentence with an injection.',
387
+ playpilot_url: 'https://some-link.com/',
388
+ poster: 'some-poster',
389
+ key: 'some-key',
390
+ }]
391
+
392
+ const mock = vi.fn()
393
+ injectLinksInDocument(elements, linkInjections, mock)
394
+
395
+ const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
396
+
397
+ await fireEvent.mouseEnter(link)
398
+ expect(mount).toHaveBeenCalled()
399
+
400
+ await fireEvent.mouseLeave(link)
401
+ expect(unmount).toHaveBeenCalled()
402
+ })
403
+
404
+ it('Should inject links of the same phrase when multiple are present', () => {
405
+ document.body.innerHTML = '<p>This is a sentence with an injection and another injection</p>'
406
+
407
+ const elements = Array.from(document.querySelectorAll('p'))
408
+
409
+ const linkInjections = [{
410
+ sid: '1',
411
+ title: 'injection',
412
+ sentence: 'This is a sentence with an injection and another injection',
413
+ playpilot_url: 'https://some-link.com/',
414
+ poster: 'some-poster',
415
+ key: 'some-key',
416
+ }, {
417
+ sid: '2',
418
+ title: 'injection',
419
+ sentence: 'This is a sentence with an injection and another injection',
420
+ playpilot_url: 'https://some-link-2.com/',
421
+ poster: 'some-poster',
422
+ key: 'some-key-2',
423
+ }]
424
+
425
+ injectLinksInDocument(elements, linkInjections, () => null)
426
+
427
+ const links = /** @type {HTMLAnchorElement[]} */ (Array.from(document.querySelectorAll('a')))
428
+
429
+ expect(links).toHaveLength(2)
430
+ expect(links[0].dataset.playpilotInjectionKey).toBe('some-key')
431
+ expect(links[1].dataset.playpilotInjectionKey).toBe('some-key-2')
432
+ })
433
+
434
+ it('Should not inject injections with in_text set to false', () => {
435
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
436
+
437
+ const elements = Array.from(document.querySelectorAll('p'))
438
+
439
+ const linkInjections = [{
440
+ sid: '1',
441
+ title: 'an injection',
442
+ sentence: 'This is a sentence with an injection.',
443
+ playpilot_url: 'https://some-link.com/',
444
+ poster: 'some-poster',
445
+ key: 'some-key',
446
+ in_text: false,
447
+ }]
448
+
449
+ injectLinksInDocument(elements, linkInjections, () => null)
450
+
451
+ expect(document.querySelector('a')).not.toBeTruthy()
452
+ })
453
+ })
454
+
455
+ it('Should not mount popover if user uses touch', async () => {
456
+ mockMatchMedia(true)
457
+
458
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
459
+
460
+ const elements = Array.from(document.body.querySelectorAll('p'))
461
+
462
+ const linkInjections = [{
463
+ sid: '1',
464
+ title: 'an injection',
465
+ sentence: 'This is a sentence with an injection.',
466
+ playpilot_url: 'https://some-link.com/',
467
+ poster: 'some-poster',
468
+ key: 'some-key',
469
+ }]
470
+
471
+ const mock = vi.fn()
472
+ injectLinksInDocument(elements, linkInjections, mock)
473
+
474
+ const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
475
+
476
+ await fireEvent.mouseEnter(link)
477
+ expect(mount).not.toHaveBeenCalled()
478
+ })
479
+
480
+ describe('clearLinkInjections', () => {
481
+ it('Should remove injected links from the page', () => {
482
+ document.body.innerHTML = '<p><a data-playpilot-injection-key>Some link</a></p>'
483
+ clearLinkInjections()
484
+
485
+ expect(document.body.innerHTML).toBe('<p>Some link</p>')
486
+ })
487
+
488
+ it('Should leave non-injected links intact', () => {
489
+ document.body.innerHTML = '<p><a>Some link</a></p>'
490
+ clearLinkInjections()
491
+
492
+ expect(document.body.innerHTML).toBe('<p><a>Some link</a></p>')
493
+ })
494
+ })
495
+
496
+ describe('clearLinkInjection', () => {
497
+ it('Should remove injected link from the page', () => {
498
+ document.body.innerHTML = '<p><a data-playpilot-injection-key="a">Some link</a></p>'
499
+ clearLinkInjection('a')
500
+
501
+ expect(document.body.innerHTML).toBe('<p>Some link</p>')
502
+ })
503
+
504
+ it('Should keep other keys intact', () => {
505
+ document.body.innerHTML = '<p><a data-playpilot-injection-key="b">Some link</a></p>'
506
+ clearLinkInjection('a')
507
+
508
+ expect(document.body.innerHTML).toBe('<p><a data-playpilot-injection-key="b">Some link</a></p>')
509
+ })
510
+ })
511
+
512
+ describe('getLinkInjectionElements', () => {
513
+ it('Should return a list of elements inside the given parent', () => {
514
+ document.body.innerHTML = '<section><p>Some paragraph</p><div>Some div</div></section>'
515
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
516
+
517
+ expect(getLinkInjectionElements(parent)).toHaveLength(2)
518
+ })
519
+
520
+ it('Should ignore elements without text content', () => {
521
+ document.body.innerHTML = '<section><p>Some paragraph</p><div></div></section>'
522
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
523
+
524
+ expect(getLinkInjectionElements(parent)).toHaveLength(1)
525
+ })
526
+
527
+ it('Should ignore child elements that were already included in parent text', () => {
528
+ document.body.innerHTML = '<section><p>Some paragraph <div>Some div</div></p></section>'
529
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
530
+
531
+ expect(getLinkInjectionElements(parent)).toHaveLength(1)
532
+ })
533
+
534
+ it('Should return separate elements inside a parent if the parent has no direct text', () => {
535
+ document.body.innerHTML = '<section><div>Some text</div> <div>Some div</div></section>'
536
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
537
+
538
+ expect(getLinkInjectionElements(parent)).toHaveLength(2)
539
+ })
540
+
541
+ it('Should return separate elements when text content is deep', () => {
542
+ document.body.innerHTML = '<section><div><div><main><p>Some <div>text<div></p><p>Some other text</p></main></div></div></section>'
543
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
544
+
545
+ expect(getLinkInjectionElements(parent)).toHaveLength(2)
546
+ })
547
+
548
+ it('Should return each item in a list separately', () => {
549
+ document.body.innerHTML = `<section>
550
+ <ul>
551
+ <li>Hey!</li>
552
+ <li>I</li>
553
+ <li>am</li>
554
+ <li>part</li>
555
+ <li>of</li>
556
+ <li>a</li>
557
+ <li>list.</li>
558
+ </ul>
559
+ </section>`
560
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
561
+
562
+ expect(getLinkInjectionElements(parent)).toHaveLength(7)
563
+ })
564
+
565
+ it('Should ignore links, buttons, script tags, style tags, iframes, and headers', () => {
566
+ document.body.innerHTML = `<section>
567
+ <h1>Some header</h1>
568
+ <h4>Some smaller header</h4>
569
+
570
+ <a>I am a link</a>
571
+ <button>And I am a button</button>
572
+
573
+ <script>I am a script</script>
574
+ <style>I am styling</style>
575
+ <iframe>I am an iframe</iframe>
576
+ <noscript>I am noscript</noscript>
577
+
578
+ <div>
579
+ <a>I am another link</a>
580
+ <p>And finally, some text</p>
581
+ </div>
582
+ </section>`
583
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
584
+
585
+ expect(getLinkInjectionElements(parent)).toHaveLength(1)
586
+ })
587
+
588
+ it('Should ignore links, buttons, and headers even when deeply nested', () => {
589
+ document.body.innerHTML = `<section>
590
+ <div>
591
+ <h2><button>Button inside header</button></h2>
592
+ <div>
593
+ <a><h3>Header inside link</h3></a>
594
+ <p>Some text</p>
595
+ </div>
596
+ </div>
597
+ </section>`
598
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
599
+
600
+ expect(getLinkInjectionElements(parent)).toHaveLength(1)
601
+ })
602
+
603
+ it('Should return an empty array if parent is empty', () => {
604
+ document.body.innerHTML = '<section></section>'
605
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
606
+
607
+ expect(getLinkInjectionElements(parent)).toEqual([])
608
+ })
609
+
610
+ it('Should ignore empty elements', () => {
611
+ document.body.innerHTML = '<section><div>&nbsp;</div><div> </div><div>\n</div><div>Some text</div></section>'
612
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
613
+
614
+ expect(getLinkInjectionElements(parent)).toHaveLength(1)
615
+ })
616
+
617
+ it('Should return elements in the same order they were given', () => {
618
+ document.body.innerHTML = `<section>
619
+ <div>
620
+ <p>Button inside header</p>
621
+ <div>
622
+ <strong>Header inside link</strong>
623
+ <div>Some text</div>
624
+ </div>
625
+ </div>
626
+ </section>`
627
+
628
+ const parent = /** @type {HTMLElement} */ (document.querySelector('section'))
629
+ const elements = getLinkInjectionElements(parent)
630
+
631
+ expect(elements[0].nodeName === 'P')
632
+ expect(elements[1].nodeName === 'STRONG')
633
+ expect(elements[2].nodeName === 'DIV')
634
+ })
635
+ })
636
+
637
+ describe('getLinkInjectionsParentElement', () => {
638
+ it('Should return based on their given importance if no selector is given', () => {
639
+ document.body.innerHTML = '<main><article></article></main>'
640
+ expect(getLinkInjectionsParentElement().nodeName).toBe('ARTICLE')
641
+
642
+ document.body.innerHTML = '<main><div></div></main>'
643
+ expect(getLinkInjectionsParentElement().nodeName).toBe('MAIN')
644
+
645
+ document.body.innerHTML = '<div></div>'
646
+ expect(getLinkInjectionsParentElement().nodeName).toBe('BODY')
647
+ })
648
+
649
+ it('Should return the element matching the given selector', () => {
650
+ // @ts-ignore
651
+ window.PlayPilotLinkInjections.selector = 'section'
652
+ document.body.innerHTML = '<div><section></section></div>'
653
+ expect(getLinkInjectionsParentElement().nodeName).toBe('SECTION')
654
+ })
655
+
656
+ it('Should fall back to fallback elements if selector was given but matching element was not found', () => {
657
+ // @ts-ignore
658
+ window.PlayPilotLinkInjections.selector = 'section'
659
+ document.body.innerHTML = '<div><article></article></div>'
660
+ expect(getLinkInjectionsParentElement().nodeName).toBe('ARTICLE')
661
+ })
662
+
663
+ it('Should escape selectors with :', () => {
664
+ // @ts-ignore
665
+ window.PlayPilotLinkInjections.selector = '.some:class'
666
+ document.body.innerHTML = '<div><section class="some:class"></section></div>'
667
+ expect(getLinkInjectionsParentElement().nodeName).toBe('SECTION')
668
+ })
669
+ })
670
+
671
+ describe('insertAfterArticlePlaylinks', () => {
672
+ it('Should insert component after given elements', () => {
673
+ document.body.innerHTML = `<section>
674
+ <div>Some text</div>
675
+ <div>Some other text</div>
676
+ </section>
677
+ `
678
+
679
+ const linkInjections = [{
680
+ sid: '1',
681
+ title: 'an injection',
682
+ sentence: 'This is a sentence with an injection.',
683
+ playpilot_url: 'https://some-link.com/',
684
+ poster: 'some-poster',
685
+ key: 'some-key',
686
+ }]
687
+
688
+ const elements = getLinkInjectionElements(document.body)
689
+ insertAfterArticlePlaylinks(elements, linkInjections, () => null)
690
+
691
+ expect(mount).toHaveBeenCalled()
692
+ expect(document.querySelector('[data-playpilot-after-article-playlinks]')).toBeTruthy()
693
+ })
694
+
695
+ it('Should not insert component if no linkInjections were given', () => {
696
+ document.body.innerHTML = `<section>
697
+ <div>Some text</div>
698
+ <div>Some other text</div>
699
+ </section>
700
+ `
701
+
702
+ const elements = getLinkInjectionElements(document.body)
703
+ insertAfterArticlePlaylinks(elements, [], () => null)
704
+
705
+ expect(mount).not.toHaveBeenCalled()
706
+ expect(document.querySelector('[data-playpilot-after-article-playlinks]')).not.toBeTruthy()
707
+ })
708
+ })
709
+
710
+ describe('sortLinkInjectionsByRange', () => {
711
+ const injections = [
712
+ { key: 'a', sid: '', title: '', playpilot_url: '', sentence: '' },
713
+ { key: 'b', sid: '', title: '', playpilot_url: '', sentence: '' },
714
+ { key: 'c', sid: '', title: '', playpilot_url: '', sentence: '' },
715
+ ]
716
+
717
+ it('Should sort link injections by their element index', () => {
718
+ const ranges = {
719
+ a: { elementIndex: 1, from: 0, to: 0 },
720
+ b: { elementIndex: 0, from: 0, to: 0 },
721
+ }
722
+
723
+ const sortedInjections = sortLinkInjectionsByRange(injections, ranges)
724
+
725
+ expect(sortedInjections[0].key).toBe('b')
726
+ expect(sortedInjections[1].key).toBe('a')
727
+ })
728
+
729
+ it('Should sort link injections by their from value', () => {
730
+ const ranges = {
731
+ a: { elementIndex: 0, from: 2, to: 0 },
732
+ b: { elementIndex: 0, from: 1, to: 0 },
733
+ }
734
+
735
+ const sortedInjections = sortLinkInjectionsByRange(injections, ranges)
736
+
737
+ expect(sortedInjections[0].key).toBe('b')
738
+ expect(sortedInjections[1].key).toBe('a')
739
+ })
740
+
741
+ it('Should place elements without range last', () => {
742
+ const ranges = {
743
+ b: { elementIndex: 0, from: 0, to: 0 },
744
+ c: { elementIndex: 0, from: 1, to: 0 },
745
+ }
746
+
747
+ const sortedInjections = sortLinkInjectionsByRange(injections, ranges)
748
+
749
+ expect(sortedInjections[0].key).toBe('b')
750
+ expect(sortedInjections[1].key).toBe('c')
751
+ expect(sortedInjections[2].key).toBe('a')
752
+ })
753
+ })
754
+ })