@playpilot/tpi 3.0.1 → 3.1.0-beta.2

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 (30) hide show
  1. package/dist/link-injections.js +7 -7
  2. package/package.json +1 -1
  3. package/src/lib/api.ts +21 -1
  4. package/src/lib/enums/TrackingEvent.ts +2 -0
  5. package/src/lib/linkInjection.ts +8 -8
  6. package/src/lib/text.ts +24 -0
  7. package/src/lib/types/config.d.ts +3 -0
  8. package/src/routes/+page.svelte +18 -3
  9. package/src/routes/components/Editorial/AIIndicator.svelte +43 -2
  10. package/src/routes/components/Editorial/DragHandle.svelte +3 -3
  11. package/src/routes/components/Editorial/Editor.svelte +58 -28
  12. package/src/routes/components/Editorial/EditorItem.svelte +52 -24
  13. package/src/routes/components/Editorial/ManualInjection.svelte +69 -16
  14. package/src/routes/components/Editorial/ResizeHandle.svelte +111 -0
  15. package/src/routes/components/Editorial/Search/TitleSearch.svelte +17 -90
  16. package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +107 -0
  17. package/src/routes/components/Title.svelte +28 -4
  18. package/src/tests/helpers.js +2 -1
  19. package/src/tests/lib/api.test.js +30 -1
  20. package/src/tests/lib/linkInjection.test.js +39 -11
  21. package/src/tests/lib/text.test.js +23 -1
  22. package/src/tests/routes/+page.test.js +52 -5
  23. package/src/tests/routes/components/Editorial/AiIndicator.test.js +5 -2
  24. package/src/tests/routes/components/Editorial/DragHandle.test.js +5 -5
  25. package/src/tests/routes/components/Editorial/Editor.test.js +69 -0
  26. package/src/tests/routes/components/Editorial/EditorItem.test.js +34 -10
  27. package/src/tests/routes/components/Editorial/ManualInjection.test.js +5 -1
  28. package/src/tests/routes/components/Editorial/ResizeHandle.test.js +45 -0
  29. package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +6 -4
  30. package/src/tests/routes/components/Editorial/Search/TitleSearchItem.test.js +44 -0
@@ -1,7 +1,7 @@
1
1
  import { fireEvent } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi, beforeEach } from 'vitest'
3
3
 
4
- import { injectLinksInDocument, clearLinkInjections, clearLinkInjection, getLinkInjectionElements, insertAfterArticlePlaylinks, getLinkInjectionsParentElement, sortLinkInjectionsByRange, isAvailableAsManualInjection, filterRemovedInjections, isEquivalentInjection, filterInvalidInTextInjections, filterInvalidAfterArticleInjections, isValidInjection } from '$lib/linkInjection'
4
+ import { injectLinksInDocument, clearLinkInjections, clearLinkInjection, getLinkInjectionElements, insertAfterArticlePlaylinks, getLinkInjectionsParentElement, sortLinkInjectionsByRange, isAvailableAsManualInjection, filterRemovedAndInactiveInjections, isEquivalentInjection, filterInvalidInTextInjections, filterInvalidAfterArticleInjections, isValidInjection } from '$lib/linkInjection'
5
5
  import { mount, unmount } from 'svelte'
6
6
  import { generateInjection } from '../helpers'
7
7
 
@@ -118,6 +118,24 @@ describe('linkInjection.js', () => {
118
118
  expect(document.body.innerHTML).toBe(`<p>${sentence}</p>`)
119
119
  })
120
120
 
121
+ it('Should ignore AI injections that are marked as inactive as a matching manual injections', () => {
122
+ const sentence = 'This is a sentence with an injection.'
123
+ const injection = generateInjection(sentence, 'an injection')
124
+
125
+ document.body.innerHTML = `<p>${sentence}</p>`
126
+
127
+ const elements = Array.from(document.body.querySelectorAll('p'))
128
+
129
+ const injections = {
130
+ aiInjections: [injection],
131
+ manualInjections: [{ ...injection, manual: true, inactive: true }],
132
+ }
133
+
134
+ injectLinksInDocument(elements, () => null, injections)
135
+
136
+ expect(document.body.innerHTML).toBe(`<p>${sentence}</p>`)
137
+ })
138
+
121
139
  it('Should keep existing HTML elements intact', () => {
122
140
  const injection = generateInjection('This is a sentence with an injection.', 'an injection')
123
141
  const linkInjections = [injection]
@@ -216,7 +234,7 @@ describe('linkInjection.js', () => {
216
234
 
217
235
  const elements = Array.from(document.body.querySelectorAll('p'))
218
236
 
219
- /** @type {LinkInjection[]} */
237
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
220
238
  const linkInjections = []
221
239
 
222
240
  injectLinksInDocument(elements, () => null, { aiInjections: linkInjections, manualInjections: [] })
@@ -228,7 +246,7 @@ describe('linkInjection.js', () => {
228
246
  /** @type {HTMLElement[]} */
229
247
  const elements = []
230
248
 
231
- /** @type {LinkInjection[]} */
249
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
232
250
  const linkInjections = []
233
251
 
234
252
  expect(() => injectLinksInDocument(elements, () => null, { aiInjections: linkInjections, manualInjections: [] })).not.toThrow()
@@ -446,8 +464,6 @@ describe('linkInjection.js', () => {
446
464
  const mock = vi.fn()
447
465
  injectLinksInDocument(elements, mock, { aiInjections: [], manualInjections: injections })
448
466
 
449
- console.log(document.body.innerHTML)
450
-
451
467
  expect(document.querySelectorAll('a')).toHaveLength(4)
452
468
  })
453
469
 
@@ -469,8 +485,6 @@ describe('linkInjection.js', () => {
469
485
  const mock = vi.fn()
470
486
  injectLinksInDocument(elements, mock, { aiInjections: [], manualInjections: injections })
471
487
 
472
- console.log(document.body.innerHTML)
473
-
474
488
  expect(document.querySelectorAll('a')).toHaveLength(3)
475
489
  })
476
490
  })
@@ -767,26 +781,40 @@ describe('linkInjection.js', () => {
767
781
  })
768
782
  })
769
783
 
770
- describe('filterRemovedInjections', () => {
784
+ describe('filterRemovedAndInactiveInjections', () => {
771
785
  it('Should not return injections that are removed or have an removed manual equivalent', () => {
772
786
  const aiInjection = generateInjection('Some sentence', 'Some title')
773
787
  const manualInjection = { ...aiInjection, manual: true, removed: true }
774
788
 
775
- expect(filterRemovedInjections([aiInjection, manualInjection])).toEqual([])
789
+ expect(filterRemovedAndInactiveInjections([aiInjection, manualInjection])).toEqual([])
790
+ })
791
+
792
+ it('Should not return injections that are inactive or have an inactive manual equivalent', () => {
793
+ const aiInjection = generateInjection('Some sentence', 'Some title')
794
+ const manualInjection = { ...aiInjection, manual: true, inactive: true }
795
+
796
+ expect(filterRemovedAndInactiveInjections([aiInjection, manualInjection])).toEqual([])
776
797
  })
777
798
 
778
799
  it('Should return ai injections that have no matching manual injection', () => {
779
800
  const aiInjection = generateInjection('Some sentence', 'Some title')
780
801
  const manualInjection = { ...generateInjection('Some other sentence', 'Some other title'), manual: true }
781
802
 
782
- expect(filterRemovedInjections([aiInjection, manualInjection])).toEqual([aiInjection, manualInjection])
803
+ expect(filterRemovedAndInactiveInjections([aiInjection, manualInjection])).toEqual([aiInjection, manualInjection])
783
804
  })
784
805
 
785
806
  it('Should return manual injections that have been removed at one point, but a new injection for the same title was created', () => {
786
807
  const manualInjection = { ...generateInjection('Some other sentence', 'Some other title'), manual: true }
787
808
  const removedManualInjection = { ...manualInjection, removed: true }
788
809
 
789
- expect(filterRemovedInjections([manualInjection, removedManualInjection])).toEqual([manualInjection])
810
+ expect(filterRemovedAndInactiveInjections([manualInjection, removedManualInjection])).toEqual([manualInjection])
811
+ })
812
+
813
+ it('Should return manual injections that have been inactive at one point, but a new injection for the same title was created', () => {
814
+ const manualInjection = { ...generateInjection('Some other sentence', 'Some other title'), manual: true }
815
+ const inactiveManualInjection = { ...manualInjection, inactive: true }
816
+
817
+ expect(filterRemovedAndInactiveInjections([manualInjection, inactiveManualInjection])).toEqual([manualInjection])
790
818
  })
791
819
  })
792
820
 
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest'
2
- import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from '$lib/text'
2
+ import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom, truncateAroundPhrase } from '$lib/text'
3
3
 
4
4
  describe('text.js', () => {
5
5
  beforeEach(() => {
@@ -105,4 +105,26 @@ describe('text.js', () => {
105
105
  expect(cleanPhrase('&#123;&amp;&#125;')).toBe('{&}')
106
106
  })
107
107
  })
108
+
109
+ describe('truncateAroundPhrase', () => {
110
+ it('Should truncate a sentence at the end when the phrase is near the start', () => {
111
+ expect(truncateAroundPhrase('Some sentence with a word', 'Some', 10)).toBe('Some sente…')
112
+ })
113
+
114
+ it('Should truncate a sentence at the start when the phrase is near the end', () => {
115
+ expect(truncateAroundPhrase('Some sentence with a word', 'a word', 10)).toBe('…ith a word')
116
+ })
117
+
118
+ it('Should truncate a sentence at the start end the start when the phrase is near the center', () => {
119
+ expect(truncateAroundPhrase('Some sentence with a word', 'with', 10)).toBe('…ce with a…')
120
+ })
121
+
122
+ it('Should not truncate a sentence if it fits regardless', () => {
123
+ expect(truncateAroundPhrase('Some sentence with a word', 'with', 50)).toBe('Some sentence with a word')
124
+ })
125
+
126
+ it('Should break phrase and truncate if the phrase would not fit in max lengtj', () => {
127
+ expect(truncateAroundPhrase('Some sentence with a word', 'sentence with a', 10)).toBe('…tence with…')
128
+ })
129
+ })
108
130
  })
@@ -2,13 +2,14 @@ import { render, waitFor } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
3
3
 
4
4
  import page from '../../routes/+page.svelte'
5
- import { pollLinkInjections } from '$lib/api'
5
+ import { fetchConfig, pollLinkInjections } from '$lib/api'
6
6
  import { track } from '$lib/tracking'
7
7
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
8
8
  import { authorize } from '$lib/auth'
9
9
 
10
10
  vi.mock('$lib/api', () => ({
11
11
  pollLinkInjections: vi.fn(),
12
+ fetchConfig: vi.fn(),
12
13
  }))
13
14
 
14
15
  vi.mock('$lib/tracking', () => ({
@@ -23,6 +24,10 @@ vi.mock(import('$lib/auth'), async (importOriginal) => {
23
24
  }
24
25
  })
25
26
 
27
+ vi.mock('$lib/url', () => ({
28
+ getFullUrlPath: () => '/test',
29
+ }))
30
+
26
31
  describe('$routes/+page.svelte', () => {
27
32
  beforeEach(() => {
28
33
  vi.resetAllMocks()
@@ -38,9 +43,11 @@ describe('$routes/+page.svelte', () => {
38
43
  window.PlayPiloLinkInjections = null
39
44
  })
40
45
 
41
- it('Should call pollLinkInjections on mount', () => {
46
+ it('Should call pollLinkInjections and fetchConfig on mount', async () => {
42
47
  render(page)
43
- expect(pollLinkInjections).toHaveBeenCalled()
48
+
49
+ expect(fetchConfig).toHaveBeenCalled()
50
+ await waitFor(() => expect(pollLinkInjections).toHaveBeenCalled())
44
51
  })
45
52
 
46
53
  it('Should call pollLinkInjections again if ai_running is true while in editorial mode', async () => {
@@ -58,14 +65,17 @@ describe('$routes/+page.svelte', () => {
58
65
  })
59
66
  })
60
67
 
61
- it('Should use the given selector when present', () => {
68
+ it('Should use the given selector when present', async () => {
62
69
  document.body.innerHTML = '<div class="some-element"><p>Here</p></div> <p>Not here</p>'
63
70
 
64
71
  // @ts-ignore
65
72
  window.PlayPilotLinkInjections = { selector: '.some-element' }
66
73
 
67
74
  render(page)
68
- expect(pollLinkInjections).toHaveBeenCalledWith(expect.any(String), '<p>Here</p>', { maxTries: 1 })
75
+
76
+ await waitFor(() => {
77
+ expect(pollLinkInjections).toHaveBeenCalledWith(expect.any(String), '<p>Here</p>', { maxTries: 1 })
78
+ })
69
79
  })
70
80
 
71
81
  it('Should render editor if correct url param is given', async () => {
@@ -121,4 +131,41 @@ describe('$routes/+page.svelte', () => {
121
131
 
122
132
  expect(track).toHaveBeenCalledWith(TrackingEvent.ArticlePageView)
123
133
  })
134
+
135
+ it('Should not inject if config exclude_urls_pattern matches current url', async () => {
136
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '/t' })
137
+
138
+ render(page)
139
+
140
+ await new Promise(res => setTimeout(res, 100)) // Await all fetches
141
+ expect(pollLinkInjections).not.toHaveBeenCalled()
142
+ })
143
+
144
+ it('Should not inject if config exclude_urls_pattern matches current url with more complex regex pattern', async () => {
145
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '^/test$' })
146
+
147
+ render(page)
148
+
149
+ await new Promise(res => setTimeout(res, 100)) // Await all fetches
150
+ expect(pollLinkInjections).not.toHaveBeenCalled()
151
+ })
152
+
153
+ it('Should not inject if config returns an error', async () => {
154
+ vi.mocked(fetchConfig).mockRejectedValueOnce('null')
155
+
156
+ render(page)
157
+
158
+ await new Promise(res => setTimeout(res, 100)) // Await all fetches
159
+ expect(pollLinkInjections).not.toHaveBeenCalled()
160
+ expect(track).toHaveBeenCalledWith(TrackingEvent.FetchingConfigFailed)
161
+ })
162
+
163
+ it('Should inject if config exclude_urls_pattern does not match current url', async () => {
164
+ vi.mocked(fetchConfig).mockResolvedValueOnce({ exclude_urls_pattern: '/something-else' })
165
+
166
+ render(page)
167
+
168
+ await new Promise(res => setTimeout(res, 100)) // Await all fetches
169
+ expect(pollLinkInjections).toHaveBeenCalled()
170
+ })
124
171
  })
@@ -6,18 +6,20 @@ import { generateInjection } from '../../../helpers'
6
6
 
7
7
  describe('AIIndicator.svelte', () => {
8
8
  it('Should should running state by default', () => {
9
- const { getByText } = render(AIIndicator, { onadd: () => null })
9
+ const { getByText, getByTestId } = render(AIIndicator, { onadd: () => null })
10
10
 
11
11
  expect(getByText('AI links are currently processing.', { exact: false })).toBeTruthy()
12
+ expect(getByTestId('loading-bar')).toBeTruthy()
12
13
  })
13
14
 
14
15
  it('Should show completed state with text depending on length of given array', async () => {
15
- const { getByText, component } = render(AIIndicator, { onadd: () => null })
16
+ const { getByText, component, queryByTestId } = render(AIIndicator, { onadd: () => null })
16
17
 
17
18
  component.notifyUserOfNewState([generateInjection('Some sentence', 'Some title')])
18
19
  await waitFor(() => {
19
20
  expect(getByText('AI links are ready.', { exact: false })).toBeTruthy()
20
21
  expect(getByText('1 New link was found.')).toBeTruthy()
22
+ expect(queryByTestId('loading-bar')).not.toBeTruthy()
21
23
  })
22
24
 
23
25
  component.notifyUserOfNewState([generateInjection('Some sentence', 'Some title'), generateInjection('Some sentence', 'Some title')])
@@ -29,6 +31,7 @@ describe('AIIndicator.svelte', () => {
29
31
  component.notifyUserOfNewState([])
30
32
  await waitFor(() => {
31
33
  expect(getByText('AI links finished running, but no new links were found.')).toBeTruthy()
34
+ expect(queryByTestId('loading-bar')).not.toBeTruthy()
32
35
  })
33
36
  })
34
37
 
@@ -6,7 +6,7 @@ import DragHandle from '../../../../routes/components/Editorial/DragHandle.svelt
6
6
  describe('DragHandle.svelte', () => {
7
7
  it('Should position the given element based on the given position', () => {
8
8
  document.body.innerHTML = '<div></div>'
9
- const element = document.querySelector('div')
9
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
10
10
  render(DragHandle, { element, position: { x: 50, y: 30 } })
11
11
 
12
12
  expect(element?.style.right).toBe('50px')
@@ -15,7 +15,7 @@ describe('DragHandle.svelte', () => {
15
15
 
16
16
  it('Should limit the position to be visible on the screen', () => {
17
17
  document.body.innerHTML = '<div></div>'
18
- const element = document.querySelector('div')
18
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
19
19
  render(DragHandle, { element, position: { x: 5000, y: 5000 } })
20
20
 
21
21
  expect(element?.style.right).toBe(window.innerWidth + 'px')
@@ -24,7 +24,7 @@ describe('DragHandle.svelte', () => {
24
24
 
25
25
  it('Should limit the position to the given limit', () => {
26
26
  document.body.innerHTML = '<div></div>'
27
- const element = document.querySelector('div')
27
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
28
28
  render(DragHandle, { element, position: { x: 0, y: 0 }, limit: { x: 10, y: 10 } })
29
29
 
30
30
  expect(element?.style.right).toBe('10px')
@@ -33,7 +33,7 @@ describe('DragHandle.svelte', () => {
33
33
 
34
34
  it('Should limit the position to both the screen size and the given limit', () => {
35
35
  document.body.innerHTML = '<div></div>'
36
- const element = document.querySelector('div')
36
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
37
37
  render(DragHandle, { element, position: { x: 5000, y: 5000 }, limit: { x: 10, y: 10 } })
38
38
 
39
39
  expect(element?.style.right).toBe(window.innerWidth - 10 + 'px')
@@ -44,7 +44,7 @@ describe('DragHandle.svelte', () => {
44
44
  document.body.innerHTML = '<div></div>'
45
45
 
46
46
  const onchange = vi.fn()
47
- const element = document.querySelector('div')
47
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
48
48
  render(DragHandle, { element, position: { x: 5000, y: 5000 }, onchange })
49
49
 
50
50
  expect(onchange).toHaveBeenCalledWith({
@@ -4,12 +4,19 @@ import { describe, expect, it, vi } from 'vitest'
4
4
  import Editor from '../../../../routes/components/Editorial/Editor.svelte'
5
5
  import { title } from '$lib/fakeData'
6
6
  import { generateInjection } from '../../../helpers'
7
+ import { track } from '$lib/tracking'
8
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
7
9
 
8
10
  vi.mock('$lib/api', () => ({
9
11
  saveLinkInjections: vi.fn(),
10
12
  }))
11
13
 
14
+ vi.mock('$lib/tracking', () => ({
15
+ track: vi.fn(),
16
+ }))
17
+
12
18
  describe('Editor.svelte', () => {
19
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
13
20
  const linkInjections = [
14
21
  generateInjection('This is a sentence with an injection.', 'an injection'),
15
22
  generateInjection('This is a sentence with an injection.', 'a sentence'),
@@ -21,12 +28,51 @@ describe('Editor.svelte', () => {
21
28
  expect(getByText(linkInjections.length)).toBeTruthy()
22
29
  })
23
30
 
31
+ it('Should show the number of injections correctly in relation to injections that will be filtered out', () => {
32
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
33
+ const linkInjections = [
34
+ { ...generateInjection('Sentence', 'word'), failed: true },
35
+ { ...generateInjection('Sentence', 'word'), duplicate: true },
36
+ { ...generateInjection('Sentence', 'word'), removed: true },
37
+ { ...generateInjection('Sentence', 'word'), title_details: undefined },
38
+ { ...generateInjection('Sentence', 'word'), manual: true, failed: true },
39
+ { ...generateInjection('Sentence', 'word'), manual: false, failed: true },
40
+ ]
41
+
42
+ const { getByText } = render(Editor, { linkInjections })
43
+
44
+ expect(getByText(1)).toBeTruthy()
45
+ })
46
+
24
47
  it('Should show injections', async () => {
25
48
  const { getAllByText } = render(Editor, { linkInjections })
26
49
 
27
50
  expect(getAllByText(title.title)).toHaveLength(2)
28
51
  })
29
52
 
53
+ it('Should not show injections that are filtered out', () => {
54
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
55
+ const linkInjections = [
56
+ { ...generateInjection('Sentence', 'word') },
57
+ { ...generateInjection('Sentence', 'word'), failed: true },
58
+ { ...generateInjection('Sentence', 'word'), duplicate: true },
59
+ { ...generateInjection('Sentence', 'word'), removed: true },
60
+ { ...generateInjection('Sentence', 'word'), title_details: undefined },
61
+ { ...generateInjection('Sentence', 'word'), manual: true, failed: true },
62
+ { ...generateInjection('Sentence', 'word'), manual: false, failed: true },
63
+ ]
64
+
65
+ const { getAllByText } = render(Editor, { linkInjections })
66
+
67
+ expect(getAllByText(title.title)).toHaveLength(2)
68
+ expect(track).toHaveBeenCalledWith(TrackingEvent.TotalInjectionsCount, null, {
69
+ total: '7',
70
+ failed_automatic: '2',
71
+ failed_manual: '1',
72
+ final_injected: '2',
73
+ })
74
+ })
75
+
30
76
  it('Should display a loading state when loading is true', async () => {
31
77
  const { getByText, container } = render(Editor, { linkInjections: [], loading: true })
32
78
 
@@ -65,4 +111,27 @@ describe('Editor.svelte', () => {
65
111
 
66
112
  expect(getByText('No links available', { exact: false })).toBeTruthy()
67
113
  })
114
+
115
+ it('Should sort injections based on their named and .failed state', () => {
116
+ const linkInjections = [
117
+ { ...generateInjection('Sentence', 'c'), manual: true, title_details: { ...title, title: 'c' } },
118
+ { ...generateInjection('Sentence', 'b'), manual: true, title_details: { ...title, title: 'b' } },
119
+ { ...generateInjection('Sentence', 'a'), manual: true, failed: true, title_details: { ...title, title: 'a' } },
120
+ { ...generateInjection('Sentence', 'b'), manual: true, title_details: { ...title, title: 'e' } },
121
+ { ...generateInjection('Sentence', 'b'), manual: true, title_details: { ...title, title: 'd' } },
122
+ ]
123
+
124
+ const { container } = render(Editor, { linkInjections })
125
+
126
+ // @ts-ignore
127
+ expect(container.querySelectorAll('.item')[0].querySelector('.title').innerText).toBe('b')
128
+ // @ts-ignore
129
+ expect(container.querySelectorAll('.item')[1].querySelector('.title').innerText).toBe('c')
130
+ // @ts-ignore
131
+ expect(container.querySelectorAll('.item')[2].querySelector('.title').innerText).toBe('d')
132
+ // @ts-ignore
133
+ expect(container.querySelectorAll('.item')[3].querySelector('.title').innerText).toBe('e')
134
+ // @ts-ignore
135
+ expect(container.querySelectorAll('.item')[4].querySelector('.title').innerText).toBe('a')
136
+ })
68
137
  })
@@ -3,7 +3,6 @@ import { describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import EditorItem from '../../../../routes/components/Editorial/EditorItem.svelte'
5
5
  import { injectLinksInDocument } from '$lib/linkInjection'
6
- import { linkInjections, title } from '$lib/fakeData'
7
6
  import { track } from '$lib/tracking'
8
7
  import { generateInjection } from '../../../helpers'
9
8
 
@@ -116,19 +115,29 @@ describe('EditorItem.svelte', () => {
116
115
  expect(track).toHaveBeenCalled()
117
116
  })
118
117
 
119
- it('Should show length of title if length is given', () => {
120
- const title_details = { ...linkInjection.title_details, length: 20 }
121
- const { getByTestId, getByText } = render(EditorItem, { linkInjection: { ...linkInjection, title_details } })
118
+ it('Should show sentence of given injection', async () => {
119
+ const { container } = render(EditorItem, { linkInjection })
120
+
121
+ expect(/** @type {HTMLElement} */ (container.querySelector('.sentence')).innerText).toContain(linkInjection.sentence)
122
+ })
122
123
 
123
- expect(getByTestId('length')).toBeTruthy()
124
- expect(getByText('20m')).toBeTruthy()
124
+ it('Should not show expand button for sentence if sentence is too short to be expanded', async () => {
125
+ const { queryByText } = render(EditorItem, { linkInjection })
126
+
127
+ expect(queryByText('More')).not.toBeTruthy()
125
128
  })
126
129
 
127
- it('Should not show length of title if no length is given', () => {
128
- const title_details = { ...linkInjection.title_details, length: null }
129
- const { queryByTestId } = render(EditorItem, { linkInjection: { ...linkInjection, title_details } })
130
+ it('Should expand and collapse sentence after clicking button', async () => {
131
+ const longLinkInjection = { ...linkInjection, sentence: 'Some long sentence with a word in it that will be truncated if its long enough to be truncated' }
132
+ const { container, getByText } = render(EditorItem, { linkInjection: longLinkInjection })
133
+
134
+ expect(/** @type {HTMLElement} */ (container.querySelector('.sentence')).innerText).not.toContain(longLinkInjection.sentence)
135
+
136
+ await fireEvent.click(getByText('More'))
137
+ expect(/** @type {HTMLElement} */ (container.querySelector('.sentence')).innerText).toContain(longLinkInjection.sentence)
130
138
 
131
- expect(queryByTestId('length')).not.toBeTruthy()
139
+ await fireEvent.click(getByText('Less'))
140
+ expect(/** @type {HTMLElement} */ (container.querySelector('.sentence')).innerText).not.toContain(longLinkInjection.sentence)
132
141
  })
133
142
 
134
143
  it('Should call given onremove function when remove button is clicked', async () => {
@@ -140,4 +149,19 @@ describe('EditorItem.svelte', () => {
140
149
 
141
150
  expect(onremove).toHaveBeenCalled()
142
151
  })
152
+
153
+ it('Should display an inactive injection as being so with text and classname', async () => {
154
+ const { getByText, container } = render(EditorItem, { linkInjection: { ...linkInjection, inactive: true } })
155
+
156
+ expect(getByText('Inactive')).toBeTruthy()
157
+ expect(container.querySelector('.inactive')).toBeTruthy()
158
+ })
159
+
160
+ it('Should not display an active injection as being inactive', async () => {
161
+ const { queryByText, container } = render(EditorItem, { linkInjection })
162
+
163
+ expect(queryByText('Inactive')).not.toBeTruthy()
164
+ expect(queryByText('Visible')).toBeTruthy()
165
+ expect(container.querySelector('.inactive')).not.toBeTruthy()
166
+ })
143
167
  })
@@ -27,6 +27,7 @@ describe('ManualInjection.svelte', () => {
27
27
  endOffset: 0,
28
28
  }),
29
29
  anchorNode: container,
30
+ focusNode: container,
30
31
  }))
31
32
  })
32
33
 
@@ -82,7 +83,7 @@ describe('ManualInjection.svelte', () => {
82
83
  it('Should select content outside of element if content is too short', async () => {
83
84
  vi.mocked(searchTitles).mockResolvedValueOnce([title])
84
85
 
85
- document.body.innerHTML = '<div>Some <strong>text</strong> in a sentence</div>'
86
+ document.body.innerHTML = '<div>Some other sentence. Some <strong><span>text</span></strong> in a sentence</div>'
86
87
 
87
88
  const container = document.querySelector('strong')
88
89
 
@@ -96,6 +97,7 @@ describe('ManualInjection.svelte', () => {
96
97
  endOffset: 0,
97
98
  }),
98
99
  anchorNode: container,
100
+ focusNode: container,
99
101
  }))
100
102
 
101
103
  const onsave = vi.fn()
@@ -136,6 +138,7 @@ describe('ManualInjection.svelte', () => {
136
138
  endOffset: 0,
137
139
  }),
138
140
  anchorNode: container,
141
+ focusNode: container,
139
142
  }))
140
143
 
141
144
  await fireEvent.click(window)
@@ -156,6 +159,7 @@ describe('ManualInjection.svelte', () => {
156
159
  endOffset: 0,
157
160
  }),
158
161
  anchorNode: container,
162
+ focusNode: container,
159
163
  }))
160
164
 
161
165
  ;(render(ManualInjection, { htmlString: document.body.innerHTML, onsave }))
@@ -0,0 +1,45 @@
1
+ import { render } from '@testing-library/svelte'
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import ResizeHandle from '../../../../routes/components/Editorial/ResizeHandle.svelte'
5
+
6
+ describe('ResizeHandle.svelte', () => {
7
+ beforeEach(() => {
8
+ // @ts-ignore
9
+ window.HTMLElement.prototype.getBoundingClientRect = () => ({ bottom: window.innerHeight })
10
+ })
11
+
12
+ it('Should set the height of the given element based on the given height', () => {
13
+ document.body.innerHTML = '<div></div>'
14
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
15
+ render(ResizeHandle, { element, height: 50 })
16
+
17
+ expect(element?.style.height).toBe('50px')
18
+ })
19
+
20
+ it('Should limit the height to be visible on the screen', () => {
21
+ document.body.innerHTML = '<div></div>'
22
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
23
+ render(ResizeHandle, { element, height: 5000 })
24
+
25
+ expect(element?.style.height).toBe(window.innerHeight - 32 + 'px')
26
+ })
27
+
28
+ it('Should limit the height to elements minimum height', () => {
29
+ document.body.innerHTML = '<div style="min-height: 5px"></div>'
30
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
31
+ render(ResizeHandle, { element, height: 0 })
32
+
33
+ expect(element?.style.height).toBe('5px')
34
+ })
35
+
36
+ it('Should fire given onchange function when height changes', () => {
37
+ document.body.innerHTML = '<div></div>'
38
+
39
+ const onchange = vi.fn()
40
+ const element = /** @type {HTMLElement} */ (document.querySelector('div'))
41
+ render(ResizeHandle, { element, height: window.innerHeight / 2, onchange })
42
+
43
+ expect(onchange).toHaveBeenCalledWith(window.innerHeight / 2)
44
+ })
45
+ })
@@ -35,24 +35,26 @@ describe('TitleSearch.svelte', () => {
35
35
  it('Should show results when given', async () => {
36
36
  vi.mocked(searchTitles).mockResolvedValueOnce([title])
37
37
 
38
- const { getByRole, getByText } = render(TitleSearch)
38
+ const { getByRole, getByText, getByTestId } = render(TitleSearch)
39
39
 
40
40
  await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
41
41
 
42
+ expect(getByTestId('search-results')).toBeTruthy()
42
43
  expect(getByText(title.title)).toBeTruthy()
43
44
  expect(getByText(title.type)).toBeTruthy()
44
45
  })
45
46
 
46
- it('Should fire the given onselect function and close the results when a result is clicked', async () => {
47
+ it('Should fire the given onselect function and close the results, and show selected result when a result is clicked', async () => {
47
48
  vi.mocked(searchTitles).mockResolvedValueOnce([title])
48
49
 
49
50
  const onselect = vi.fn()
50
- const { getByRole, queryByRole } = render(TitleSearch, { onselect })
51
+ const { getByRole, getByText, queryByTestId } = render(TitleSearch, { onselect, query: '' })
51
52
 
52
53
  await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
53
54
  await fireEvent.click(getByRole('button'))
54
55
 
55
56
  expect(onselect).toHaveBeenCalledWith(title)
56
- expect(queryByRole('button')).not.toBeTruthy()
57
+ expect(queryByTestId('search-results')).not.toBeTruthy()
58
+ expect(getByText(title.title)).toBeTruthy()
57
59
  })
58
60
  })
@@ -0,0 +1,44 @@
1
+ import { fireEvent, render } from '@testing-library/svelte'
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import TitleSearchItem from '../../../../../routes/components/Editorial/Search/TitleSearchItem.svelte'
5
+ import { title } from '$lib/fakeData'
6
+
7
+ vi.mock('$lib/search', () => ({
8
+ searchTitles: vi.fn(),
9
+ }))
10
+
11
+ describe('TitleSearchItem.svelte', () => {
12
+ beforeEach(() => {
13
+ vi.resetAllMocks()
14
+ })
15
+
16
+ it('Should fire given onclick function', async () => {
17
+ const onclick = vi.fn()
18
+ const { getByRole } = render(TitleSearchItem, { onclick, title })
19
+
20
+ await fireEvent.click(getByRole('button'))
21
+
22
+ expect(onclick).toHaveBeenCalled()
23
+ })
24
+
25
+ it('Should render title data', () => {
26
+ const { getByText } = render(TitleSearchItem, { title })
27
+
28
+ expect(getByText(title.title)).toBeTruthy()
29
+ expect(getByText(title.year)).toBeTruthy()
30
+ expect(getByText(title.type)).toBeTruthy()
31
+ })
32
+
33
+ it('Should have hoverable class by default', () => {
34
+ const { getByRole } = render(TitleSearchItem, { title })
35
+
36
+ expect(getByRole('button').classList).toContain('hoverable')
37
+ })
38
+
39
+ it('Should not have hoverable class when hoverable prop is false', () => {
40
+ const { getByRole } = render(TitleSearchItem, { title, hoverable: false })
41
+
42
+ expect(getByRole('button').classList).not.toContain('hoverable')
43
+ })
44
+ })