diff --git a/demo-vault-v2/area/refactoring.md b/demo-vault-v2/area/refactoring.md new file mode 100644 index 00000000..e6a8f65f --- /dev/null +++ b/demo-vault-v2/area/refactoring.md @@ -0,0 +1,7 @@ +--- +Is A: Area +Status: Active +--- +# Refactoring + +Area note covering refactoring practices, principles, and techniques for improving code quality without changing external behavior. diff --git a/demo-vault-v2/note/refactoring-ideas.md b/demo-vault-v2/note/refactoring-ideas.md new file mode 100644 index 00000000..10e8492b --- /dev/null +++ b/demo-vault-v2/note/refactoring-ideas.md @@ -0,0 +1,6 @@ +--- +Is A: Note +--- +# Refactoring Ideas + +Collection of ideas for refactoring the codebase to improve maintainability and performance. diff --git a/demo-vault-v2/note/refactoring-key-ideas.md b/demo-vault-v2/note/refactoring-key-ideas.md new file mode 100644 index 00000000..48925948 --- /dev/null +++ b/demo-vault-v2/note/refactoring-key-ideas.md @@ -0,0 +1,6 @@ +--- +Is A: Note +--- +# Refactoring Key Ideas + +Key takeaways from Martin Fowler's Refactoring book and other refactoring resources. diff --git a/demo-vault-v2/note/refactoring-patterns.md b/demo-vault-v2/note/refactoring-patterns.md new file mode 100644 index 00000000..5841c9ab --- /dev/null +++ b/demo-vault-v2/note/refactoring-patterns.md @@ -0,0 +1,6 @@ +--- +Is A: Note +--- +# Refactoring Patterns + +Common refactoring patterns including Extract Method, Rename Variable, and Replace Conditional with Polymorphism. diff --git a/src/mock-tauri/mock-entries.ts b/src/mock-tauri/mock-entries.ts index 9779b016..a833d901 100644 --- a/src/mock-tauri/mock-entries.ts +++ b/src/mock-tauri/mock-entries.ts @@ -1099,6 +1099,119 @@ export const MOCK_ENTRIES: VaultEntry[] = [ 'Type': ['[[type/experiment]]'], }, }, + // --- Refactoring entries for exact-match search testing --- + { + path: '/Users/luca/Laputa/area/refactoring.md', + filename: 'refactoring.md', + title: 'Refactoring', + isA: 'Area', + aliases: [], + belongsTo: [], + relatedTo: [], + status: 'Active', + owner: null, + cadence: null, + archived: false, + trashed: false, + trashedAt: null, + modifiedAt: now - 86400 * 30, + createdAt: now - 86400 * 365, + fileSize: 1200, + snippet: 'Area note covering refactoring practices and principles.', + wordCount: 180, + relationships: {}, + icon: null, + color: null, + order: null, + sidebarLabel: null, + template: null, sort: null, view: null, visible: null, + outgoingLinks: [], + properties: {}, + }, + { + path: '/Users/luca/Laputa/note/refactoring-ideas.md', + filename: 'refactoring-ideas.md', + title: 'Refactoring Ideas', + isA: 'Note', + aliases: [], + belongsTo: [], + relatedTo: [], + status: null, + owner: null, + cadence: null, + archived: false, + trashed: false, + trashedAt: null, + modifiedAt: now - 86400 * 5, + createdAt: now - 86400 * 60, + fileSize: 800, + snippet: 'Ideas for refactoring the codebase.', + wordCount: 120, + relationships: {}, + icon: null, + color: null, + order: null, + sidebarLabel: null, + template: null, sort: null, view: null, visible: null, + outgoingLinks: [], + properties: {}, + }, + { + path: '/Users/luca/Laputa/note/refactoring-key-ideas.md', + filename: 'refactoring-key-ideas.md', + title: 'Refactoring Key Ideas', + isA: 'Note', + aliases: [], + belongsTo: [], + relatedTo: [], + status: null, + owner: null, + cadence: null, + archived: false, + trashed: false, + trashedAt: null, + modifiedAt: now - 86400 * 10, + createdAt: now - 86400 * 90, + fileSize: 600, + snippet: 'Key ideas from the refactoring book.', + wordCount: 95, + relationships: {}, + icon: null, + color: null, + order: null, + sidebarLabel: null, + template: null, sort: null, view: null, visible: null, + outgoingLinks: [], + properties: {}, + }, + { + path: '/Users/luca/Laputa/note/refactoring-patterns.md', + filename: 'refactoring-patterns.md', + title: 'Refactoring Patterns', + isA: 'Note', + aliases: [], + belongsTo: [], + relatedTo: [], + status: null, + owner: null, + cadence: null, + archived: false, + trashed: false, + trashedAt: null, + modifiedAt: now - 86400 * 15, + createdAt: now - 86400 * 120, + fileSize: 950, + snippet: 'Common refactoring patterns and when to apply them.', + wordCount: 150, + relationships: {}, + icon: null, + color: null, + order: null, + sidebarLabel: null, + template: null, sort: null, view: null, visible: null, + outgoingLinks: [], + properties: {}, + }, ] // --- Bulk entry generator for large vault testing --- diff --git a/tests/smoke/exact-match-search.spec.ts b/tests/smoke/exact-match-search.spec.ts new file mode 100644 index 00000000..9739324c --- /dev/null +++ b/tests/smoke/exact-match-search.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '@playwright/test' +import { sendShortcut } from './helpers' + +const QUICK_OPEN_INPUT = 'input[placeholder="Search notes..."]' + +async function openQuickOpen(page: import('@playwright/test').Page) { + await page.locator('body').click() + await sendShortcut(page, 'p', ['Control']) + await expect(page.locator(QUICK_OPEN_INPUT)).toBeVisible() +} + +/** + * Get the title text of the first (selected) search result in the Quick Open palette. + * The selected item has `bg-accent` class, and the title is in a nested `.truncate` span. + */ +async function getFirstResultTitle(page: import('@playwright/test').Page): Promise { + // The selected result row contains the title in a span.truncate + const titleSpan = page.locator('[class*="bg-accent"] span.truncate') + await titleSpan.first().waitFor({ timeout: 3000 }) + return (await titleSpan.first().textContent()) ?? '' +} + +test.describe('Exact match search ranking', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + }) + + test('exact title match appears as first result', async ({ page }) => { + await openQuickOpen(page) + // "Writing" is a Topic note in the demo vault; other notes have "Writing" as a prefix + await page.locator(QUICK_OPEN_INPUT).fill('Writing') + await page.waitForTimeout(300) + + const firstTitle = await getFirstResultTitle(page) + expect(firstTitle).toBe('Writing') + }) + + test('case-insensitive exact match appears first', async ({ page }) => { + await openQuickOpen(page) + await page.locator(QUICK_OPEN_INPUT).fill('writing') + await page.waitForTimeout(300) + + const firstTitle = await getFirstResultTitle(page) + expect(firstTitle).toBe('Writing') + }) + + test('partial matches still appear below exact match', async ({ page }) => { + await openQuickOpen(page) + await page.locator(QUICK_OPEN_INPUT).fill('Writing') + await page.waitForTimeout(300) + + // Should have multiple results (exact + prefix/fuzzy matches) + const resultRows = page.locator('[class*="cursor-pointer"][class*="items-center"]') + const count = await resultRows.count() + expect(count).toBeGreaterThan(1) + + // First result is the exact match + const firstTitle = await getFirstResultTitle(page) + expect(firstTitle).toBe('Writing') + }) + + test('arrow keys navigate search results past the exact match', async ({ page }) => { + await openQuickOpen(page) + await page.locator(QUICK_OPEN_INPUT).fill('Writing') + await page.waitForTimeout(300) + + // First result should be selected (exact match) + const firstTitle = await getFirstResultTitle(page) + expect(firstTitle).toBe('Writing') + + // ArrowDown moves to next result (a prefix or fuzzy match) + await page.keyboard.press('ArrowDown') + await page.waitForTimeout(100) + const secondTitle = await getFirstResultTitle(page) + expect(secondTitle).not.toBe('Writing') + }) +})