PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.1
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 All 35 releases
xspeed / tests-js / components / SettingsSearch.test.tsx

SettingsSearch.test.tsx in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.1, at tests-js/components/SettingsSearch.test.tsx

218 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { describe, it, expect } from 'vitest';
2 import { render, waitFor } from '@testing-library/react';
3 import { matchModules, highlight, SearchHighlighter } from '../../src/components/SettingsSearch';
4 import type { ModuleDescriptor } from '../../src/types';
5
6 function module(overrides: Partial<ModuleDescriptor>): ModuleDescriptor {
7 return {
8 slug: 'demo',
9 label: 'Demo',
10 description: '',
11 icon: 'Square',
12 schema: {},
13 settings: {},
14 ...overrides,
15 } as ModuleDescriptor;
16 }
17
18 describe('matchModules', () => {
19 it('returns every module when query is empty', () => {
20 const mods = [module({ slug: 'a' }), module({ slug: 'b' })];
21 expect(matchModules(mods, '')).toHaveLength(2);
22 });
23
24 it('matches module by label (case-insensitive)', () => {
25 const mods = [
26 module({ slug: 'cache', label: 'Cache' }),
27 module({ slug: 'minify', label: 'Minify' }),
28 ];
29 expect(matchModules(mods, 'CACHE')).toHaveLength(1);
30 expect(matchModules(mods, 'CACHE')[0].module.slug).toBe('cache');
31 });
32
33 it('matches module by description', () => {
34 const mods = [
35 module({ slug: 'lazy', label: 'Lazy Load', description: 'Defer images and iframes' }),
36 ];
37 expect(matchModules(mods, 'defer')).toHaveLength(1);
38 });
39
40 it('drills into schema fields when label/description miss', () => {
41 const mods = [
42 module({
43 slug: 'cache',
44 label: 'Cache',
45 schema: {
46 expiry_hours: { type: 'int', label: 'Cache Expiry (hours)', default: 24 },
47 },
48 }),
49 ];
50 const results = matchModules(mods, 'expiry');
51 expect(results).toHaveLength(1);
52 expect(results[0].fieldKey).toBe('expiry_hours');
53 expect(results[0].fieldLabel).toBe('Cache Expiry (hours)');
54 });
55
56 it('also searches field descriptions and option labels', () => {
57 const mods = [
58 module({
59 slug: 'cf',
60 label: 'Cloudflare',
61 schema: {
62 auth_method: {
63 type: 'enum',
64 label: 'Authentication',
65 options: ['token', 'key'],
66 option_labels: { token: 'API Token', key: 'Global API Key' },
67 default: 'token',
68 },
69 },
70 }),
71 ];
72 expect(matchModules(mods, 'global api')).toHaveLength(1);
73 });
74
75 it('returns no results when nothing matches', () => {
76 const mods = [module({ slug: 'cache', label: 'Cache' })];
77 expect(matchModules(mods, 'nonexistent')).toHaveLength(0);
78 });
79
80 it('matches by field key when label is identical to default', () => {
81 const mods = [
82 module({
83 slug: 'rum',
84 label: 'RUM',
85 schema: { sample_rate: { type: 'int', label: 'Sample rate', default: 100 } },
86 }),
87 ];
88 expect(matchModules(mods, 'sample_rate')).toHaveLength(1);
89 });
90 });
91
92 describe('highlight', () => {
93 it('returns plain text when query is empty', () => {
94 const { container } = render(<>{highlight('Cache settings', '')}</>);
95 expect(container.querySelector('mark')).toBeNull();
96 expect(container.textContent).toBe('Cache settings');
97 });
98
99 it('wraps the matched substring in <mark>', () => {
100 const { container } = render(<>{highlight('Cache settings', 'set')}</>);
101 const marks = container.querySelectorAll('mark');
102 expect(marks).toHaveLength(1);
103 expect(marks[0].textContent).toBe('set');
104 });
105
106 it('preserves original casing of the matched span', () => {
107 // Query is lowercase; the rendered <mark> still shows "Cache".
108 const { container } = render(<>{highlight('Cache settings', 'cache')}</>);
109 expect(container.querySelector('mark')?.textContent).toBe('Cache');
110 });
111
112 it('highlights every occurrence', () => {
113 const { container } = render(<>{highlight('cache cache cache', 'cache')}</>);
114 expect(container.querySelectorAll('mark')).toHaveLength(3);
115 });
116
117 it('escapes regex metacharacters in the query', () => {
118 // "*.pdf" would explode without escaping (* is a quantifier).
119 const { container } = render(<>{highlight('Match *.pdf files', '*.pdf')}</>);
120 expect(container.querySelector('mark')?.textContent).toBe('*.pdf');
121 });
122
123 it('returns input unchanged when no match', () => {
124 const { container } = render(<>{highlight('Cache settings', 'xyz')}</>);
125 expect(container.querySelector('mark')).toBeNull();
126 expect(container.textContent).toBe('Cache settings');
127 });
128 });
129
130 describe('SearchHighlighter', () => {
131 it('wraps matching text inside arbitrary descendant elements', async () => {
132 const { container } = render(
133 <SearchHighlighter query="rocket">
134 <div>
135 <p>Migrate from WP Rocket plugin</p>
136 <p>Also supports W3 Total Cache</p>
137 </div>
138 </SearchHighlighter>
139 );
140 await waitFor(() => {
141 const marks = container.querySelectorAll('mark[data-xs-mark]');
142 expect(marks.length).toBe(1);
143 expect(marks[0].textContent).toBe('Rocket');
144 });
145 });
146
147 it('matches multiple occurrences case-insensitively', async () => {
148 const { container } = render(
149 <SearchHighlighter query="cache">
150 <div>
151 <span>Cache control</span>
152 <span>WP Super Cache</span>
153 <span>cache size</span>
154 </div>
155 </SearchHighlighter>
156 );
157 await waitFor(() => {
158 expect(container.querySelectorAll('mark[data-xs-mark]').length).toBe(3);
159 });
160 });
161
162 it('skips input + textarea + pre + code subtrees', async () => {
163 const { container } = render(
164 <SearchHighlighter query="cache">
165 <div>
166 <p>Cache description</p>
167 <input type="text" aria-label="test-input" defaultValue="cache value" />
168 <textarea aria-label="test-textarea" defaultValue="cache notes" />
169 <pre>cache code</pre>
170 <code>cache literal</code>
171 </div>
172 </SearchHighlighter>
173 );
174 await waitFor(() => {
175 const marks = container.querySelectorAll('mark[data-xs-mark]');
176 // Only the <p> text — input/textarea/pre/code untouched.
177 expect(marks.length).toBe(1);
178 expect(marks[0].textContent).toBe('Cache');
179 });
180 // Form control values remain intact.
181 const input = container.querySelector('input') as HTMLInputElement;
182 expect(input.value).toBe('cache value');
183 });
184
185 it('empty query does not insert any marks', async () => {
186 const { container } = render(
187 <SearchHighlighter query="">
188 <div>Cache description</div>
189 </SearchHighlighter>
190 );
191 await waitFor(() => {
192 expect(container.querySelectorAll('mark[data-xs-mark]').length).toBe(0);
193 });
194 });
195
196 it('unwraps previous marks before applying new ones on query change', async () => {
197 const { container, rerender } = render(
198 <SearchHighlighter query="cache">
199 <div>Cache and minify settings</div>
200 </SearchHighlighter>
201 );
202 await waitFor(() => {
203 expect(container.querySelectorAll('mark[data-xs-mark]').length).toBe(1);
204 });
205
206 rerender(
207 <SearchHighlighter query="minify">
208 <div>Cache and minify settings</div>
209 </SearchHighlighter>
210 );
211 await waitFor(() => {
212 const marks = container.querySelectorAll('mark[data-xs-mark]');
213 expect(marks.length).toBe(1);
214 expect(marks[0].textContent).toBe('minify');
215 });
216 });
217 });
218