Skip to content

Commit 9444d3e

Browse files
committed
test: cover comment rendering and edge-route states
1 parent a8ef232 commit 9444d3e

File tree

7 files changed

+1138
-1
lines changed

7 files changed

+1138
-1
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import * as React from 'react';
2+
import { createElement } from 'react';
3+
import { createRoot, type Root } from 'react-dom/client';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
import CatalogSearch from '../catalog-search';
6+
7+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
8+
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
9+
10+
const testState = vi.hoisted(() => ({
11+
clearSearchFilterMock: vi.fn(),
12+
debounceCancelMock: vi.fn(),
13+
isMobile: false,
14+
location: {
15+
pathname: '/mu/catalog',
16+
search: '',
17+
},
18+
navigateMock: vi.fn(),
19+
setSearchFilterMock: vi.fn(),
20+
}));
21+
22+
vi.mock('react-i18next', () => ({
23+
useTranslation: () => ({
24+
t: (key: string) => key,
25+
}),
26+
}));
27+
28+
vi.mock('react-router-dom', async () => {
29+
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
30+
return {
31+
...actual,
32+
useLocation: () => testState.location,
33+
useNavigate: () => testState.navigateMock,
34+
};
35+
});
36+
37+
vi.mock('../../../hooks/use-is-mobile', () => ({
38+
default: () => testState.isMobile,
39+
}));
40+
41+
vi.mock('../../../stores/use-catalog-filters-store', () => ({
42+
default: () => ({
43+
clearSearchFilter: testState.clearSearchFilterMock,
44+
setSearchFilter: testState.setSearchFilterMock,
45+
}),
46+
}));
47+
48+
vi.mock('lodash/debounce', () => ({
49+
default: <T extends (...args: any[]) => void>(fn: T) => {
50+
const wrapped = ((...args: Parameters<T>) => fn(...args)) as T & { cancel: () => void };
51+
wrapped.cancel = () => testState.debounceCancelMock();
52+
return wrapped;
53+
},
54+
}));
55+
56+
let container: HTMLDivElement;
57+
let root: Root;
58+
59+
const renderSearch = async () => {
60+
await act(async () => {
61+
root.render(createElement(CatalogSearch));
62+
});
63+
await act(async () => {
64+
await Promise.resolve();
65+
});
66+
};
67+
68+
const clickElement = async (element: Element | null) => {
69+
expect(element).toBeTruthy();
70+
await act(async () => {
71+
element?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
72+
});
73+
};
74+
75+
const querySearchButton = () => Array.from(container.querySelectorAll('span')).find((node) => node.textContent === 'search') ?? null;
76+
const queryCloseButton = () => Array.from(container.querySelectorAll('span')).find((node) => node.textContent === '✖') ?? null;
77+
const queryInput = () => container.querySelector('input');
78+
79+
const dispatchInput = async (element: HTMLInputElement, value: string) => {
80+
await act(async () => {
81+
const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
82+
descriptor?.set?.call(element, value);
83+
element.dispatchEvent(new Event('input', { bubbles: true }));
84+
element.dispatchEvent(new Event('change', { bubbles: true }));
85+
});
86+
};
87+
88+
describe('CatalogSearch', () => {
89+
beforeEach(() => {
90+
vi.clearAllMocks();
91+
testState.clearSearchFilterMock.mockReset();
92+
testState.debounceCancelMock.mockReset();
93+
testState.isMobile = false;
94+
testState.location = {
95+
pathname: '/mu/catalog',
96+
search: '',
97+
};
98+
testState.navigateMock.mockReset();
99+
testState.setSearchFilterMock.mockReset();
100+
101+
container = document.createElement('div');
102+
document.body.appendChild(container);
103+
root = createRoot(container);
104+
});
105+
106+
afterEach(() => {
107+
act(() => root.unmount());
108+
container.remove();
109+
});
110+
111+
it('opens from the query param and seeds the catalog search filter', async () => {
112+
testState.location = {
113+
pathname: '/mu/catalog',
114+
search: '?q=linux',
115+
};
116+
117+
await renderSearch();
118+
119+
expect(testState.setSearchFilterMock).toHaveBeenCalledWith('linux');
120+
expect(queryInput()).toBeTruthy();
121+
expect(queryInput()?.getAttribute('value')).toBe('linux');
122+
});
123+
124+
it('toggles the search UI, updates the filter and URL, and closes via Escape', async () => {
125+
await renderSearch();
126+
127+
await clickElement(querySearchButton());
128+
const input = queryInput();
129+
expect(input).toBeTruthy();
130+
131+
if (!input) {
132+
throw new Error('Expected catalog search input');
133+
}
134+
135+
await dispatchInput(input, 'web3');
136+
137+
expect(testState.setSearchFilterMock).toHaveBeenCalledWith('web3');
138+
expect(testState.navigateMock).toHaveBeenCalledWith('/mu/catalog?q=web3', { replace: true });
139+
140+
await act(async () => {
141+
input.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }));
142+
});
143+
144+
expect(testState.clearSearchFilterMock).toHaveBeenCalled();
145+
expect(testState.navigateMock).toHaveBeenLastCalledWith('/mu/catalog', { replace: true });
146+
expect(queryInput()).toBeNull();
147+
});
148+
149+
it('closes through the toggle button and cancels the debounced updater on unmount', async () => {
150+
testState.isMobile = true;
151+
152+
await renderSearch();
153+
await clickElement(querySearchButton());
154+
expect(queryInput()).toBeTruthy();
155+
156+
await clickElement(querySearchButton());
157+
158+
expect(testState.clearSearchFilterMock).toHaveBeenCalled();
159+
expect(testState.navigateMock).toHaveBeenCalledWith('/mu/catalog', { replace: true });
160+
expect(queryInput()).toBeNull();
161+
162+
act(() => root.unmount());
163+
expect(testState.debounceCancelMock).toHaveBeenCalled();
164+
});
165+
166+
it('closes with the explicit close button after typing', async () => {
167+
await renderSearch();
168+
await clickElement(querySearchButton());
169+
170+
const input = queryInput();
171+
expect(input).toBeTruthy();
172+
if (!input) {
173+
throw new Error('Expected catalog search input');
174+
}
175+
176+
await dispatchInput(input, 'cats');
177+
await clickElement(queryCloseButton());
178+
179+
expect(testState.clearSearchFilterMock).toHaveBeenCalled();
180+
expect(testState.navigateMock).toHaveBeenLastCalledWith('/mu/catalog', { replace: true });
181+
expect(queryInput()).toBeNull();
182+
});
183+
});

0 commit comments

Comments
 (0)