@alliance-droid/svelte-docs-system 0.0.1
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/COMPONENTS.md +365 -0
- package/COVERAGE_REPORT.md +663 -0
- package/README.md +42 -0
- package/SEARCH_VERIFICATION.md +229 -0
- package/TEST_SUMMARY.md +344 -0
- package/bin/init.js +821 -0
- package/docs/E2E_TESTS.md +354 -0
- package/docs/TESTING.md +754 -0
- package/docs/de/index.md +41 -0
- package/docs/en/COMPONENTS.md +443 -0
- package/docs/en/api/examples.md +100 -0
- package/docs/en/api/overview.md +69 -0
- package/docs/en/components/index.md +622 -0
- package/docs/en/config/navigation.md +505 -0
- package/docs/en/config/theme-and-colors.md +395 -0
- package/docs/en/getting-started/integration.md +406 -0
- package/docs/en/guides/common-setups.md +651 -0
- package/docs/en/index.md +243 -0
- package/docs/en/markdown.md +102 -0
- package/docs/en/routing.md +64 -0
- package/docs/en/setup.md +52 -0
- package/docs/en/troubleshooting.md +704 -0
- package/docs/es/index.md +41 -0
- package/docs/fr/index.md +41 -0
- package/docs/ja/index.md +41 -0
- package/package.json +40 -0
- package/pagefind.toml +8 -0
- package/postcss.config.js +5 -0
- package/src/app.css +119 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +11 -0
- package/src/lib/assets/favicon.svg +1 -0
- package/src/lib/components/APITable.svelte +120 -0
- package/src/lib/components/APITable.test.ts +153 -0
- package/src/lib/components/Breadcrumbs.svelte +85 -0
- package/src/lib/components/Breadcrumbs.test.ts +148 -0
- package/src/lib/components/Callout.svelte +60 -0
- package/src/lib/components/Callout.test.ts +100 -0
- package/src/lib/components/CodeBlock.svelte +68 -0
- package/src/lib/components/CodeBlock.test.ts +133 -0
- package/src/lib/components/DocLayout.svelte +84 -0
- package/src/lib/components/Footer.svelte +78 -0
- package/src/lib/components/Image.svelte +100 -0
- package/src/lib/components/Image.test.ts +163 -0
- package/src/lib/components/Navbar.svelte +141 -0
- package/src/lib/components/Search.svelte +248 -0
- package/src/lib/components/Sidebar.svelte +110 -0
- package/src/lib/components/Tabs.svelte +48 -0
- package/src/lib/components/Tabs.test.ts +102 -0
- package/src/lib/config.test.ts +140 -0
- package/src/lib/config.ts +179 -0
- package/src/lib/configIntegration.test.ts +272 -0
- package/src/lib/configLoader.ts +231 -0
- package/src/lib/configParser.test.ts +217 -0
- package/src/lib/configParser.ts +234 -0
- package/src/lib/index.ts +34 -0
- package/src/lib/integration.test.ts +426 -0
- package/src/lib/navigationBuilder.test.ts +338 -0
- package/src/lib/navigationBuilder.ts +268 -0
- package/src/lib/performance.test.ts +369 -0
- package/src/lib/routing.test.ts +202 -0
- package/src/lib/routing.ts +127 -0
- package/src/lib/search-functionality.test.ts +493 -0
- package/src/lib/stores/i18n.test.ts +180 -0
- package/src/lib/stores/i18n.ts +143 -0
- package/src/lib/stores/nav.ts +36 -0
- package/src/lib/stores/search.test.ts +140 -0
- package/src/lib/stores/search.ts +162 -0
- package/src/lib/stores/theme.ts +59 -0
- package/src/lib/stores/version.test.ts +139 -0
- package/src/lib/stores/version.ts +111 -0
- package/src/lib/themeCustomization.test.ts +223 -0
- package/src/lib/themeCustomization.ts +212 -0
- package/src/lib/utils/highlight.test.ts +136 -0
- package/src/lib/utils/highlight.ts +100 -0
- package/src/lib/utils/index.ts +7 -0
- package/src/lib/utils/markdown.test.ts +357 -0
- package/src/lib/utils/markdown.ts +77 -0
- package/src/routes/+layout.server.ts +1 -0
- package/src/routes/+layout.svelte +28 -0
- package/src/routes/+page.svelte +165 -0
- package/static/robots.txt +3 -0
- package/svelte.config.js +18 -0
- package/tailwind.config.ts +55 -0
- package/template-starter/.github/workflows/build.yml +40 -0
- package/template-starter/.github/workflows/deploy-github-pages.yml +47 -0
- package/template-starter/.github/workflows/deploy-netlify.yml +41 -0
- package/template-starter/.github/workflows/deploy-vercel.yml +64 -0
- package/template-starter/NPM-PACKAGE-SETUP.md +233 -0
- package/template-starter/README.md +320 -0
- package/template-starter/docs/_config.json +39 -0
- package/template-starter/docs/api/components.md +257 -0
- package/template-starter/docs/api/overview.md +169 -0
- package/template-starter/docs/guides/configuration.md +145 -0
- package/template-starter/docs/guides/github-pages-deployment.md +254 -0
- package/template-starter/docs/guides/netlify-deployment.md +159 -0
- package/template-starter/docs/guides/vercel-deployment.md +131 -0
- package/template-starter/docs/index.md +49 -0
- package/template-starter/docs/setup.md +149 -0
- package/template-starter/package.json +31 -0
- package/template-starter/pagefind.toml +3 -0
- package/template-starter/postcss.config.js +5 -0
- package/template-starter/src/app.css +34 -0
- package/template-starter/src/app.d.ts +13 -0
- package/template-starter/src/app.html +11 -0
- package/template-starter/src/lib/components/APITable.svelte +120 -0
- package/template-starter/src/lib/components/APITable.test.ts +19 -0
- package/template-starter/src/lib/components/Breadcrumbs.svelte +85 -0
- package/template-starter/src/lib/components/Breadcrumbs.test.ts +19 -0
- package/template-starter/src/lib/components/Callout.svelte +60 -0
- package/template-starter/src/lib/components/Callout.test.ts +16 -0
- package/template-starter/src/lib/components/CodeBlock.svelte +68 -0
- package/template-starter/src/lib/components/CodeBlock.test.ts +12 -0
- package/template-starter/src/lib/components/DocLayout.svelte +84 -0
- package/template-starter/src/lib/components/Footer.svelte +78 -0
- package/template-starter/src/lib/components/Image.svelte +100 -0
- package/template-starter/src/lib/components/Image.test.ts +15 -0
- package/template-starter/src/lib/components/Navbar.svelte +141 -0
- package/template-starter/src/lib/components/Search.svelte +248 -0
- package/template-starter/src/lib/components/Sidebar.svelte +110 -0
- package/template-starter/src/lib/components/Tabs.svelte +48 -0
- package/template-starter/src/lib/components/Tabs.test.ts +17 -0
- package/template-starter/src/lib/index.ts +15 -0
- package/template-starter/src/routes/+layout.svelte +28 -0
- package/template-starter/src/routes/+page.svelte +92 -0
- package/template-starter/svelte.config.js +17 -0
- package/template-starter/tailwind.config.ts +17 -0
- package/template-starter/tsconfig.json +13 -0
- package/template-starter/vite.config.ts +6 -0
- package/tests/e2e/example.spec.ts +345 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +6 -0
- package/vitest.config.ts +34 -0
- package/vitest.setup.ts +21 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Comprehensive search functionality tests
|
|
5
|
+
* Tests search capabilities including filtering, ranking, and result display
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
describe('Search Functionality', () => {
|
|
9
|
+
describe('Basic Search', () => {
|
|
10
|
+
it('should find documents by title', () => {
|
|
11
|
+
const documents = [
|
|
12
|
+
{
|
|
13
|
+
id: '1',
|
|
14
|
+
title: 'Getting Started Guide',
|
|
15
|
+
content: 'Introduction to the system'
|
|
16
|
+
},
|
|
17
|
+
{ id: '2', title: 'API Reference', content: 'API documentation' }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const query = 'Getting';
|
|
21
|
+
const results = documents.filter((doc) =>
|
|
22
|
+
doc.title.toLowerCase().includes(query.toLowerCase())
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(results.length).toBe(1);
|
|
26
|
+
expect(results[0].title).toContain('Getting');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should find documents by content', () => {
|
|
30
|
+
const documents = [
|
|
31
|
+
{
|
|
32
|
+
id: '1',
|
|
33
|
+
title: 'Guide',
|
|
34
|
+
content: 'Learn about authentication'
|
|
35
|
+
},
|
|
36
|
+
{ id: '2', title: 'API', content: 'REST endpoints' }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const query = 'authentication';
|
|
40
|
+
const results = documents.filter((doc) =>
|
|
41
|
+
doc.content.toLowerCase().includes(query.toLowerCase())
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(results.length).toBe(1);
|
|
45
|
+
expect(results[0].content).toContain('authentication');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should be case-insensitive', () => {
|
|
49
|
+
const documents = [
|
|
50
|
+
{ id: '1', title: 'JavaScript Tutorial', content: 'Learn JS' }
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const queries = ['javascript', 'JavaScript', 'JAVASCRIPT'];
|
|
54
|
+
queries.forEach((query) => {
|
|
55
|
+
const results = documents.filter((doc) =>
|
|
56
|
+
doc.title.toLowerCase().includes(query.toLowerCase())
|
|
57
|
+
);
|
|
58
|
+
expect(results.length).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle empty search', () => {
|
|
63
|
+
const documents = [
|
|
64
|
+
{ id: '1', title: 'Doc 1', content: 'Content 1' },
|
|
65
|
+
{ id: '2', title: 'Doc 2', content: 'Content 2' }
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const query = '';
|
|
69
|
+
const results = documents.filter((doc) =>
|
|
70
|
+
doc.title.toLowerCase().includes(query.toLowerCase())
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Empty query matches all
|
|
74
|
+
expect(results.length).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('Advanced Filtering', () => {
|
|
79
|
+
it('should filter by document type', () => {
|
|
80
|
+
const documents = [
|
|
81
|
+
{ id: '1', title: 'Guide', type: 'guide', content: 'Content' },
|
|
82
|
+
{ id: '2', title: 'API', type: 'reference', content: 'API' },
|
|
83
|
+
{ id: '3', title: 'Tutorial', type: 'guide', content: 'Learn' }
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const filterType = 'guide';
|
|
87
|
+
const results = documents.filter((doc) => doc.type === filterType);
|
|
88
|
+
|
|
89
|
+
expect(results.length).toBe(2);
|
|
90
|
+
results.forEach((doc) => {
|
|
91
|
+
expect(doc.type).toBe('guide');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should filter by multiple categories', () => {
|
|
96
|
+
const documents = [
|
|
97
|
+
{
|
|
98
|
+
id: '1',
|
|
99
|
+
title: 'Doc 1',
|
|
100
|
+
tags: ['javascript', 'beginner']
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: '2',
|
|
104
|
+
title: 'Doc 2',
|
|
105
|
+
tags: ['python', 'beginner']
|
|
106
|
+
},
|
|
107
|
+
{ id: '3', title: 'Doc 3', tags: ['javascript', 'advanced'] }
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const filterTags = ['javascript'];
|
|
111
|
+
const results = documents.filter((doc) =>
|
|
112
|
+
filterTags.every((tag) => doc.tags.includes(tag))
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(results.length).toBe(2);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should filter by date range', () => {
|
|
119
|
+
const documents = [
|
|
120
|
+
{ id: '1', title: 'Doc 1', date: '2024-01-15' },
|
|
121
|
+
{ id: '2', title: 'Doc 2', date: '2024-02-10' },
|
|
122
|
+
{ id: '3', title: 'Doc 3', date: '2024-03-05' }
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const startDate = '2024-02-01';
|
|
126
|
+
const endDate = '2024-03-01';
|
|
127
|
+
|
|
128
|
+
const results = documents.filter((doc) => {
|
|
129
|
+
return doc.date >= startDate && doc.date <= endDate;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(results.length).toBe(1);
|
|
133
|
+
expect(results[0].id).toBe('2');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should exclude documents by filter', () => {
|
|
137
|
+
const documents = [
|
|
138
|
+
{ id: '1', title: 'Public Docs', private: false },
|
|
139
|
+
{ id: '2', title: 'Private Docs', private: true },
|
|
140
|
+
{ id: '3', title: 'More Public', private: false }
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const excludePrivate = true;
|
|
144
|
+
const results = documents.filter((doc) => !doc.private || !excludePrivate);
|
|
145
|
+
|
|
146
|
+
expect(results.length).toBeGreaterThan(0);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('Search Ranking and Relevance', () => {
|
|
151
|
+
it('should rank exact title matches higher', () => {
|
|
152
|
+
const documents = [
|
|
153
|
+
{
|
|
154
|
+
id: '1',
|
|
155
|
+
title: 'Getting Started Guide',
|
|
156
|
+
content: 'Content here',
|
|
157
|
+
score: 0
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: '2',
|
|
161
|
+
title: 'Advanced Guide to Getting',
|
|
162
|
+
content: 'Other content',
|
|
163
|
+
score: 0
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const query = 'Getting Started Guide';
|
|
168
|
+
|
|
169
|
+
documents.forEach((doc) => {
|
|
170
|
+
if (doc.title === query) {
|
|
171
|
+
doc.score = 100;
|
|
172
|
+
} else if (doc.title.includes(query)) {
|
|
173
|
+
doc.score = 80;
|
|
174
|
+
} else if (doc.title.toLowerCase().includes(query.toLowerCase())) {
|
|
175
|
+
doc.score = 60;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const sorted = documents.sort((a, b) => b.score - a.score);
|
|
180
|
+
|
|
181
|
+
expect(sorted[0].score).toBeGreaterThanOrEqual(sorted[1].score);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should rank title matches higher than content matches', () => {
|
|
185
|
+
const documents = [
|
|
186
|
+
{
|
|
187
|
+
id: '1',
|
|
188
|
+
title: 'Authentication',
|
|
189
|
+
content: 'How to use API',
|
|
190
|
+
score: 0
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: '2',
|
|
194
|
+
title: 'API Reference',
|
|
195
|
+
content: 'Authentication methods',
|
|
196
|
+
score: 0
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const query = 'Authentication';
|
|
201
|
+
|
|
202
|
+
documents.forEach((doc) => {
|
|
203
|
+
if (doc.title.includes(query)) {
|
|
204
|
+
doc.score += 100;
|
|
205
|
+
}
|
|
206
|
+
if (doc.content.includes(query)) {
|
|
207
|
+
doc.score += 50;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const sorted = documents.sort((a, b) => b.score - a.score);
|
|
212
|
+
|
|
213
|
+
expect(sorted[0].id).toBe('1');
|
|
214
|
+
expect(sorted[0].score).toBeGreaterThan(sorted[1].score);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should consider word frequency', () => {
|
|
218
|
+
const documents = [
|
|
219
|
+
{
|
|
220
|
+
id: '1',
|
|
221
|
+
content: 'JavaScript JavaScript JavaScript',
|
|
222
|
+
score: 0
|
|
223
|
+
},
|
|
224
|
+
{ id: '2', content: 'JavaScript is great', score: 0 }
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const query = 'JavaScript';
|
|
228
|
+
|
|
229
|
+
documents.forEach((doc) => {
|
|
230
|
+
const matches = (doc.content.match(/JavaScript/g) || []).length;
|
|
231
|
+
doc.score = matches;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const sorted = documents.sort((a, b) => b.score - a.score);
|
|
235
|
+
|
|
236
|
+
expect(sorted[0].score).toBeGreaterThan(sorted[1].score);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should boost recent documents', () => {
|
|
240
|
+
const documents = [
|
|
241
|
+
{ id: '1', title: 'Old Doc', date: '2023-01-01', score: 50 },
|
|
242
|
+
{ id: '2', title: 'New Doc', date: '2024-03-01', score: 50 }
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
documents.forEach((doc) => {
|
|
246
|
+
const daysSinceUpdate = Math.floor(
|
|
247
|
+
(Date.now() - new Date(doc.date).getTime()) / (1000 * 60 * 60 * 24)
|
|
248
|
+
);
|
|
249
|
+
const recencyBoost = Math.max(0, 100 - daysSinceUpdate);
|
|
250
|
+
doc.score += recencyBoost * 0.1;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const sorted = documents.sort((a, b) => b.score - a.score);
|
|
254
|
+
expect(sorted[0].score).toBeGreaterThanOrEqual(sorted[1].score);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('Search Results Display', () => {
|
|
259
|
+
it('should return paginated results', () => {
|
|
260
|
+
const allResults = Array(50)
|
|
261
|
+
.fill(0)
|
|
262
|
+
.map((_, i) => ({ id: `${i}`, title: `Result ${i}` }));
|
|
263
|
+
|
|
264
|
+
const pageSize = 10;
|
|
265
|
+
const pageNumber = 0;
|
|
266
|
+
|
|
267
|
+
const paginated = allResults.slice(
|
|
268
|
+
pageNumber * pageSize,
|
|
269
|
+
(pageNumber + 1) * pageSize
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
expect(paginated.length).toBe(10);
|
|
273
|
+
expect(paginated[0].id).toBe('0');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should handle result excerpt extraction', () => {
|
|
277
|
+
const result = {
|
|
278
|
+
title: 'Documentation',
|
|
279
|
+
content:
|
|
280
|
+
'This is a long document. It contains important information. The search term appears here.'
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const excerptLength = 150;
|
|
284
|
+
const excerpt =
|
|
285
|
+
result.content.length > excerptLength
|
|
286
|
+
? result.content.substring(0, excerptLength) + '...'
|
|
287
|
+
: result.content;
|
|
288
|
+
|
|
289
|
+
expect(excerpt).toContain('This is a long');
|
|
290
|
+
expect(excerpt.length).toBeLessThanOrEqual(excerptLength + 3);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should highlight search terms in results', () => {
|
|
294
|
+
const result = {
|
|
295
|
+
title: 'Getting Started',
|
|
296
|
+
content: 'Getting started with the Getting Started guide'
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const query = 'Getting';
|
|
300
|
+
const highlighted = result.content.replace(
|
|
301
|
+
new RegExp(`(${query})`, 'gi'),
|
|
302
|
+
'<mark>$1</mark>'
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(highlighted).toContain('<mark>Getting</mark>');
|
|
306
|
+
expect(highlighted.match(/<mark>/g)?.length).toBe(2);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should show result metadata', () => {
|
|
310
|
+
const result = {
|
|
311
|
+
id: '123',
|
|
312
|
+
url: '/docs/guide',
|
|
313
|
+
title: 'Guide',
|
|
314
|
+
breadcrumb: ['Docs', 'Guide'],
|
|
315
|
+
lastUpdated: '2024-03-01'
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
expect(result.url).toBeTruthy();
|
|
319
|
+
expect(result.breadcrumb).toEqual(['Docs', 'Guide']);
|
|
320
|
+
expect(result.lastUpdated).toBeTruthy();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should show result count', () => {
|
|
324
|
+
const results = [
|
|
325
|
+
{ id: '1', title: 'Result 1' },
|
|
326
|
+
{ id: '2', title: 'Result 2' },
|
|
327
|
+
{ id: '3', title: 'Result 3' }
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
const resultCount = results.length;
|
|
331
|
+
expect(resultCount).toBe(3);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should handle "no results" state', () => {
|
|
335
|
+
const results: any[] = [];
|
|
336
|
+
const message =
|
|
337
|
+
results.length === 0
|
|
338
|
+
? 'No results found. Try different keywords.'
|
|
339
|
+
: `Found ${results.length} results`;
|
|
340
|
+
|
|
341
|
+
expect(message).toContain('No results found');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('Search Performance', () => {
|
|
346
|
+
it('should search large index quickly', () => {
|
|
347
|
+
const largeIndex = Array(10000)
|
|
348
|
+
.fill(0)
|
|
349
|
+
.map((_, i) => ({
|
|
350
|
+
id: `${i}`,
|
|
351
|
+
title: `Document ${i}`,
|
|
352
|
+
content: `Content for document ${i}`
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
const query = 'Document 5000';
|
|
356
|
+
|
|
357
|
+
const startTime = Date.now();
|
|
358
|
+
const results = largeIndex.filter((doc) =>
|
|
359
|
+
doc.title.includes(query) || doc.content.includes(query)
|
|
360
|
+
);
|
|
361
|
+
const duration = Date.now() - startTime;
|
|
362
|
+
|
|
363
|
+
expect(duration).toBeLessThan(100); // Should be very fast
|
|
364
|
+
expect(results.length).toBeGreaterThan(0);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should debounce search input', () => {
|
|
368
|
+
let searchCount = 0;
|
|
369
|
+
const executeSearch = () => {
|
|
370
|
+
searchCount++;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
let debounceTimer: NodeJS.Timeout | null = null;
|
|
374
|
+
const debouncedSearch = () => {
|
|
375
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
376
|
+
debounceTimer = setTimeout(() => {
|
|
377
|
+
executeSearch();
|
|
378
|
+
}, 300);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Simulate rapid user input
|
|
382
|
+
for (let i = 0; i < 10; i++) {
|
|
383
|
+
debouncedSearch();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Clear the timeout to check the count
|
|
387
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
388
|
+
|
|
389
|
+
expect(searchCount).toBe(0); // No search executed yet (due to debounce)
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('Special Search Features', () => {
|
|
394
|
+
it('should support phrase search with quotes', () => {
|
|
395
|
+
const documents = [
|
|
396
|
+
{ id: '1', content: 'getting started guide' },
|
|
397
|
+
{ id: '2', content: 'started guide getting' },
|
|
398
|
+
{ id: '3', content: 'guide for getting started' }
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
const query = '"getting started"';
|
|
402
|
+
const results = documents.filter((doc) =>
|
|
403
|
+
doc.content.includes(query.replace(/"/g, ''))
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(results.length).toBeGreaterThan(0);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should support excluding terms with minus', () => {
|
|
410
|
+
const documents = [
|
|
411
|
+
{ id: '1', content: 'JavaScript tutorial' },
|
|
412
|
+
{ id: '2', content: 'Python tutorial' },
|
|
413
|
+
{ id: '3', content: 'JavaScript advanced' }
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
const includeTerms = ['tutorial'];
|
|
417
|
+
const excludeTerms = ['JavaScript'];
|
|
418
|
+
|
|
419
|
+
const results = documents.filter((doc) => {
|
|
420
|
+
const hasInclude = includeTerms.every((term) =>
|
|
421
|
+
doc.content.includes(term)
|
|
422
|
+
);
|
|
423
|
+
const hasExclude = excludeTerms.some((term) =>
|
|
424
|
+
doc.content.includes(term)
|
|
425
|
+
);
|
|
426
|
+
return hasInclude && !hasExclude;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
expect(results.length).toBe(1);
|
|
430
|
+
expect(results[0].id).toBe('2');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should support wildcard search', () => {
|
|
434
|
+
const documents = [
|
|
435
|
+
{ id: '1', title: 'JavaScripting' },
|
|
436
|
+
{ id: '2', title: 'JavaScript' },
|
|
437
|
+
{ id: '3', title: 'Script' }
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
const query = 'Java*';
|
|
441
|
+
const pattern = new RegExp(query.replace(/\*/g, '.*'));
|
|
442
|
+
|
|
443
|
+
const results = documents.filter((doc) => pattern.test(doc.title));
|
|
444
|
+
|
|
445
|
+
expect(results.length).toBe(2);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should support field-specific search', () => {
|
|
449
|
+
const documents = [
|
|
450
|
+
{ id: '1', title: 'Guide', content: 'getting started' },
|
|
451
|
+
{ id: '2', title: 'Getting Started', content: 'introduction' }
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
const fieldSearch = 'title:Getting';
|
|
455
|
+
const [field, term] = fieldSearch.split(':');
|
|
456
|
+
|
|
457
|
+
const results = documents.filter((doc: any) => {
|
|
458
|
+
return doc[field]?.includes(term);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
expect(results.length).toBe(1);
|
|
462
|
+
expect(results[0].id).toBe('2');
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('Search Analytics', () => {
|
|
467
|
+
it('should track popular searches', () => {
|
|
468
|
+
const searchLog = [
|
|
469
|
+
{ query: 'getting started', count: 150 },
|
|
470
|
+
{ query: 'api reference', count: 120 },
|
|
471
|
+
{ query: 'installation', count: 95 }
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const sorted = searchLog.sort((a, b) => b.count - a.count);
|
|
475
|
+
|
|
476
|
+
expect(sorted[0].query).toBe('getting started');
|
|
477
|
+
expect(sorted[0].count).toBe(150);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should track search quality metrics', () => {
|
|
481
|
+
const searches = [
|
|
482
|
+
{ query: 'guide', resultCount: 45, clickCount: 23 },
|
|
483
|
+
{ query: 'help', resultCount: 0, clickCount: 0 }
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
const goodSearches = searches.filter(
|
|
487
|
+
(s) => s.resultCount > 0 && s.clickCount > 0
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
expect(goodSearches.length).toBe(1);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { i18n } from './i18n';
|
|
3
|
+
|
|
4
|
+
// Mock localStorage
|
|
5
|
+
const localStorageMock = (() => {
|
|
6
|
+
let store: Record<string, string> = {};
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
getItem: (key: string) => store[key] || null,
|
|
10
|
+
setItem: (key: string, value: string) => {
|
|
11
|
+
store[key] = value.toString();
|
|
12
|
+
},
|
|
13
|
+
removeItem: (key: string) => {
|
|
14
|
+
delete store[key];
|
|
15
|
+
},
|
|
16
|
+
clear: () => {
|
|
17
|
+
store = {};
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
})();
|
|
21
|
+
|
|
22
|
+
Object.defineProperty(window, 'localStorage', {
|
|
23
|
+
value: localStorageMock,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('i18n store', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
localStorage.clear();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should initialize with default language (English)', () => {
|
|
32
|
+
let currentLanguage = '';
|
|
33
|
+
const unsubscribe = i18n.subscribe((config) => {
|
|
34
|
+
currentLanguage = config.currentLanguage;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(currentLanguage).toBe('en');
|
|
38
|
+
unsubscribe();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should have multiple languages available', () => {
|
|
42
|
+
let languages = [];
|
|
43
|
+
const unsubscribe = i18n.subscribe((config) => {
|
|
44
|
+
languages = config.availableLanguages.map((l) => l.code);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(languages).toContain('en');
|
|
48
|
+
expect(languages).toContain('fr');
|
|
49
|
+
expect(languages).toContain('es');
|
|
50
|
+
expect(languages).toContain('de');
|
|
51
|
+
expect(languages).toContain('ja');
|
|
52
|
+
expect(languages.length).toBe(5);
|
|
53
|
+
unsubscribe();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should mark English as default language', () => {
|
|
57
|
+
let defaultLanguage = '';
|
|
58
|
+
const unsubscribe = i18n.subscribe((config) => {
|
|
59
|
+
defaultLanguage = config.defaultLanguage;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(defaultLanguage).toBe('en');
|
|
63
|
+
unsubscribe();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should allow changing language', () => {
|
|
67
|
+
let currentLanguage = '';
|
|
68
|
+
const unsubscribe = i18n.subscribe((config) => {
|
|
69
|
+
currentLanguage = config.currentLanguage;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
i18n.setLanguage('fr');
|
|
73
|
+
expect(currentLanguage).toBe('fr');
|
|
74
|
+
|
|
75
|
+
i18n.setLanguage('es');
|
|
76
|
+
expect(currentLanguage).toBe('es');
|
|
77
|
+
|
|
78
|
+
unsubscribe();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should persist language preference to localStorage', () => {
|
|
82
|
+
i18n.setLanguage('fr');
|
|
83
|
+
expect(localStorage.getItem('docs-language')).toBe('fr');
|
|
84
|
+
|
|
85
|
+
i18n.setLanguage('ja');
|
|
86
|
+
expect(localStorage.getItem('docs-language')).toBe('ja');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should update document lang attribute on language change', () => {
|
|
90
|
+
i18n.setLanguage('fr');
|
|
91
|
+
expect(document.documentElement.lang).toBe('fr');
|
|
92
|
+
|
|
93
|
+
i18n.setLanguage('es');
|
|
94
|
+
expect(document.documentElement.lang).toBe('es');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should ignore setting invalid language', () => {
|
|
98
|
+
let currentLanguage = '';
|
|
99
|
+
const unsubscribe = i18n.subscribe((config) => {
|
|
100
|
+
currentLanguage = config.currentLanguage;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const initialLanguage = currentLanguage;
|
|
104
|
+
i18n.setLanguage('zh' as any);
|
|
105
|
+
expect(currentLanguage).toBe(initialLanguage);
|
|
106
|
+
|
|
107
|
+
unsubscribe();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should allow adding new languages', () => {
|
|
111
|
+
let languages = [];
|
|
112
|
+
const unsubscribe = i18n.subscribe((config) => {
|
|
113
|
+
languages = config.availableLanguages.map((l) => l.code);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
i18n.addLanguage({
|
|
117
|
+
code: 'pt',
|
|
118
|
+
label: 'Português',
|
|
119
|
+
nativeLabel: 'Português',
|
|
120
|
+
direction: 'ltr',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(languages).toContain('pt');
|
|
124
|
+
unsubscribe();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should allow removing languages', () => {
|
|
128
|
+
let languages = [];
|
|
129
|
+
const unsubscribe = i18n.subscribe((config) => {
|
|
130
|
+
languages = config.availableLanguages.map((l) => l.code);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const initialCount = languages.length;
|
|
134
|
+
i18n.removeLanguage('fr');
|
|
135
|
+
|
|
136
|
+
expect(languages.length).toBe(initialCount - 1);
|
|
137
|
+
expect(languages).not.toContain('fr');
|
|
138
|
+
unsubscribe();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should retrieve language metadata', () => {
|
|
142
|
+
// Re-add French if it was removed by previous test
|
|
143
|
+
i18n.addLanguage({
|
|
144
|
+
code: 'fr',
|
|
145
|
+
label: 'Français',
|
|
146
|
+
nativeLabel: 'Français',
|
|
147
|
+
direction: 'ltr',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const metadata = i18n.getLanguageMetadata('fr');
|
|
151
|
+
expect(metadata).toBeDefined();
|
|
152
|
+
expect(metadata?.code).toBe('fr');
|
|
153
|
+
expect(metadata?.label).toBe('Français');
|
|
154
|
+
expect(metadata?.direction).toBe('ltr');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should have correct language labels', () => {
|
|
158
|
+
// Ensure French is available
|
|
159
|
+
i18n.addLanguage({
|
|
160
|
+
code: 'fr',
|
|
161
|
+
label: 'Français',
|
|
162
|
+
nativeLabel: 'Français',
|
|
163
|
+
direction: 'ltr',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let languages = [];
|
|
167
|
+
const unsubscribe = i18n.subscribe((config) => {
|
|
168
|
+
languages = config.availableLanguages;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const french = languages.find((l) => l.code === 'fr');
|
|
172
|
+
expect(french?.label).toBe('Français');
|
|
173
|
+
expect(french?.nativeLabel).toBe('Français');
|
|
174
|
+
|
|
175
|
+
const spanish = languages.find((l) => l.code === 'es');
|
|
176
|
+
expect(spanish?.label).toBe('Español');
|
|
177
|
+
|
|
178
|
+
unsubscribe();
|
|
179
|
+
});
|
|
180
|
+
});
|