@playpilot/tpi 3.4.0 → 3.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -11,9 +11,8 @@ import { playFallbackViewTransition } from './viewTransition'
11
11
  const keyDataAttribute = 'data-playpilot-injection-key'
12
12
  const keySelector = `[${keyDataAttribute}]`
13
13
 
14
- const activePopovers: Record<string, { injection: LinkInjection; component: object }> = {}
15
-
16
14
  let currentlyHoveredInjection: EventTarget | null = null
15
+ let activePopoverInsertedComponent: object | null = null
17
16
  let afterArticlePlaylinkInsertedComponent: object | null = null
18
17
  let activeModalInsertedComponent: object | null = null
19
18
 
@@ -32,7 +31,7 @@ export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElemen
32
31
  if (validElements.includes(element)) continue
33
32
 
34
33
  // Ignore links, buttons, and headers
35
- if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|FIGCAPTION|H[1-6])$/.test(element.tagName)) continue
34
+ if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|FIGCAPTION|TIME|H[1-6])$/.test(element.tagName)) continue
36
35
 
37
36
  // Check if this element has a direct text node
38
37
  const hasTextNode = Array.from(element.childNodes).some(
@@ -71,12 +70,11 @@ export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElemen
71
70
  }
72
71
 
73
72
  /**
74
- * Get the parent selector that will be used to find the link injections in.
75
- * This selector is passed when the script is initialized.
76
- * If no selector is passed a default is returned instead.
77
- */
73
+ * Get the parent selector that will be used to find the link injections in.
74
+ * This selector is passed through the api config, or when the script is initialized.
75
+ * If no selector is passed a default is returned instead.
76
+ */
78
77
  export function getLinkInjectionsParentElement(): HTMLElement {
79
- // @ts-ignore
80
78
  const selector = window.PlayPilotLinkInjections?.selector
81
79
 
82
80
  if (selector) {
@@ -241,11 +239,23 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
241
239
  event.preventDefault()
242
240
 
243
241
  playFallbackViewTransition(() => {
244
- destroyLinkPopover(injection, false)
242
+ destroyLinkPopover(false)
245
243
  openLinkModal(event, injection)
246
244
  }, window.innerWidth >= 600 && !window.matchMedia("(pointer: coarse)").matches)
247
245
  })
248
246
 
247
+ window.addEventListener('mousemove', (event) => {
248
+ if (!activePopoverInsertedComponent) return
249
+
250
+ const target = event.target as Element
251
+
252
+ // Mousemove is inside of popover or link that popover
253
+ if (target.hasAttribute('data-playpilot-title-popover') || target.closest('[data-playpilot-title-popover]') ||
254
+ target.hasAttribute(keyDataAttribute) || target.closest(keySelector)) return
255
+
256
+ destroyLinkPopover()
257
+ })
258
+
249
259
  const createdInjectionElements = Array.from(document.querySelectorAll(keySelector)) as HTMLElement[]
250
260
 
251
261
  // Open and close popover on mouseenter/mouseleave
@@ -257,7 +267,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
257
267
 
258
268
  // @ts-ignore
259
269
  injectionElement.addEventListener('mouseenter', (event) => openLinkPopover(event, injection))
260
- injectionElement.addEventListener('mouseleave', () => destroyLinkPopover(injection))
270
+ injectionElement.addEventListener('mouseleave', () => currentlyHoveredInjection = null)
261
271
  })
262
272
  }
263
273
 
@@ -291,6 +301,7 @@ function destroyLinkModal(outro: boolean = true): void {
291
301
  function openLinkPopover(event: MouseEvent, injection: LinkInjection): void {
292
302
  // Skip touch devices
293
303
  if (window.matchMedia('(pointer: coarse)').matches) return
304
+ if (activePopoverInsertedComponent) destroyLinkPopover()
294
305
 
295
306
  const target = event.currentTarget as Element
296
307
  currentlyHoveredInjection = target
@@ -298,26 +309,22 @@ function openLinkPopover(event: MouseEvent, injection: LinkInjection): void {
298
309
  // Only show if the link is hovered for more than 100ms. This is to prevent the popover from showing
299
310
  // when a user just happens to mouseover as they are moving their mouse about.
300
311
  setTimeout(() => {
301
- if (activePopovers[injection.key]) return // Popover for this link was already open and was called again... for some reason
302
312
  if (currentlyHoveredInjection !== target) return // User is no longer hovering this link
303
313
 
304
- const popover = mount(TitlePopover, { target, props: { title: injection.title_details! } })
305
-
306
- activePopovers[injection.key] = { injection, component: popover }
314
+ activePopoverInsertedComponent = mount(TitlePopover, { target: document.body, props: { event, title: injection.title_details! } })
307
315
  }, 100)
308
316
  }
309
317
 
310
318
  /**
311
319
  * Unmount the popover, removing it from the dom
312
320
  */
313
- function destroyLinkPopover(injection: LinkInjection, outro: boolean = true) {
314
- const popover = activePopovers[injection.key]
315
- currentlyHoveredInjection = null
321
+ function destroyLinkPopover(outro: boolean = true) {
322
+ if (!activePopoverInsertedComponent) return
316
323
 
317
- if (!popover) return
324
+ unmount(activePopoverInsertedComponent, { outro })
318
325
 
319
- unmount(popover.component, { outro })
320
- delete activePopovers[injection.key]
326
+ currentlyHoveredInjection = null
327
+ activePopoverInsertedComponent = null
321
328
  }
322
329
 
323
330
  /**
@@ -352,15 +359,12 @@ function clearAfterArticlePlaylinks(): void {
352
359
  * Clear link injections from the page
353
360
  */
354
361
  export function clearLinkInjections(): void {
355
- Object.values(activePopovers).forEach(popover => destroyLinkPopover(popover.injection))
356
-
357
362
  const elements = document.querySelectorAll(keySelector)
358
363
  elements.forEach((element) => element.outerHTML = element.textContent || '')
359
364
 
360
- Object.values(activePopovers).forEach(({ injection }) => destroyLinkPopover(injection, false))
361
-
362
365
  clearAfterArticlePlaylinks()
363
- destroyLinkModal()
366
+ destroyLinkModal(false)
367
+ destroyLinkPopover(false)
364
368
  }
365
369
 
366
370
  /**
@@ -1,5 +1,3 @@
1
- import { get } from "svelte/store"
2
- import { currentDomainSid, currentOrganizationSid } from "./stores/organization"
3
1
  import type { TitleData } from "./types/title"
4
2
  import { getFullUrlPath } from "./url"
5
3
 
@@ -24,8 +22,8 @@ export async function track(event: string, title: TitleData | null = null, paylo
24
22
  }
25
23
 
26
24
  payload.url = getFullUrlPath()
27
- payload.organization_sid = get(currentOrganizationSid)
28
- payload.domain_sid = get(currentDomainSid)
25
+ payload.organization_sid = window.PlayPilotLinkInjections?.organization_sid
26
+ payload.domain_sid = window.PlayPilotLinkInjections?.domain_sid
29
27
 
30
28
  fetch(baseUrl, {
31
29
  headers,
@@ -39,10 +37,10 @@ export async function track(event: string, title: TitleData | null = null, paylo
39
37
  }
40
38
 
41
39
  /**
42
- * Set the sid of the organization and domain to a store..
40
+ * Set the sid of the organization and domain to the window object.
43
41
  * These are saved for tracking purposes and currently serve no other function.
44
42
  */
45
43
  export function setTrackingSids({ organizationSid = null, domainSid = null }: { organizationSid?: string | null, domainSid?: string | null }): void {
46
- currentOrganizationSid.set(organizationSid || null)
47
- currentDomainSid.set(domainSid || null)
44
+ window.PlayPilotLinkInjections.organization_sid = organizationSid || null
45
+ window.PlayPilotLinkInjections.domain_sid = domainSid || null
48
46
  }
@@ -1,3 +1,5 @@
1
1
  export type ConfigResponse = {
2
2
  exclude_urls_pattern?: string
3
+ custom_style?: string
4
+ html_selector?: string
3
5
  }
@@ -7,6 +7,8 @@ declare global {
7
7
  after_article_selector: string
8
8
  after_article_insert_position: InsertPosition | ''
9
9
  language: string | null
10
+ organization_sid: string | null
11
+ domain_sid: string | null
10
12
  }
11
13
  }
12
14
  }
package/src/main.js CHANGED
@@ -11,9 +11,11 @@ window.PlayPilotLinkInjections = {
11
11
  after_article_selector: '',
12
12
  after_article_insert_position: '',
13
13
  language: null,
14
+ organization_sid: null,
15
+ domain_sid: null,
14
16
  app: null,
15
17
 
16
- initialize(config = { token: '', selector: '', after_article_selector: '', language: null, editorial_token: '' }) {
18
+ initialize(config = { token: '', selector: '', after_article_selector: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }) {
17
19
  if (!config.token) {
18
20
  console.error('An API token is required.')
19
21
  return
@@ -25,6 +27,8 @@ window.PlayPilotLinkInjections = {
25
27
  this.after_article_selector = config.after_article_selector
26
28
  this.after_article_insert_position = config.after_article_insert_position
27
29
  this.language = config.language
30
+ this.organization_sid = config.organization_sid
31
+ this.domain_sid = config.domain_sid
28
32
 
29
33
  if (this.app) this.destroy()
30
34
 
@@ -11,10 +11,8 @@
11
11
  import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
12
12
  import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
13
13
 
14
- const parentElement = getLinkInjectionsParentElement()
15
- const elements: HTMLElement[] = getLinkInjectionElements(parentElement)
16
- const htmlString = elements.map(p => p.outerHTML).join('')
17
-
14
+ let parentElement: HTMLElement | null = $state(null)
15
+ let elements: HTMLElement[] = $state([])
18
16
  let response: LinkInjectionResponse | null = $state(null)
19
17
  let isEditorialMode = $state(isEditorialModeEnabled())
20
18
  let hasAuthToken = $state(!!getAuthToken())
@@ -24,6 +22,7 @@
24
22
 
25
23
  // @ts-ignore It's ok if the response is empty
26
24
  const { ai_injections: aiInjections = [], link_injections: manualInjections = [] } = $derived(response || {})
25
+ const htmlString = $derived(elements.map(p => p.outerHTML).join(''))
27
26
 
28
27
  // Rerender link injections when linkInjections change. This is only relevant for editiorial mode.
29
28
  $effect(() => {
@@ -52,6 +51,9 @@
52
51
 
53
52
  // URL was marked as being excluded, we stop injections here unless we're in editorial mode.
54
53
  if (!isEditorialMode && config?.exclude_urls_pattern && url.match(config.exclude_urls_pattern)) return
54
+ if (config?.custom_style) insertCustomStyle(config.custom_style || '')
55
+
56
+ setElements(config?.html_selector || '')
55
57
  } catch(error) {
56
58
  // We also return if the config did not get fetched properly, as we can't determine what should and should
57
59
  // get injected without it.
@@ -116,12 +118,32 @@
116
118
  })
117
119
  }
118
120
 
121
+ // Set elements to be used by script, if a selector is passed from the config request we update
122
+ // the selector on the window object.
123
+ function setElements(configSelector: string) {
124
+ if (configSelector) window.PlayPilotLinkInjections.selector = configSelector
125
+
126
+ parentElement = getLinkInjectionsParentElement()
127
+ elements = getLinkInjectionElements(parentElement)
128
+ }
129
+
119
130
  function openEditorialMode() {
120
131
  isEditorialMode = true
121
132
  setEditorialParamInUrl()
122
133
 
123
134
  initialize()
124
135
  }
136
+
137
+ function insertCustomStyle(customStyleString: string) {
138
+ const id = 'playpilot-custom-style'
139
+ const existingElement = document.getElementById(id)
140
+ const styleElement = existingElement || document.createElement('style')
141
+
142
+ styleElement.textContent = `${window.PlayPilotLinkInjections?.selector || 'body'}, .modal, .popover { ${customStyleString} }`
143
+ styleElement.id = id
144
+
145
+ if (!existingElement) document.body.appendChild(styleElement)
146
+ }
125
147
  </script>
126
148
 
127
149
  <div class="playpilot-link-injections">
@@ -37,7 +37,10 @@
37
37
  {#if after_article_style === 'modal_button'}
38
38
  "{title}" {t('Is Available To Stream')}
39
39
 
40
- <span>
40
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
41
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
42
+ <!-- This onclick is here to prevent the event from regular injections from firing, as those are attached to the window and fire on any key data attribute -->
43
+ <span onclick={event => event.stopPropagation()}>
41
44
  <button onclick={(event) => openModal(event, title_details as TitleData, linkInjection)}>
42
45
  {t('View Streaming Options')}
43
46
  </button>
@@ -2,20 +2,54 @@
2
2
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
3
  import { track } from '$lib/tracking'
4
4
  import type { TitleData } from '$lib/types/title'
5
+ import { onMount } from 'svelte'
5
6
  import Popover from './Popover.svelte'
6
7
  import Title from './Title.svelte'
7
8
 
8
9
  interface Props {
10
+ event: MouseEvent
9
11
  title: TitleData
10
12
  }
11
13
 
12
- const { title }: Props = $props()
14
+ const { event, title }: Props = $props()
13
15
 
14
16
  let maxHeight = $state(0)
17
+ let element: HTMLElement | null = $state(null)
15
18
 
16
19
  track(TrackingEvent.TitlePopoverView, title)
20
+
21
+ onMount(setOffset)
22
+
23
+ /**
24
+ * An element can be split up over multiple lines, giving it multiple ClientRects.
25
+ * We calculate the distannce to the closest one and use that for it's positioning.
26
+ * The height is used to acculately flip the Popover when it's too close to the
27
+ * bottom of the screen. This is done in the Popover component itself.
28
+ */
29
+ function setOffset(): void {
30
+ const target = event.target as Element
31
+
32
+ if (!target?.getClientRects) return
33
+
34
+ const rects = Array.from(target.getClientRects())
35
+ const xOffsets = rects.map(rect => rect.x)
36
+ const differences = xOffsets.map(x => Math.abs(x - event.clientX))
37
+ const closestRect = rects[differences.indexOf(Math.min(...differences))]
38
+
39
+ element!.style.top = closestRect.top + 'px'
40
+ element!.style.left = closestRect.left + 'px'
41
+ element!.style.height = closestRect.height + 'px'
42
+ }
17
43
  </script>
18
44
 
19
- <Popover bind:maxHeight>
20
- <Title {title} small compact={!!maxHeight && maxHeight < 250} />
21
- </Popover>
45
+ <div class="title-popover" bind:this={element} data-playpilot-title-popover>
46
+ <Popover bind:maxHeight>
47
+ <Title {title} small compact={!!maxHeight && maxHeight < 250} />
48
+ </Popover>
49
+ </div>
50
+
51
+ <style lang="scss">
52
+ .title-popover {
53
+ position: absolute;
54
+ }
55
+ </style>
@@ -385,7 +385,7 @@ describe('linkInjection.js', () => {
385
385
  expect(results[1].failed_message).toBe('Given sentence was not found in the article.')
386
386
  })
387
387
 
388
- it('Should mount popover component when link is hovered and unmount when link is blurred', async () => {
388
+ it('Should mount popover component when element other than popover or link is hovered', async () => {
389
389
  document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
390
390
 
391
391
  const elements = Array.from(document.body.querySelectorAll('p'))
@@ -401,7 +401,10 @@ describe('linkInjection.js', () => {
401
401
  vi.advanceTimersByTime(200)
402
402
  expect(mount).toHaveBeenCalled()
403
403
 
404
- await fireEvent.mouseLeave(link)
404
+ await fireEvent.mouseMove(link)
405
+ expect(unmount).not.toHaveBeenCalled()
406
+
407
+ await fireEvent.mouseMove(document.body)
405
408
  expect(unmount).toHaveBeenCalled()
406
409
  })
407
410
 
@@ -419,7 +422,7 @@ describe('linkInjection.js', () => {
419
422
 
420
423
  await fireEvent.mouseEnter(link)
421
424
  vi.advanceTimersByTime(200)
422
- await fireEvent.mouseLeave(link)
425
+ await fireEvent.mouseMove(document.body)
423
426
  await fireEvent.mouseEnter(link)
424
427
  vi.advanceTimersByTime(200)
425
428
 
@@ -446,28 +449,6 @@ describe('linkInjection.js', () => {
446
449
  expect(mount).not.toHaveBeenCalled()
447
450
  })
448
451
 
449
- it('Should mount popover component only once when the same popover is already open', async () => {
450
- document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
451
-
452
- const elements = Array.from(document.body.querySelectorAll('p'))
453
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
454
-
455
- injectLinksInDocument(elements, { aiInjections: [injection], manualInjections: [] })
456
-
457
- const link = /** @type {HTMLAnchorElement} */ (document.querySelector('[data-playpilot-injection-key]'))
458
-
459
- vi.useFakeTimers()
460
-
461
- await fireEvent.mouseEnter(link)
462
- vi.advanceTimersByTime(200)
463
- expect(mount).toHaveBeenCalled()
464
-
465
- await fireEvent.mouseEnter(link)
466
- vi.advanceTimersByTime(200)
467
-
468
- expect(mount).toHaveBeenCalledTimes(1)
469
- })
470
-
471
452
  it('Should inject links of the same phrase when multiple are present', () => {
472
453
  document.body.innerHTML = '<p>This is a sentence with an injection and another injection</p>'
473
454
 
@@ -709,6 +690,7 @@ describe('linkInjection.js', () => {
709
690
  <iframe>I am an iframe</iframe>
710
691
  <noscript>I am noscript</noscript>
711
692
  <figcaption>I am a figcaption</figcaption>
693
+ <time>I am time</time>
712
694
 
713
695
  <div>
714
696
  <a>I am another link</a>
@@ -2,16 +2,14 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
 
3
3
  import { setTrackingSids, track } from '$lib/tracking'
4
4
  import { title } from '$lib/fakeData'
5
- import { get } from 'svelte/store'
6
- import { currentDomainSid, currentOrganizationSid } from '$lib/stores/organization'
7
5
  import { getFullUrlPath } from '$lib/url'
8
6
 
9
7
  global.fetch = vi.fn()
10
8
 
11
9
  describe('$lib/tracking', () => {
12
10
  beforeEach(() => {
13
- currentDomainSid.set(null)
14
- currentOrganizationSid.set(null)
11
+ // @ts-ignore
12
+ window.PlayPilotLinkInjections = {}
15
13
 
16
14
  vi.resetAllMocks()
17
15
  })
@@ -57,7 +55,7 @@ describe('$lib/tracking', () => {
57
55
  })
58
56
 
59
57
  it('Should include domain sid in request', () => {
60
- currentDomainSid.set('some-domain')
58
+ window.PlayPilotLinkInjections.domain_sid = 'some-domain'
61
59
 
62
60
  track('Some event')
63
61
 
@@ -70,7 +68,7 @@ describe('$lib/tracking', () => {
70
68
  })
71
69
 
72
70
  it('Should include organization sid in request', () => {
73
- currentOrganizationSid.set('some-organization')
71
+ window.PlayPilotLinkInjections.organization_sid = 'some-organization'
74
72
 
75
73
  track('Some event')
76
74
 
@@ -83,7 +81,7 @@ describe('$lib/tracking', () => {
83
81
  })
84
82
 
85
83
  it('Should include organization sid in request', () => {
86
- currentOrganizationSid.set('some-organization')
84
+ window.PlayPilotLinkInjections.organization_sid = 'some-organization'
87
85
 
88
86
  track('Some event')
89
87
 
@@ -132,15 +130,15 @@ describe('$lib/tracking', () => {
132
130
  it('Should set stores equal to the given values', () => {
133
131
  setTrackingSids({ domainSid: 'some-domain', organizationSid: 'some-organization' })
134
132
 
135
- expect(get(currentDomainSid)).toBe('some-domain')
136
- expect(get(currentOrganizationSid)).toBe('some-organization')
133
+ expect(window.PlayPilotLinkInjections.domain_sid).toBe('some-domain')
134
+ expect(window.PlayPilotLinkInjections.organization_sid).toBe('some-organization')
137
135
  })
138
136
 
139
137
  it('Should set stores to null if invalid values are given', () => {
140
138
  setTrackingSids({ domainSid: '', organizationSid: undefined })
141
139
 
142
- expect(get(currentDomainSid)).toBe(null)
143
- expect(get(currentOrganizationSid)).toBe(null)
140
+ expect(window.PlayPilotLinkInjections.domain_sid).toBe(null)
141
+ expect(window.PlayPilotLinkInjections.organization_sid).toBe(null)
144
142
  })
145
143
  })
146
144
  })
@@ -58,6 +58,7 @@ vi.mock('$lib/url', () => ({
58
58
 
59
59
  describe('$routes/+page.svelte', () => {
60
60
  beforeEach(() => {
61
+ document.body.innerHTML = ''
61
62
  vi.resetAllMocks()
62
63
  vi.mocked(injectLinksInDocument).mockReturnValue([])
63
64
  })
@@ -327,40 +328,90 @@ describe('$routes/+page.svelte', () => {
327
328
  expect(pollLinkInjections).not.toHaveBeenCalled()
328
329
  })
329
330
 
330
- it('Should not inject if config exclude_urls_pattern matches current url', async () => {
331
- vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '/t' })
331
+ describe('Config', () => {
332
+ describe('exclude_urls_pattern', () => {
332
333
 
333
- render(page)
334
+ it('Should not inject if config exclude_urls_pattern matches current url', async () => {
335
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '/t' })
334
336
 
335
- await new Promise(res => setTimeout(res, 100)) // Await all fetches
336
- expect(pollLinkInjections).not.toHaveBeenCalled()
337
- })
337
+ render(page)
338
338
 
339
- it('Should not inject if config exclude_urls_pattern matches current url with more complex regex pattern', async () => {
340
- vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '^/test$' })
339
+ await waitFor(() => expect(fetchConfig).toHaveBeenCalled())
340
+ expect(pollLinkInjections).not.toHaveBeenCalled()
341
+ })
341
342
 
342
- render(page)
343
+ it('Should not inject if config exclude_urls_pattern matches current url with more complex regex pattern', async () => {
344
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '^/test$' })
343
345
 
344
- await new Promise(res => setTimeout(res, 100)) // Await all fetches
345
- expect(pollLinkInjections).not.toHaveBeenCalled()
346
- })
346
+ render(page)
347
347
 
348
- it('Should not inject if config returns an error', async () => {
349
- vi.mocked(fetchConfig).mockRejectedValueOnce('null')
348
+ await waitFor(() => expect(fetchConfig).toHaveBeenCalled())
349
+ expect(pollLinkInjections).not.toHaveBeenCalled()
350
+ })
350
351
 
351
- render(page)
352
+ it('Should not inject if config returns an error', async () => {
353
+ vi.mocked(fetchConfig).mockRejectedValueOnce('null')
352
354
 
353
- await new Promise(res => setTimeout(res, 100)) // Await all fetches
354
- expect(pollLinkInjections).not.toHaveBeenCalled()
355
- expect(track).toHaveBeenCalledWith(TrackingEvent.FetchingConfigFailed)
356
- })
355
+ render(page)
357
356
 
358
- it('Should inject if config exclude_urls_pattern does not match current url', async () => {
359
- vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '/something-else' })
357
+ await waitFor(() => expect(fetchConfig).toHaveBeenCalled())
358
+ expect(pollLinkInjections).not.toHaveBeenCalled()
359
+ expect(track).toHaveBeenCalledWith(TrackingEvent.FetchingConfigFailed)
360
+ })
360
361
 
361
- render(page)
362
+ it('Should inject if config exclude_urls_pattern does not match current url', async () => {
363
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '/something-else' })
364
+
365
+ render(page)
366
+
367
+ await waitFor(() => expect(fetchConfig).toHaveBeenCalled())
368
+ expect(pollLinkInjections).toHaveBeenCalled()
369
+ })
370
+ })
371
+
372
+ describe('html_selector', () => {
373
+ it('Should set window object selector to selector given in config', async () => {
374
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ html_selector: 'some-config-selector' })
362
375
 
363
- await new Promise(res => setTimeout(res, 100)) // Await all fetches
364
- expect(pollLinkInjections).toHaveBeenCalled()
376
+ render(page)
377
+
378
+ await waitFor(() => expect(fetchConfig).toHaveBeenCalled())
379
+ expect(window.PlayPilotLinkInjections.selector).toBe('some-config-selector')
380
+ })
381
+ })
382
+
383
+ describe('custom_style', () => {
384
+ it('Should insert custom html tag with returned value for given selector from config', async () => {
385
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ custom_style: 'some: style' })
386
+
387
+ // @ts-ignore
388
+ window.PlayPilotLinkInjections = { selector: '.some-element' }
389
+
390
+ render(page)
391
+
392
+ await waitFor(() => {
393
+ expect(document.querySelector('#playpilot-custom-style')?.textContent).toBe('.some-element, .modal, .popover { some: style }')
394
+ })
395
+ })
396
+
397
+ it('Should insert custom html tag with returned value for body when no selector is given', async () => {
398
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ custom_style: 'some: style' })
399
+
400
+ render(page)
401
+
402
+ await waitFor(() => {
403
+ expect(document.querySelector('#playpilot-custom-style')?.textContent).toBe('body, .modal, .popover { some: style }')
404
+ })
405
+ })
406
+
407
+ it('Should not insert custom html tag when config returns no value for custom_style', async () => {
408
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: 'value' })
409
+
410
+ render(page)
411
+
412
+ await new Promise(res => setTimeout(res, 100)) // Await all fetches
413
+ expect(document.querySelector('#playpilot-custom-style')).not.toBeTruthy()
414
+ })
415
+ })
365
416
  })
366
417
  })
@@ -16,7 +16,8 @@ describe('TitlePopover.svelte', () => {
16
16
  })
17
17
 
18
18
  it('Should call track function when rendered', () => {
19
- render(TitlePopover, { title })
19
+ const event = new MouseEvent('mouseenter')
20
+ render(TitlePopover, { event, title })
20
21
 
21
22
  expect(track).toHaveBeenCalledWith(TrackingEvent.TitlePopoverView, title)
22
23
  })
@@ -1,4 +0,0 @@
1
- import { writable, type Writable } from "svelte/store"
2
-
3
- export const currentOrganizationSid: Writable<string | null> = writable(null)
4
- export const currentDomainSid: Writable<string | null> = writable(null)