@playpilot/tpi 3.8.2 → 3.10.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/eslint.config.js CHANGED
@@ -48,6 +48,21 @@ export default [
48
48
  'comma-dangle': ['error', 'always-multiline'],
49
49
  'no-trailing-spaces': ['error'],
50
50
  'indent': ['warn', 2],
51
+ 'no-unused-vars': [
52
+ 'error',
53
+ {
54
+ vars: 'all',
55
+ args: 'after-used',
56
+ ignoreRestSiblings: true,
57
+ argsIgnorePattern: '^_',
58
+ },
59
+ ],
60
+ 'prefer-const': [
61
+ 'error',
62
+ {
63
+ destructuring: 'all',
64
+ },
65
+ ],
51
66
  },
52
67
  },
53
68
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "3.8.2",
3
+ "version": "3.10.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -18,4 +18,6 @@ export const TrackingEvent = Object.freeze({
18
18
  TotalInjectionsCount: 'ali_injection_count',
19
19
  FetchingConfigFailed: 'ali_fetch_config_failed',
20
20
  AuthFailed: 'ali_auth_failed',
21
+
22
+ ManualReport: 'ali_manual_report',
21
23
  })
@@ -6,6 +6,7 @@ import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom
6
6
  import type { LinkInjection, LinkInjectionTypes } from './types/injection'
7
7
  import { isHoldingSpecialKey } from './event'
8
8
  import { playFallbackViewTransition } from './viewTransition'
9
+ import { prefersReducedMotion } from 'svelte/motion'
9
10
 
10
11
  const keyDataAttribute = 'data-playpilot-injection-key'
11
12
  const keySelector = `[${keyDataAttribute}]`
@@ -243,7 +244,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
243
244
  playFallbackViewTransition(() => {
244
245
  destroyLinkPopover(false)
245
246
  openLinkModal(event, injection)
246
- }, window.innerWidth >= 600 && !window.matchMedia("(pointer: coarse)").matches)
247
+ }, !prefersReducedMotion.current && window.innerWidth >= 600 && !window.matchMedia("(pointer: coarse)").matches)
247
248
  })
248
249
 
249
250
  window.addEventListener('mousemove', (event) => {
@@ -10,7 +10,7 @@ const baseUrl = 'https://insights.playpilot.net'
10
10
  * @param [title] Title related to the event
11
11
  * @param [payload] Any data that will be included with the event
12
12
  */
13
- export async function track(event: string, title: TitleData | null = null, payload: Record<string, string | number | null> = {}): Promise<void> {
13
+ export async function track(event: string, title: TitleData | null = null, payload: Record<string, string | number | null | undefined> = {}): Promise<void> {
14
14
  const headers = new Headers({ 'Content-Type': 'application/json' })
15
15
 
16
16
  if (title) {
@@ -3,6 +3,8 @@ export type PlaylinkData = {
3
3
  name: string
4
4
  url: string
5
5
  logo_url: string
6
+ highlighted?: boolean
7
+ cta_text?: string | null
6
8
  extra_info: {
7
9
  category: PlaylinkCategory
8
10
  }
@@ -63,8 +63,7 @@
63
63
  position: absolute;
64
64
  top: 100%;
65
65
  right: 0;
66
- max-height: margin(5);
67
- max-width: margin(10);
66
+ max-width: margin(15);
68
67
  border-radius: margin(0.5);
69
68
  background: var(--playpilot-lighter);
70
69
  }
@@ -3,10 +3,10 @@
3
3
 
4
4
  interface Props {
5
5
  aiRunning?: boolean
6
- automationEnabled?: boolean,
7
- message?: string,
8
- percentage?: number,
9
- aiInjectionsCount?: number,
6
+ automationEnabled?: boolean
7
+ message?: string
8
+ percentage?: number
9
+ aiInjectionsCount?: number
10
10
  }
11
11
 
12
12
  const { aiRunning = false, automationEnabled = false, message = '', percentage = 0, aiInjectionsCount = 0 }: Props = $props()
@@ -33,7 +33,7 @@
33
33
  {message}
34
34
 
35
35
  <span class="ellipses">
36
- {#each { length: 3 }}
36
+ {#each { length: 3 } as _}
37
37
  <span>.</span>
38
38
  {/each}
39
39
  </span>
@@ -44,7 +44,7 @@
44
44
  <div class="loading-bar-fill" data-testid="loading-bar" style:width="{Math.max(percentage, 3)}%"></div>
45
45
  </div>
46
46
 
47
- <div class="loading-bar-label">{percentage}%</div>
47
+ <div class="loading-bar-label">{Math.floor(percentage)}%</div>
48
48
  </div>
49
49
  {:else}
50
50
  <p>
@@ -43,9 +43,10 @@
43
43
  const editorPositionKey = 'editor-position'
44
44
  const editorHeightKey = 'editor-height'
45
45
 
46
+ const position: Position = JSON.parse(localStorage.getItem(editorPositionKey) || '{ "x": 0, "y": 0 }')
47
+ const height: number = parseInt(localStorage.getItem(editorHeightKey) || '0')
48
+
46
49
  let editorElement: HTMLElement | null = $state(null)
47
- let position: Position = $state(JSON.parse(localStorage.getItem(editorPositionKey) || '{ "x": 0, "y": 0 }'))
48
- let height: number = $state(parseInt(localStorage.getItem(editorHeightKey) || '0'))
49
50
  let manualInjectionActive = $state(false)
50
51
  let saving = $state(false)
51
52
  let hasError = $state(false)
@@ -7,7 +7,7 @@
7
7
  import PlaylinkTypeSelect from './PlaylinkTypeSelect.svelte'
8
8
  import Alert from './Alert.svelte'
9
9
  import ContextMenu from '../ContextMenu.svelte'
10
- import { onMount } from 'svelte'
10
+ import { mount, onMount, unmount } from 'svelte'
11
11
  import { track } from '$lib/tracking'
12
12
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
13
13
  import type { LinkInjection } from '$lib/types/injection'
@@ -16,6 +16,7 @@
16
16
  import { getLinkInjectionElements, getLinkInjectionsParentElement, isValidPlaylinkType } from '$lib/linkInjection'
17
17
  import { imagePlaceholderDataUrl } from '$lib/constants'
18
18
  import { removeImageUrlPrefix } from '$lib/image'
19
+ import ReportIssueModal from './ReportIssueModal.svelte'
19
20
 
20
21
  interface Props {
21
22
  linkInjection: LinkInjection,
@@ -90,6 +91,13 @@
90
91
  return cleanPhrase(element.innerText).includes(cleanPhrase(linkInjection.sentence))
91
92
  }) || []
92
93
  }
94
+
95
+ function showReportIssueModal(): void {
96
+ const component: any = mount(ReportIssueModal, {
97
+ target: document.body,
98
+ props: { linkInjection, onclose: () => unmount(component) },
99
+ })
100
+ }
93
101
  </script>
94
102
 
95
103
  <svelte:window on:mouseover={setInEditorHighlight} />
@@ -126,6 +134,7 @@
126
134
  <div class="context-menu">
127
135
  <ContextMenu ariaLabel="More options">
128
136
  <button class="context-menu-action" onclick={onremove}>Remove</button>
137
+ <button class="context-menu-action" onclick={showReportIssueModal}>Report issue</button>
129
138
  </ContextMenu>
130
139
  </div>
131
140
  </div>
@@ -257,13 +266,25 @@
257
266
  appearance: none;
258
267
  background: transparent;
259
268
  border: 0;
260
- padding: margin(1);
269
+ padding: margin(0.5) margin(1);
270
+ width: 100%;
271
+ text-align: left;
261
272
  font-family: inherit;
273
+ white-space: nowrap;
262
274
  color: var(--playpilot-text-color-alt);
263
275
  cursor: pointer;
264
276
 
265
277
  &:hover {
266
278
  color: var(--playpilot-text-color);
279
+ background-color: var(--playpilot-content-light);
280
+ }
281
+
282
+ &:first-child {
283
+ margin-top: margin(0.5);
284
+ }
285
+
286
+ &:last-child {
287
+ margin-bottom: margin(0.5);
267
288
  }
268
289
  }
269
290
 
@@ -20,7 +20,7 @@
20
20
  onclose?: () => void
21
21
  }
22
22
 
23
- let { pageText = '', onsave, onclose = () => null }: Props = $props()
23
+ const { pageText = '', onsave, onclose = () => null }: Props = $props()
24
24
 
25
25
  let currentSelection = $state('')
26
26
  let selectionSentence = $state('')
@@ -0,0 +1,156 @@
1
+ <script lang="ts">
2
+ import { heading } from '$lib/actions/heading'
3
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
4
+ import { track } from '$lib/tracking'
5
+ import { truncateAroundPhrase } from '$lib/text'
6
+ import type { LinkInjection } from '$lib/types/injection'
7
+ import Modal from '../Modal.svelte'
8
+ import { fade } from 'svelte/transition'
9
+
10
+ interface Props {
11
+ onclose: () => void,
12
+ linkInjection: LinkInjection
13
+ }
14
+
15
+ const { onclose, linkInjection }: Props = $props()
16
+
17
+ const options = [{
18
+ value: 'failed_injection',
19
+ label: 'The link failed to appear on the page.',
20
+ }, {
21
+ value: 'layout_issue',
22
+ label: 'The link is breaking the layout or formatting of my page.',
23
+ }, {
24
+ value: 'wrong_content',
25
+ label: 'The link points to the wrong movie or show.',
26
+ }, {
27
+ value: 'inappropriate_title',
28
+ label: 'The link points to an inappropriate movie or show.',
29
+ }, {
30
+ value: 'broken_link',
31
+ label: 'The link is broken or leads to an error page.',
32
+ }, {
33
+ value: 'other',
34
+ label: 'My issue is not listed, but please have a look regardless.',
35
+ }]
36
+
37
+ let reason = $state('')
38
+ let reportSent = $state(false)
39
+
40
+ function sendReport() {
41
+ if (!reason) return
42
+
43
+ track(TrackingEvent.ManualReport, linkInjection.title_details, {
44
+ report_reason: reason,
45
+ sid: linkInjection.sid,
46
+ title: linkInjection.title,
47
+ sentence: linkInjection.sentence,
48
+ failed: linkInjection.failed?.toString(),
49
+ failed_message: linkInjection.failed_message,
50
+ manual: linkInjection.manual?.toString(),
51
+ })
52
+
53
+ reportSent = true
54
+ setTimeout(onclose, 3000)
55
+ }
56
+ </script>
57
+
58
+ <Modal {onclose}>
59
+ <div class="form">
60
+ <div class="heading" use:heading={2}>Report issue</div>
61
+
62
+ <div class="info">
63
+ <div><strong>Title</strong>: {linkInjection.title}</div>
64
+ <div>
65
+ <strong>Sentence</strong>:
66
+ {truncateAroundPhrase(linkInjection.sentence, linkInjection.title, 100)}
67
+ </div>
68
+ </div>
69
+
70
+ <label for="reason">What would you like to report?</label>
71
+ <select id="reason" name="reason" bind:value={reason}>
72
+ <option value=''>Select an issue...</option>
73
+
74
+ {#each options as { value, label }}
75
+ <!-- This onclick exists purely for Vitest, the bind:value above would just not work :( -->
76
+ <option {value} onclick={() => reason = value}>{label}</option>
77
+ {/each}
78
+ </select>
79
+
80
+ <div>
81
+ {#if reportSent}
82
+ <div class="sent" in:fade={{ duration: 200 }}>Report has been sent, thank you! Closing window...</div>
83
+ {:else}
84
+ <button class="submit" onclick={sendReport} disabled={!reason}>Send report</button>
85
+ {/if}
86
+ </div>
87
+ </div>
88
+ </Modal>
89
+
90
+ <style lang="scss">
91
+ select {
92
+ background: var(--playpilot-content);
93
+ padding: margin(0.5) margin(1);
94
+ width: 100%;
95
+ border: 0;
96
+ border-radius: margin(0.5);
97
+ color: var(--playpilot-text-color);
98
+ font-family: inherit;
99
+ font-size: margin(0.85);
100
+ }
101
+
102
+ option:hover,
103
+ option:active,
104
+ option:checked {
105
+ background-color: var(--playpilot-content-light);
106
+ font-weight: bold;
107
+ }
108
+
109
+ .form {
110
+ padding: margin(2);
111
+ font-family: var(--playpilot-font-family);
112
+ color: var(--playpilot-text-color);
113
+ }
114
+
115
+ .heading {
116
+ font-size: margin(1.25);
117
+ }
118
+
119
+ .info {
120
+ margin: margin(1) 0;
121
+ font-size: margin(0.85);
122
+ color: var(--playpilot-text-color-alt);
123
+
124
+ strong {
125
+ color: var(--playpilot-text-color);
126
+ }
127
+ }
128
+
129
+ .submit {
130
+ margin-top: margin(1);
131
+ padding: margin(0.5) margin(1);
132
+ border: 0;
133
+ border-radius: margin(2);
134
+ background: var(--playpilot-green);
135
+ box-shadow: var(--playpilot-shadow);
136
+ font-family: inherit;
137
+ color: var(--playpilot-text-color);
138
+ font-size: margin(0.85);
139
+ cursor: pointer;
140
+
141
+ &[disabled] {
142
+ background: var(--playpilot-content);
143
+ color: var(--playpilot-text-color-alt);
144
+ box-shadow: none;
145
+ opacity: 0.5;
146
+ cursor: default;
147
+ }
148
+ }
149
+
150
+ .sent {
151
+ margin-top: margin(1);
152
+ padding: margin(0.5) 0;
153
+ color: var(--playpilot-green);
154
+ font-size: margin(0.85);
155
+ }
156
+ </style>
@@ -3,6 +3,7 @@
3
3
  import IconClose from './Icons/IconClose.svelte'
4
4
  import RoundButton from './RoundButton.svelte'
5
5
  import { onMount, setContext, type Snippet } from 'svelte'
6
+ import { prefersReducedMotion } from 'svelte/motion'
6
7
 
7
8
  interface Props {
8
9
  children: Snippet
@@ -22,6 +23,8 @@
22
23
  })
23
24
 
24
25
  function scaleOrFly(node: Element): TransitionConfig {
26
+ if (prefersReducedMotion.current) return fade(node, { duration: 0 })
27
+
25
28
  const shouldFly = window.innerWidth < 600
26
29
 
27
30
  if (shouldFly) return fly(node, { duration: 250, y: window.innerHeight })
@@ -31,8 +34,8 @@
31
34
 
32
35
  <svelte:window on:keydown={({ key }) => { if (key === 'Escape') onclose() }} />
33
36
 
34
- <div class="modal" transition:fade={{ duration: 150 }}>
35
- <div class="dialog" {onscroll} role="dialog" transition:scaleOrFly|global data-view-transition-new>
37
+ <div class="modal" transition:fade|global={{ duration: 150 }}>
38
+ <div class="dialog" {onscroll} role="dialog" aria-labelledby="title" transition:scaleOrFly|global data-view-transition-new>
36
39
  <div class="close">
37
40
  <RoundButton onclick={() => onclose()}>
38
41
  <IconClose />
@@ -36,16 +36,16 @@
36
36
  }
37
37
  </script>
38
38
 
39
- <div class="heading" use:heading={2}>{t('Where To Stream Online')}</div>
39
+ <div class="heading" use:heading={3}>{t('Where To Stream Online')}</div>
40
40
 
41
41
  <div class="playlinks" class:list>
42
- {#each mergedPlaylink as { name, url, logo_url, extra_info: { category } }}
43
- <a href={url} target="_blank" class="playlink" onclick={() => onclick(name)} data-playlink={name} rel="sponsored">
42
+ {#each mergedPlaylink as { name, url, logo_url, highlighted, cta_text, extra_info: { category } }}
43
+ <a href={url} target="_blank" class="playlink" class:highlighted={highlighted && cta_text} onclick={() => onclick(name)} data-playlink={name} rel="sponsored">
44
44
  <img src={removeImageUrlPrefix(logo_url)} alt="" height="32" width="32" />
45
45
 
46
46
  <div>
47
47
  <span class="name">{name}</span>
48
- <div class="category">{categoryStrings[category] || t('Stream')}</div>
48
+ <div class="category">{cta_text || categoryStrings[category] || t('Stream')}</div>
49
49
  </div>
50
50
 
51
51
  {#if list}
@@ -112,6 +112,7 @@
112
112
  }
113
113
 
114
114
  .playlink {
115
+ position: relative;
115
116
  display: flex;
116
117
  align-items: center;
117
118
  gap: margin(0.75);
@@ -134,6 +135,23 @@
134
135
  text-decoration: none !important;
135
136
  }
136
137
 
138
+ &.highlighted {
139
+ &::before {
140
+ content: "";
141
+ z-index: 1;
142
+ display: block;
143
+ position: absolute;
144
+ top: 0;
145
+ right: 0;
146
+ bottom: 0;
147
+ left: 0;
148
+ border-radius: inherit;
149
+ box-shadow: inset 0 0 0 2px currentColor;
150
+ opacity: 0.35;
151
+ pointer-events: none;
152
+ }
153
+ }
154
+
137
155
  img {
138
156
  margin: 0;
139
157
  }
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onMount, setContext, tick, type Snippet } from 'svelte'
3
+ import { prefersReducedMotion } from 'svelte/motion'
3
4
  import { fly } from 'svelte/transition'
4
5
 
5
6
  interface Props {
@@ -59,7 +60,7 @@
59
60
  </script>
60
61
 
61
62
  <div class="popover" class:flip bind:this={element} style:--max-height={maxHeight ? maxHeight + 'px' : null} tabindex="-1" aria-hidden="true">
62
- <div class="dialog" transition:fly|global={{ duration: 100, y: 10 }} data-view-transition-old>
63
+ <div class="dialog" transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }} data-view-transition-old>
63
64
  {@render children()}
64
65
  </div>
65
66
  </div>
@@ -34,7 +34,7 @@
34
34
  </div>
35
35
  {/if}
36
36
 
37
- <div class="heading" use:heading class:truncate={small}>{title.title}</div>
37
+ <div class="heading" use:heading={2} class:truncate={small} id="title">{title.title}</div>
38
38
 
39
39
  <div class="info">
40
40
  <div class="imdb">
@@ -42,7 +42,7 @@
42
42
  }
43
43
  </script>
44
44
 
45
- <div class="title-popover" bind:this={element} data-playpilot-title-popover>
45
+ <div class="title-popover" bind:this={element} data-playpilot-title-popover role="region" aria-labelledby="title">
46
46
  <Popover bind:maxHeight>
47
47
  <Title {title} small compact={!!maxHeight && maxHeight < 250} />
48
48
  </Popover>
@@ -70,7 +70,7 @@ describe('$lib/auth', () => {
70
70
 
71
71
  it('Should not authorize is fetch response was negative', async () => {
72
72
  fakeFetch({ response: '', ok: false, status: 403 })
73
- let authorized = await authorize('https://example.com/some-path?articleReplacementEditToken=some-token')
73
+ const authorized = await authorize('https://example.com/some-path?articleReplacementEditToken=some-token')
74
74
 
75
75
  expect(authorized).not.toBeTruthy()
76
76
  expect(track).toHaveBeenCalled()
@@ -804,7 +804,7 @@ describe('linkInjection.js', () => {
804
804
  const result = getLinkInjectionElements(parent, '[data-exclude]')
805
805
  expect(result).toHaveLength(1)
806
806
  expect(result[0].innerText).toBe('I am a regular element')
807
- });
807
+ })
808
808
 
809
809
  it('Should return paragraphs fully even if they contain no direct text nodes, skipping empty paragraphs', () => {
810
810
  document.body.innerHTML = `<section>
@@ -38,6 +38,13 @@ describe('AIIndicator.svelte', () => {
38
38
  expect(getByText('25%')).toBeTruthy()
39
39
  })
40
40
 
41
+ it('Should floor the percentage label if a decimal is given', () => {
42
+ const { getByText, getByTestId } = render(AIIndicator, { aiRunning: true, automationEnabled: true, percentage: 25.55 })
43
+
44
+ expect(getByTestId('loading-bar').style.width).toBe('25.55%')
45
+ expect(getByText('25%')).toBeTruthy()
46
+ })
47
+
41
48
  it('Should limit progress to minimum value in loading bar style only', () => {
42
49
  const { getByText, getByTestId } = render(AIIndicator, { aiRunning: true, automationEnabled: true, percentage: 0 })
43
50
 
@@ -165,14 +165,14 @@ describe('EditorItem.svelte', () => {
165
165
  expect(onremove).toHaveBeenCalled()
166
166
  })
167
167
 
168
- it('Should display an inactive injection as being so with text and classname', async () => {
168
+ it('Should display an inactive injection as being so with text and classname', () => {
169
169
  const { getByText, container } = render(EditorItem, { linkInjection: { ...linkInjection, inactive: true } })
170
170
 
171
171
  expect(getByText('Inactive')).toBeTruthy()
172
172
  expect(container.querySelector('.inactive')).toBeTruthy()
173
173
  })
174
174
 
175
- it('Should not display an active injection as being inactive', async () => {
175
+ it('Should not display an active injection as being inactive', () => {
176
176
  const { queryByText, container } = render(EditorItem, { linkInjection })
177
177
 
178
178
  expect(queryByText('Inactive')).not.toBeTruthy()
@@ -180,15 +180,24 @@ describe('EditorItem.svelte', () => {
180
180
  expect(container.querySelector('.inactive')).not.toBeTruthy()
181
181
  })
182
182
 
183
- it('Should display an icon when playlink types are invalid', async () => {
183
+ it('Should display an icon when playlink types are invalid', () => {
184
184
  const { getByLabelText } = render(EditorItem, { linkInjection: { ...linkInjection, in_text: false } })
185
185
 
186
186
  expect(getByLabelText('Invalid playlink settings')).toBeTruthy()
187
187
  })
188
188
 
189
- it('Should not display an icon when playlink types are valid', async () => {
189
+ it('Should not display an icon when playlink types are valid', () => {
190
190
  const { queryByLabelText } = render(EditorItem, { linkInjection: { ...linkInjection, in_text: true } })
191
191
 
192
192
  expect(queryByLabelText('Invalid playlink settings')).not.toBeTruthy()
193
193
  })
194
+
195
+ it('Should mount component when clicking report issue button', async () => {
196
+ const { getByLabelText, getByText } = render(EditorItem, { linkInjection })
197
+
198
+ await fireEvent.click(getByLabelText('More options'))
199
+ await fireEvent.click(getByText('Report issue'))
200
+
201
+ expect(getByText('What would you like to report?')).toBeTruthy()
202
+ })
194
203
  })
@@ -0,0 +1,63 @@
1
+ import { fireEvent, render } from '@testing-library/svelte'
2
+ import { describe, expect, it, vi, beforeEach } from 'vitest'
3
+
4
+ import ReportIssueModal from '../../../../routes/components/Editorial/ReportIssueModal.svelte'
5
+ import { track } from '$lib/tracking'
6
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
7
+ import { generateInjection } from '../../../helpers'
8
+
9
+ vi.mock('$lib/tracking', () => ({
10
+ track: vi.fn(),
11
+ }))
12
+
13
+ describe('ReportIssueModal.svelte', () => {
14
+ beforeEach(() => {
15
+ vi.resetAllMocks()
16
+ })
17
+
18
+ const onclose = vi.fn()
19
+ const linkInjection = generateInjection('a word', 'Some sentence with a word')
20
+
21
+ it('Should enable button after selecting issue', async () => {
22
+ const { getByRole, getAllByRole, getByText } = render(ReportIssueModal, { onclose, linkInjection })
23
+
24
+ expect(/** @type {HTMLButtonElement } */ (getByText('Send report')).disabled).toBeTruthy()
25
+
26
+ await fireEvent.click(getByRole('combobox'))
27
+ await fireEvent.click(getAllByRole('option')[1])
28
+
29
+ expect(/** @type {HTMLButtonElement } */ (getByText('Send report')).disabled).not.toBeTruthy()
30
+ })
31
+
32
+ it('Should submit form when clicked', async () => {
33
+ const { getByRole, getAllByRole, getByText } = render(ReportIssueModal, { onclose, linkInjection })
34
+
35
+ await fireEvent.click(getByRole('combobox'))
36
+ await fireEvent.click(getAllByRole('option')[1])
37
+ await fireEvent.click(getByText('Send report'))
38
+
39
+ expect(track).toHaveBeenCalledWith(
40
+ TrackingEvent.ManualReport,
41
+ linkInjection.title_details,
42
+ {
43
+ report_reason: /** @type {HTMLOptionElement} */ (getAllByRole('option')[1]).value,
44
+ sid: linkInjection.sid,
45
+ title: linkInjection.title,
46
+ sentence: linkInjection.sentence,
47
+ failed: linkInjection.failed?.toString(),
48
+ failed_message: linkInjection.failed_message,
49
+ manual: linkInjection.manual?.toString(),
50
+ },
51
+ )
52
+
53
+ expect(getByText('Report has been sent', { exact: false }))
54
+ })
55
+
56
+ it('Should not submit form when no reason is selected', async () => {
57
+ const { getByText } = render(ReportIssueModal, { onclose, linkInjection })
58
+
59
+ await fireEvent.click(getByText('Send report'))
60
+
61
+ expect(track).not.toHaveBeenCalled()
62
+ })
63
+ })