Skip to content

Commit b662e5d

Browse files
committed
Test(web): test search highlighting at the SearchList boundary
Exercise both SearchList display branches instead of the HighlightedText helper, so the XSS tests fail on the pre-fix code for the intended reason (a real <img> element is created) and HighlightedText no longer needs to be exported only for tests. Drop the highlightMatch unit tests, which asserted the exact parts array rather than rendered output. Signed-off-by: mday-io <mdaytn@gmail.com>
1 parent 34ddb9d commit b662e5d

2 files changed

Lines changed: 54 additions & 53 deletions

File tree

‎web/client/src/library/components/search/SearchList.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ export default function SearchList<
281281
)
282282
}
283283

284-
export function HighlightedText({
284+
function HighlightedText({
285285
source,
286286
search,
287287
}: {
Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,66 @@
1-
import { describe, it, expect } from 'vitest'
2-
import { render } from '../../../../tests/utils'
3-
import { HighlightedText } from '../SearchList'
4-
import { highlightMatch } from '../help'
1+
import { describe, it, expect, vi } from 'vitest'
2+
import { MemoryRouter } from 'react-router'
3+
import { render, fireEvent } from '../../../../tests/utils'
4+
import SearchList from '../SearchList'
55

6-
describe('highlightMatch', () => {
7-
it('should split source into matched and unmatched parts', () => {
8-
expect(highlightMatch('abcab', 'ab')).toEqual([
9-
{ text: 'ab', isMatch: true },
10-
{ text: 'c', isMatch: false },
11-
{ text: 'ab', isMatch: true },
12-
])
13-
})
6+
// jsdom has no ResizeObserver, which TBKResizeObserver needs to mount
7+
vi.stubGlobal(
8+
'ResizeObserver',
9+
class {
10+
observe(): void {}
11+
unobserve(): void {}
12+
disconnect(): void {}
13+
},
14+
)
1415

15-
it('should handle adjacent matches and matches at the edges', () => {
16-
expect(highlightMatch('abab', 'ab')).toEqual([
17-
{ text: 'ab', isMatch: true },
18-
{ text: 'ab', isMatch: true },
19-
])
20-
expect(highlightMatch('xab', 'ab')).toEqual([
21-
{ text: 'x', isMatch: false },
22-
{ text: 'ab', isMatch: true },
23-
])
24-
})
16+
const PAYLOAD = '<img src=x onerror="alert(1)">'
17+
18+
function renderSearch(
19+
name: string,
20+
search: string,
21+
showIndex: boolean,
22+
): HTMLElement {
23+
const { container } = render(
24+
<MemoryRouter>
25+
<SearchList<{ name: string }>
26+
list={[{ name }]}
27+
searchBy="name"
28+
displayBy="name"
29+
showIndex={showIndex}
30+
/>
31+
</MemoryRouter>,
32+
)
2533

26-
it('should return no parts for an empty source', () => {
27-
expect(highlightMatch('', 'ab')).toEqual([])
34+
fireEvent.input(container.querySelector('input')!, {
35+
target: { value: search },
2836
})
2937

30-
it('should return the whole source when there is no match', () => {
31-
expect(highlightMatch('abc', '')).toEqual([{ text: 'abc', isMatch: false }])
32-
expect(highlightMatch('abc', 'x')).toEqual([
33-
{ text: 'abc', isMatch: false },
34-
])
38+
return container.querySelector('[role="menuitem"]')!
39+
}
40+
41+
describe.each([true, false])('SearchList with showIndex=%s', showIndex => {
42+
it('should render markup in project text as text', () => {
43+
const name = `${PAYLOAD}model`
44+
const result = renderSearch(name, 'model', showIndex)
45+
46+
expect(result.querySelector('img')).toBeNull()
47+
expect(result.textContent).toContain(name)
48+
expect(result.querySelector('b')?.textContent).toBe('model')
3549
})
36-
})
3750

38-
describe('HighlightedText', () => {
39-
it('should render project text as text, not HTML', () => {
40-
const source = '<img src=x onerror="alert(1)">model'
41-
const { container } = render(
42-
<HighlightedText
43-
source={source}
44-
search="model"
45-
/>,
46-
)
51+
it('should render markup in the search string as text', () => {
52+
const result = renderSearch(`a${PAYLOAD}b`, PAYLOAD, showIndex)
4753

48-
expect(container.querySelector('img')).toBeNull()
49-
expect(container.textContent).toBe(source)
50-
expect(container.querySelector('b')?.textContent).toBe('model')
54+
expect(result.querySelector('img')).toBeNull()
55+
expect(result.querySelector('b')?.textContent).toBe(PAYLOAD)
5156
})
5257

53-
it('should not interpret markup in the search string', () => {
54-
const search = '<img src=x onerror="alert(1)">'
55-
const { container } = render(
56-
<HighlightedText
57-
source={`a${search}b`}
58-
search={search}
59-
/>,
60-
)
58+
it('should highlight every match, including adjacent and trailing ones', () => {
59+
const result = renderSearch('ababxab', 'ab', showIndex)
6160

62-
expect(container.querySelector('img')).toBeNull()
63-
expect(container.querySelector('b')?.textContent).toBe(search)
61+
expect(
62+
Array.from(result.querySelectorAll('b'), b => b.textContent),
63+
).toEqual(['ab', 'ab', 'ab'])
64+
expect(result.textContent).toContain('ababxab')
6465
})
6566
})

0 commit comments

Comments
 (0)