Skip to content

Commit 6076f76

Browse files
authored
Merge pull request #362 from Quorafind/feat/settings-search-functionality
feat(settings): add search functionality with fuzzy matching
2 parents ab71dfb + 7bd60f4 commit 6076f76

File tree

10 files changed

+2153
-12190
lines changed

10 files changed

+2153
-12190
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
// Mock Obsidian's prepareFuzzySearch
2+
const mockPrepareFuzzySearch = jest.fn((query: string) => {
3+
return (target: string) => {
4+
// Simple mock implementation for testing
5+
return target.toLowerCase().includes(query.toLowerCase());
6+
};
7+
});
8+
9+
// Mock translations
10+
const mockT = jest.fn((key: string) => key);
11+
12+
jest.mock("obsidian", () => ({
13+
prepareFuzzySearch: mockPrepareFuzzySearch
14+
}));
15+
16+
jest.mock("../translations/helper", () => ({
17+
t: mockT
18+
}));
19+
20+
import { SettingsIndexer } from "../components/settings/SettingsIndexer";
21+
import { SETTINGS_METADATA } from "../data/settings-metadata";
22+
23+
describe("Settings Search Tests", () => {
24+
let indexer: SettingsIndexer;
25+
26+
beforeEach(() => {
27+
indexer = new SettingsIndexer();
28+
jest.clearAllMocks();
29+
});
30+
31+
describe("Settings Metadata", () => {
32+
test("should have valid metadata structure", () => {
33+
expect(SETTINGS_METADATA).toBeDefined();
34+
expect(Array.isArray(SETTINGS_METADATA)).toBe(true);
35+
expect(SETTINGS_METADATA.length).toBeGreaterThan(0);
36+
37+
// Check first few items have required fields
38+
const firstItem = SETTINGS_METADATA[0];
39+
expect(firstItem).toHaveProperty("id");
40+
expect(firstItem).toHaveProperty("tabId");
41+
expect(firstItem).toHaveProperty("name");
42+
expect(firstItem).toHaveProperty("translationKey");
43+
expect(firstItem).toHaveProperty("keywords");
44+
expect(Array.isArray(firstItem.keywords)).toBe(true);
45+
});
46+
47+
test("should have progress bar settings", () => {
48+
const progressBarMain = SETTINGS_METADATA.find(item => item.id === "progress-bar-main");
49+
expect(progressBarMain).toBeDefined();
50+
expect(progressBarMain?.name).toBe("Progress bar");
51+
expect(progressBarMain?.tabId).toBe("progress-bar");
52+
expect(progressBarMain?.keywords).toContain("progress");
53+
});
54+
55+
test("should have enable task filter setting", () => {
56+
const taskFilter = SETTINGS_METADATA.find(item => item.id === "enable-task-filter");
57+
expect(taskFilter).toBeDefined();
58+
expect(taskFilter?.name).toBe("Enable Task Filter");
59+
expect(taskFilter?.tabId).toBe("task-filter");
60+
});
61+
});
62+
63+
describe("SettingsIndexer", () => {
64+
test("should initialize correctly", () => {
65+
expect(indexer).toBeDefined();
66+
67+
// Should not be initialized yet
68+
const stats = indexer.getStats();
69+
expect(stats.itemCount).toBeGreaterThan(0);
70+
});
71+
72+
test("should build index on first search", () => {
73+
const results = indexer.search("progress");
74+
expect(mockT).toHaveBeenCalled();
75+
});
76+
77+
test("should search by name", () => {
78+
const results = indexer.search("progress");
79+
80+
console.log("Search results for 'progress':", results.length);
81+
results.forEach(result => {
82+
console.log(`- ${result.item.name} (${result.matchType}, score: ${result.score})`);
83+
});
84+
85+
expect(results.length).toBeGreaterThan(0);
86+
87+
// Should find progress bar settings
88+
const progressResult = results.find(result =>
89+
result.item.id === "progress-bar-main" ||
90+
result.item.name.toLowerCase().includes("progress")
91+
);
92+
expect(progressResult).toBeDefined();
93+
});
94+
95+
test("should search by description", () => {
96+
const results = indexer.search("toggle");
97+
98+
console.log("Search results for 'toggle':", results.length);
99+
results.forEach(result => {
100+
console.log(`- ${result.item.name} (${result.matchType}, score: ${result.score})`);
101+
if (result.item.description) {
102+
console.log(` Description: ${result.item.description.substring(0, 100)}...`);
103+
}
104+
});
105+
106+
expect(results.length).toBeGreaterThan(0);
107+
});
108+
109+
test("should search by keywords", () => {
110+
const results = indexer.search("checkbox");
111+
112+
console.log("Search results for 'checkbox':", results.length);
113+
results.forEach(result => {
114+
console.log(`- ${result.item.name} (${result.matchType}, score: ${result.score})`);
115+
console.log(` Keywords: ${result.item.keywords.join(", ")}`);
116+
});
117+
118+
expect(results.length).toBeGreaterThan(0);
119+
});
120+
121+
test("should prioritize name matches over description matches", () => {
122+
const results = indexer.search("filter");
123+
124+
console.log("Search results for 'filter' with scores:");
125+
results.forEach(result => {
126+
console.log(`- ${result.item.name} (${result.matchType}, score: ${result.score})`);
127+
});
128+
129+
expect(results.length).toBeGreaterThan(0);
130+
131+
// Find name matches and description matches
132+
const nameMatches = results.filter(r => r.matchType === "name");
133+
const descriptionMatches = results.filter(r => r.matchType === "description");
134+
135+
if (nameMatches.length > 0 && descriptionMatches.length > 0) {
136+
// Name matches should have higher scores
137+
const highestNameScore = Math.max(...nameMatches.map(r => r.score));
138+
const highestDescScore = Math.max(...descriptionMatches.map(r => r.score));
139+
expect(highestNameScore).toBeGreaterThan(highestDescScore);
140+
}
141+
});
142+
143+
test("should return empty results for empty query", () => {
144+
const results = indexer.search("");
145+
expect(results.length).toBe(0);
146+
});
147+
148+
test("should return empty results for very short query", () => {
149+
const results = indexer.search("a");
150+
expect(results.length).toBe(0);
151+
});
152+
153+
test("should return results for 2+ character query", () => {
154+
const results = indexer.search("pr");
155+
expect(results.length).toBeGreaterThan(0);
156+
});
157+
158+
test("should limit results", () => {
159+
const results = indexer.search("task");
160+
console.log(`Found ${results.length} results for 'task'`);
161+
expect(results.length).toBeLessThanOrEqual(10); // Default limit
162+
});
163+
164+
test("should handle case insensitive search", () => {
165+
const lowerResults = indexer.search("progress");
166+
const upperResults = indexer.search("PROGRESS");
167+
const mixedResults = indexer.search("Progress");
168+
169+
expect(lowerResults.length).toBe(upperResults.length);
170+
expect(lowerResults.length).toBe(mixedResults.length);
171+
});
172+
});
173+
174+
describe("Translation Integration", () => {
175+
test("should call translation function for setting names", () => {
176+
indexer.search("progress");
177+
178+
// Should have called t() for translating setting names
179+
expect(mockT).toHaveBeenCalled();
180+
181+
// Check if it was called with expected translation keys
182+
const calls = mockT.mock.calls.map((call: any) => call[0]);
183+
console.log("Translation calls:", calls.slice(0, 10)); // Show first 10 calls
184+
185+
expect(calls).toContain("Progress bar");
186+
});
187+
});
188+
189+
describe("Edge Cases", () => {
190+
test("should handle special characters in search", () => {
191+
const results = indexer.search("task-filter");
192+
expect(results.length).toBeGreaterThanOrEqual(0);
193+
});
194+
195+
test("should handle unicode characters", () => {
196+
const results = indexer.search("设置");
197+
expect(results.length).toBeGreaterThanOrEqual(0);
198+
});
199+
200+
test("should handle numbers in search", () => {
201+
const results = indexer.search("100");
202+
expect(results.length).toBeGreaterThanOrEqual(0);
203+
});
204+
});
205+
206+
describe("Performance", () => {
207+
test("should build index quickly", () => {
208+
const start = performance.now();
209+
indexer.initialize();
210+
const end = performance.now();
211+
212+
const buildTime = end - start;
213+
console.log(`Index build time: ${buildTime.toFixed(2)}ms`);
214+
expect(buildTime).toBeLessThan(50); // Should be under 50ms
215+
});
216+
217+
test("should search quickly", () => {
218+
indexer.initialize(); // Pre-initialize
219+
220+
const start = performance.now();
221+
const results = indexer.search("progress");
222+
const end = performance.now();
223+
224+
const searchTime = end - start;
225+
console.log(`Search time: ${searchTime.toFixed(2)}ms for ${results.length} results`);
226+
expect(searchTime).toBeLessThan(10); // Should be under 10ms
227+
});
228+
});
229+
230+
describe("Specific Setting Tests", () => {
231+
test("should find 'Enable Task Filter' setting", () => {
232+
const results = indexer.search("enable task filter");
233+
234+
console.log("Searching for 'enable task filter':");
235+
results.forEach(result => {
236+
console.log(`- ${result.item.name} (ID: ${result.item.id})`);
237+
});
238+
239+
const exactMatch = results.find(r => r.item.id === "enable-task-filter");
240+
expect(exactMatch).toBeDefined();
241+
});
242+
243+
test("should find progress bar settings", () => {
244+
const results = indexer.search("progress bar");
245+
246+
console.log("Searching for 'progress bar':");
247+
results.forEach(result => {
248+
console.log(`- ${result.item.name} (ID: ${result.item.id})`);
249+
});
250+
251+
const progressBarMain = results.find(r => r.item.id === "progress-bar-main");
252+
expect(progressBarMain).toBeDefined();
253+
expect(progressBarMain?.matchType).toBe("name");
254+
});
255+
256+
test("should find settings by partial name", () => {
257+
const results = indexer.search("workflow");
258+
259+
console.log("Searching for 'workflow':");
260+
results.forEach(result => {
261+
console.log(`- ${result.item.name} (ID: ${result.item.id})`);
262+
});
263+
264+
expect(results.length).toBeGreaterThan(0);
265+
266+
// Should find workflow-related settings
267+
const workflowSettings = results.filter(r =>
268+
r.item.name.toLowerCase().includes("workflow") ||
269+
r.item.tabId === "workflow"
270+
);
271+
expect(workflowSettings.length).toBeGreaterThan(0);
272+
});
273+
});
274+
});

src/components/settings/ProgressSettingsTab.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ export function renderProgressSettingsTab(
1515
"You can customize the progress bar behind the parent task(usually at the end of the task). You can also customize the progress bar for the task below the heading."
1616
)
1717
)
18-
.setHeading();
18+
.setHeading()
19+
.settingEl.setAttribute("data-setting-id", "progress-bar-main");
1920

20-
new Setting(containerEl)
21+
const progressDisplaySetting = new Setting(containerEl)
2122
.setName(t("Progress display mode"))
2223
.setDesc(t("Choose how to display task progress"))
2324
.addDropdown((dropdown) =>
@@ -33,6 +34,7 @@ export function renderProgressSettingsTab(
3334
settingTab.display();
3435
})
3536
);
37+
progressDisplaySetting.settingEl.setAttribute("data-setting-id", "progress-display-mode");
3638

3739
// Only show these options if some form of progress bar is enabled
3840
if (settingTab.plugin.settings.progressBarDisplayMode !== "none") {

0 commit comments

Comments
 (0)