@playpilot/tpi 3.3.0-beta.2 → 3.3.1

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.3.0-beta.2",
3
+ "version": "3.3.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
package/src/lib/api.ts CHANGED
@@ -51,7 +51,12 @@ export async function fetchLinkInjections(url: string, html: string, { hash = st
51
51
  * @param url URL of the given article
52
52
  * @param html HTML to be crawled
53
53
  */
54
- export async function pollLinkInjections(url: string, html: string, { requireCompletedResult = false, pollInterval = 3000, maxTries = 600 } = {}): Promise<LinkInjectionResponse> {
54
+ export async function pollLinkInjections(
55
+ url: string,
56
+ html: string,
57
+ { requireCompletedResult = false, pollInterval = 3000, maxTries = 600, onpoll = () => null }:
58
+ { requireCompletedResult?: boolean, pollInterval?: number, maxTries?: number, onpoll?: (_response: LinkInjectionResponse) => void } = {}
59
+ ): Promise<LinkInjectionResponse> {
55
60
  let hash = stringToHash(html)
56
61
  let currentTry = 0
57
62
 
@@ -65,8 +70,9 @@ export async function pollLinkInjections(url: string, html: string, { requireCom
65
70
  * @param reject Injections are not yet ready
66
71
  */
67
72
  const poll = async (resolve: Function, reject: Function): Promise<void> => {
73
+ let response
68
74
  try {
69
- const response = await fetchLinkInjections(url, html, { hash })
75
+ response = await fetchLinkInjections(url, html, { hash })
70
76
 
71
77
  if (requireCompletedResult && (response.automation_enabled && response.ai_running)) throw new Error
72
78
 
@@ -84,6 +90,8 @@ export async function pollLinkInjections(url: string, html: string, { requireCom
84
90
  return
85
91
  }
86
92
 
93
+ if (response) onpoll(response)
94
+
87
95
  pollTimeout = setTimeout(() => poll(resolve, reject), pollInterval)
88
96
  }
89
97
  }
package/src/lib/image.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * NOTE: This is a temporary measure. Images url from the API use a previous format which is to be replaced,
3
3
  * but that isn't live yet and requires some extra work. In the meantime we remove part of the URL ourselves.
4
4
  */
5
- export function removeImageUrlPrefix(url: string): string {
5
+ export function removeImageUrlPrefix(url: string | null): string | null {
6
+ if (!url) return null
6
7
  return url.replace('/src/img', '')
7
8
  }
@@ -1,4 +1,5 @@
1
1
  import { mount, unmount } from 'svelte'
2
+ import TitleModal from '../routes/components/TitleModal.svelte'
2
3
  import TitlePopover from '../routes/components/TitlePopover.svelte'
3
4
  import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
4
5
  import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
@@ -12,6 +13,7 @@ const activePopovers: Record<string, { injection: LinkInjection; component: obje
12
13
 
13
14
  let currentlyHoveredInjection: EventTarget | null = null
14
15
  let afterArticlePlaylinkInsertedComponent: object | null = null
16
+ let activeModalInsertedComponent: object | null = null
15
17
 
16
18
  /**
17
19
  * Return a list of all valid text containing elements that may get injected into.
@@ -28,7 +30,7 @@ export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElemen
28
30
  if (validElements.includes(element)) continue
29
31
 
30
32
  // Ignore links, buttons, and headers
31
- if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|H[1-6])$/.test(element.tagName)) continue
33
+ if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|FIGCAPTION|H[1-6])$/.test(element.tagName)) continue
32
34
 
33
35
  // Check if this element has a direct text node
34
36
  const hasTextNode = Array.from(element.childNodes).some(
@@ -90,9 +92,10 @@ export function getLinkInjectionsParentElement(): HTMLElement {
90
92
  * Replace all found injections within all given elements on the page
91
93
  * @returns Returns an array of injections with injections that failed to be inserted marked as `failed`.
92
94
  */
93
- export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInjection: LinkInjection) => void, injections: LinkInjectionTypes = { aiInjections: [], manualInjections: [] }): LinkInjection[] {
94
- const mergedInjections = mergeInjectionTypes(injections)
95
+ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkInjectionTypes = { aiInjections: [], manualInjections: [] }): LinkInjection[] {
96
+ clearLinkInjections()
95
97
 
98
+ const mergedInjections = mergeInjectionTypes(injections)
96
99
  if (!mergedInjections) return []
97
100
 
98
101
  // Find injection in text content of all elements together, ignore potential HTML elements.
@@ -162,11 +165,11 @@ export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInj
162
165
  }
163
166
  }
164
167
 
165
- addLinkInjectionEventListeners(validInjections, onclick)
168
+ addLinkInjectionEventListeners(validInjections)
166
169
  addCSSVariablesToLinks()
167
170
 
168
171
  const afterArticleInjections = filterInvalidAfterArticleInjections(mergedInjections)
169
- if (afterArticleInjections.length) insertAfterArticlePlaylinks(elements, afterArticleInjections, onclick)
172
+ if (afterArticleInjections.length) insertAfterArticlePlaylinks(elements, afterArticleInjections)
170
173
 
171
174
  return mergedInjections.filter(i => i.title_details).map((injection, index) => {
172
175
  // Favour manual injections over AI injections
@@ -219,7 +222,7 @@ function addCSSVariablesToLinks(): void {
219
222
  /**
220
223
  * Add event listeners to all injected links. These events are for both the popover and the modal.
221
224
  */
222
- function addLinkInjectionEventListeners(injections: LinkInjection[], onclick: (injection: LinkInjection) => void): void {
225
+ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
223
226
  // Open modal on click
224
227
  window.addEventListener('click', (event) => {
225
228
  const target = event.target as HTMLElement | null
@@ -231,7 +234,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[], onclick: (i
231
234
  const injection = injections.find(injection => key === injection.key)
232
235
  if (!injection) return
233
236
 
234
- openLinkModal(event, injection, onclick)
237
+ openLinkModal(event, injection)
235
238
  })
236
239
 
237
240
  const createdInjectionElements = Array.from(document.querySelectorAll(keySelector)) as HTMLElement[]
@@ -250,16 +253,27 @@ function addLinkInjectionEventListeners(injections: LinkInjection[], onclick: (i
250
253
  }
251
254
 
252
255
  /**
253
- * Prevent default click and run onclick from parent. Ignore clicks that used modifier keys or that were not left click.
254
- * The event is not fired when the click happens from inside a popover.
256
+ * Open modal for the corresponding injection by mounting the component and saving it to a variable.
257
+ * Ignore clicks that used modifier keys or that were not left click.
255
258
  */
256
- function openLinkModal(event: MouseEvent, injection: LinkInjection, onclick: (injection: LinkInjection) => void): void {
259
+ function openLinkModal(event: MouseEvent, injection: LinkInjection): void {
257
260
  if (event.ctrlKey || event.metaKey || event.button !== 0) return
261
+ if (activeModalInsertedComponent) return
258
262
 
259
263
  event.preventDefault()
260
264
 
261
- onclick(injection)
262
265
  destroyLinkPopover(injection)
266
+ activeModalInsertedComponent = mount(TitleModal, { target: document.body, props: { title: injection.title_details!, onclose: destroyLinkModal } })
267
+ }
268
+
269
+ /**
270
+ * Unmount the modal, removing it from the dom
271
+ */
272
+ function destroyLinkModal(outro: boolean = true): void {
273
+ if (!activeModalInsertedComponent) return
274
+
275
+ unmount(activeModalInsertedComponent, { outro })
276
+ activeModalInsertedComponent = null
263
277
  }
264
278
 
265
279
  /**
@@ -303,7 +317,8 @@ function destroyLinkPopover(injection: LinkInjection, outro: boolean = true) {
303
317
  * The config object contains a selector option as well as a position. This way a selector can be given and you can
304
318
  * choose to insert the after article before or after the given element.
305
319
  */
306
- export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections: LinkInjection[], onclickmodal: (linkInjection: LinkInjection) => void) {
320
+ export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections: LinkInjection[]): void {
321
+ if (afterArticlePlaylinkInsertedComponent) return
307
322
  if (!injections.length) return
308
323
 
309
324
  const target = document.createElement('div')
@@ -314,7 +329,7 @@ export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections:
314
329
  target.dataset.playpilotAfterArticlePlaylinks = 'true'
315
330
  insertElement.insertAdjacentElement(insertPosition, target)
316
331
 
317
- afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, { target, props: { linkInjections: injections, onclickmodal } })
332
+ afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, { target, props: { linkInjections: injections, onclickmodal: (event, injection) => openLinkModal(event, injection) } })
318
333
  }
319
334
 
320
335
  function clearAfterArticlePlaylinks(): void {
@@ -337,6 +352,7 @@ export function clearLinkInjections(): void {
337
352
  Object.values(activePopovers).forEach(({ injection }) => destroyLinkPopover(injection, false))
338
353
 
339
354
  clearAfterArticlePlaylinks()
355
+ destroyLinkModal()
340
356
  }
341
357
 
342
358
  /**
@@ -6,3 +6,16 @@
6
6
  fill: none;
7
7
  }
8
8
  }
9
+
10
+ @mixin global-outlines() {
11
+ :global(a),
12
+ :global(button),
13
+ :global(input) {
14
+ transition: outline-offset 100ms;
15
+
16
+ &:focus-visible {
17
+ outline: 2px solid white;
18
+ outline-offset: 2px;
19
+ }
20
+ }
21
+ }
@@ -7,7 +7,7 @@
7
7
  import { isCrawler } from '$lib/crawler'
8
8
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
9
  import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/auth'
10
- import TitleModal from './components/TitleModal.svelte'
10
+ import Editor from './components/Editorial/Editor.svelte'
11
11
  import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
12
12
  import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
13
13
 
@@ -16,7 +16,6 @@
16
16
  const htmlString = elements.map(p => p.outerHTML).join('')
17
17
 
18
18
  let response: LinkInjectionResponse | null = $state(null)
19
- let activeInjection: LinkInjection | null = $state(null)
20
19
  let isEditorialMode = $state(isEditorialModeEnabled())
21
20
  let hasAuthToken = $state(!!getAuthToken())
22
21
  let authorized = $state(false)
@@ -85,20 +84,18 @@
85
84
  // so as not to suddenly insert new links while a user is reading the article.
86
85
  if (!isEditorialMode) return
87
86
 
88
- response = await pollLinkInjections(url, htmlString, { requireCompletedResult: true })
89
-
87
+ response = await pollLinkInjections(url, htmlString, { requireCompletedResult: true, onpoll: (update) => response = update })
90
88
  inject({ aiInjections, manualInjections })
91
89
  }
92
90
 
93
91
  function rerender(): void {
94
- clearLinkInjections()
95
92
  inject(separateLinkInjectionTypes(linkInjections))
96
93
  }
97
94
 
98
95
  function inject(injections: LinkInjectionTypes = { aiInjections, manualInjections }): void {
99
96
  // Get filtered injections as they are shown on the page.
100
97
  // Only update state if it they are different from current injections.
101
- const filteredInjections = injectLinksInDocument(elements, setTarget, injections)
98
+ const filteredInjections = injectLinksInDocument(elements, injections)
102
99
  if (JSON.stringify(filteredInjections) !== JSON.stringify(linkInjections)) linkInjections = filteredInjections
103
100
 
104
101
  const successfulInjections = filteredInjections.filter(i => !i.failed)
@@ -112,10 +109,6 @@
112
109
  })
113
110
  }
114
111
 
115
- function setTarget(injection: LinkInjection): void {
116
- activeInjection = injection
117
- }
118
-
119
112
  function openEditorialMode() {
120
113
  isEditorialMode = true
121
114
  setEditorialParamInUrl()
@@ -132,26 +125,18 @@
132
125
  {/if}
133
126
 
134
127
  {#if isEditorialMode && authorized}
135
- {#await import('./components/Editorial/Editor.svelte') then result}
136
- {@const Editor = result.default}
137
-
138
- <Editor
139
- bind:linkInjections
140
- bind:this={editor}
141
- {htmlString}
142
- {loading}
143
- injectionsEnabled={response?.injections_enabled}
144
- aiStatus={{
145
- automationEnabled: response?.automation_enabled,
146
- aiRunning: response?.ai_running && response?.automation_enabled,
147
- message: response?.ai_progress_message,
148
- percentage: response?.ai_progress_percentage,
149
- }} />
150
- {/await}
151
- {/if}
152
-
153
- {#if activeInjection && activeInjection.title_details}
154
- <TitleModal title={activeInjection.title_details} onclose={() => activeInjection = null} />
128
+ <Editor
129
+ bind:linkInjections
130
+ bind:this={editor}
131
+ {htmlString}
132
+ {loading}
133
+ injectionsEnabled={response?.injections_enabled}
134
+ aiStatus={{
135
+ automationEnabled: response?.automation_enabled,
136
+ aiRunning: response?.ai_running && response?.automation_enabled,
137
+ message: response?.ai_progress_message,
138
+ percentage: response?.ai_progress_percentage,
139
+ }} />
155
140
  {/if}
156
141
  </div>
157
142
 
@@ -164,16 +149,5 @@
164
149
  :global(*) {
165
150
  box-sizing: border-box;
166
151
  }
167
-
168
- :global(.playpilot-link-injections button),
169
- :global(.playpilot-link-injections input) {
170
- transition: outline-offset 100ms;
171
-
172
- &:focus-visible,
173
- &:focus-visible {
174
- outline: 2px solid white;
175
- outline-offset: 2px;
176
- }
177
- }
178
152
  }
179
153
  </style>
@@ -8,11 +8,10 @@
8
8
  interface Props {
9
9
  linkInjections: LinkInjection[],
10
10
  // eslint-disable-next-line no-unused-vars
11
- onclickmodal?: (linkInjection: LinkInjection) => void
11
+ onclickmodal?: (event: MouseEvent, linkInjection: LinkInjection) => void
12
12
  }
13
13
 
14
- // eslint-disable-next-line no-unused-vars
15
- const { linkInjections, onclickmodal = (linkInjection) => null }: Props = $props()
14
+ const { linkInjections, onclickmodal = () => null }: Props = $props()
16
15
 
17
16
  function onclick(title: TitleData, playlink: string): void {
18
17
  track(TrackingEvent.AfterArticlePlaylinkClick, title, { playlink })
@@ -21,9 +20,9 @@
21
20
  /**
22
21
  * Open a modal for the given injection and track the click
23
22
  */
24
- function openModal(title: TitleData, linkInjection: LinkInjection): void {
23
+ function openModal(event: MouseEvent, title: TitleData, linkInjection: LinkInjection): void {
25
24
  track(TrackingEvent.AfterArticleModalButtonClick, title)
26
- onclickmodal(linkInjection)
25
+ onclickmodal(event, linkInjection)
27
26
  }
28
27
  </script>
29
28
 
@@ -39,7 +38,7 @@
39
38
  "{title}" {t('Is Available To Stream')}
40
39
 
41
40
  <span>
42
- <button onclick={() => openModal(title_details as TitleData, linkInjection)}>
41
+ <button onclick={(event) => openModal(event, title_details as TitleData, linkInjection)}>
43
42
  {t('View Streaming Options')}
44
43
  </button>
45
44
  </span>
@@ -6,57 +6,78 @@
6
6
  automationEnabled?: boolean,
7
7
  message?: string,
8
8
  percentage?: number,
9
+ aiInjectionsCount?: number,
9
10
  }
10
11
 
11
- const { aiRunning = false, automationEnabled = false, message = '', percentage = 0 }: Props = $props()
12
+ const { aiRunning = false, automationEnabled = false, message = '', percentage = 0, aiInjectionsCount = 0 }: Props = $props()
13
+
14
+ const initialAIInjectionsCount = aiInjectionsCount
15
+
16
+ let dismissed = $state(false)
12
17
  </script>
13
18
 
14
- <div class="ai-indicator" class:running={aiRunning}>
15
- <div class="content">
16
- <div class="icon">
17
- <IconAi />
18
- </div>
19
+ {#if !dismissed}
20
+ <div class="ai-indicator" class:running={aiRunning} data-testid="ai-indicator">
21
+ <div class="content">
22
+ <div class="icon">
23
+ <IconAi />
24
+ </div>
25
+
26
+ <div>
27
+ {#if !automationEnabled}
28
+ <strong>AI processing is disabled.</strong> Enable AI from the <a href="https://partner.playpilot.net">Partner Portal</a>
29
+ {:else if aiRunning}
30
+ <strong>AI links are currently processing.</strong> This can take several minutes. We'll insert all found injections once ready.
31
+
32
+ <div class="message">
33
+ {message}
34
+
35
+ <span class="ellipses">
36
+ {#each { length: 3 }}
37
+ <span>.</span>
38
+ {/each}
39
+ </span>
40
+ </div>
41
+
42
+ <div class="loading-bar">
43
+ <div class="loading-bar-progress">
44
+ <div class="loading-bar-fill" data-testid="loading-bar" style:width="{Math.max(percentage, 3)}%"></div>
45
+ </div>
19
46
 
20
- <div>
21
- {#if !automationEnabled}
22
- <strong>AI processing is disabled.</strong> Enable AI from the <a href="https://partner.playpilot.net">Partner Portal</a>
23
- {:else if aiRunning}
24
- AI links are currently processing. This can take several minutes. We'll automatically insert all found injections once ready.
25
-
26
- <div class="message">
27
- {message}
28
-
29
- <span class="ellipses">
30
- {#each { length: 3 }}
31
- <span>.</span>
32
- {/each}
33
- </span>
34
- </div>
35
-
36
- <div class="loading-bar">
37
- <div class="loading-bar-progress">
38
- <div class="loading-bar-fill" data-testid="loading-bar" style:width="{Math.max(percentage, 3)}%"></div>
47
+ <div class="loading-bar-label">{percentage}%</div>
39
48
  </div>
49
+ {:else}
50
+ <p>
51
+ <strong>AI has finished processing.</strong> <br>
52
+ {#if initialAIInjectionsCount === aiInjectionsCount}
53
+ No new injections where found.
54
+ {:else}
55
+ {aiInjectionsCount} Injections where found and added to the page.
56
+ {/if}
57
+ </p>
58
+
59
+ <button class="dismiss" onclick={() => dismissed = true}>Dismiss</button>
60
+ {/if}
61
+ </div>
62
+ </div>
40
63
 
41
- <div class="loading-bar-label">{percentage}%</div>
42
- </div>
64
+ <div class="border">
65
+ {#if aiRunning}
66
+ <div class="animator" data-test-id="animator"></div>
43
67
  {/if}
44
68
  </div>
45
-
46
- </div>
47
-
48
- <div class="border">
49
- {#if aiRunning}
50
- <div class="animator" data-test-id="animator"></div>
51
- {/if}
52
69
  </div>
53
- </div>
70
+ {/if}
54
71
 
55
72
  <style lang="scss">
56
73
  a {
57
74
  color: currentColor;
58
75
  }
59
76
 
77
+ p {
78
+ margin: 0;
79
+ }
80
+
60
81
  .ai-indicator {
61
82
  position: relative;
62
83
  margin: 0 margin(0.5);
@@ -168,4 +189,23 @@
168
189
  }
169
190
  }
170
191
  }
192
+
193
+ .dismiss {
194
+ appearance: none;
195
+ margin-top: margin(0.25);
196
+ padding: margin(0.25);
197
+ border: 2px solid var(--playpilot-content-light);
198
+ border-radius: margin(0.25);
199
+ background: var(--playpilot-light);
200
+ font-family: inherit;
201
+ color: var(--playpilot-text-color-alt);
202
+ font-size: margin(0.75);
203
+ line-height: 1;
204
+ cursor: pointer;
205
+
206
+ &:hover {
207
+ background: var(--playpilot-content-light);
208
+ color: var(--playpilot-text-color);
209
+ }
210
+ }
171
211
  </style>
@@ -14,6 +14,7 @@
14
14
  import { track } from '$lib/tracking'
15
15
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
16
16
  import { heading } from '$lib/actions/heading'
17
+ import { separateLinkInjectionTypes } from '$lib/linkInjection'
17
18
 
18
19
  interface Props {
19
20
  linkInjections: LinkInjection[],
@@ -54,6 +55,7 @@
54
55
  const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
55
56
  const sortedInjections = $derived(sortInjections(filteredInjections))
56
57
  const { automationEnabled = false, aiRunning = false } = $derived(aiStatus)
58
+ const initialAiRunning = $derived(!loading && untrack(() => aiStatus.aiRunning))
57
59
 
58
60
  $effect(() => {
59
61
  if (loading) return
@@ -173,8 +175,8 @@
173
175
  </div>
174
176
  {/if}
175
177
 
176
- {#if aiRunning || !automationEnabled}
177
- <AIIndicator {...aiStatus} />
178
+ {#if initialAiRunning || !automationEnabled}
179
+ <AIIndicator {...aiStatus} aiInjectionsCount={separateLinkInjectionTypes(linkInjections).aiInjections.length} />
178
180
  {/if}
179
181
 
180
182
  {#if hasError}
@@ -227,7 +229,7 @@
227
229
  width: 100%;
228
230
  max-width: margin(22);
229
231
  height: min(70vh, margin(40));
230
- min-height: 10rem;
232
+ min-height: min(25rem, 80vh);
231
233
  margin: 0;
232
234
  padding: margin(1);
233
235
  border-radius: margin(1.5);
@@ -241,6 +243,7 @@
241
243
  line-height: normal;
242
244
 
243
245
  @include reset-svg();
246
+ @include global-outlines();
244
247
  }
245
248
 
246
249
  .panel-open {
@@ -338,7 +341,7 @@
338
341
  transition: opacity 100ms;
339
342
  font-family: inherit;
340
343
  color: var(--playpilot-text-color-alt);
341
- font-size: 0.85rem;
344
+ font-size: margin(0.85);
342
345
  cursor: pointer;
343
346
 
344
347
  &:hover {
@@ -65,6 +65,7 @@
65
65
  }
66
66
 
67
67
  @include reset-svg();
68
+ @include global-outlines();
68
69
  }
69
70
 
70
71
 
@@ -77,6 +77,7 @@
77
77
  z-index: 2147483647; // As high as she goes
78
78
 
79
79
  @include reset-svg();
80
+ @include global-outlines();
80
81
 
81
82
  &.flip {
82
83
  top: auto;
@@ -3,6 +3,7 @@ import { fetchConfig, fetchLinkInjections, getApiToken, pollLinkInjections } fro
3
3
  import { fakeFetch } from '../helpers'
4
4
  import { authorize, isEditorialModeEnabled } from '$lib/auth'
5
5
  import { Language } from '$lib/enums/Language'
6
+ import { waitFor } from '@testing-library/svelte'
6
7
 
7
8
  vi.mock('$lib/auth', async importActual => {
8
9
  const actual = await importActual()
@@ -140,6 +141,23 @@ describe('$lib/api', () => {
140
141
  expect(global.fetch).toHaveBeenCalledTimes(3)
141
142
  })
142
143
 
144
+ it('Should call given onpoll function when polling', async () => {
145
+ fakeFetch({ response: { automation_enabled: true, ai_running: true } })
146
+
147
+ const onpoll = vi.fn()
148
+ pollLinkInjections('https://some-url', 'some-html', { requireCompletedResult: true, pollInterval: 500, onpoll })
149
+
150
+ await waitFor(() => {
151
+ expect(onpoll).toHaveBeenCalledTimes(1)
152
+ })
153
+
154
+ await new Promise(res => setTimeout(res, 600)) // Wait for a polling
155
+ expect(onpoll).toHaveBeenCalledTimes(2)
156
+
157
+ await new Promise(res => setTimeout(res, 600)) // Wait for a polling
158
+ expect(onpoll).toHaveBeenCalledTimes(3)
159
+ })
160
+
143
161
  it('Should stop polling if replacements are ready', async () => {
144
162
  const response = { automation_enabled: true, ai_running: false, link_injections: [{ title: 'value' }] }
145
163
  fakeFetch({ response })