test: add Playwright smoke test for exact-match search ranking

Verifies that searching "Writing" in Quick Open shows the exact title
match first, followed by prefix matches. Also adds Refactoring test
entries to mock data and demo vault.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-08 22:15:08 +01:00
parent 60f3139b3e
commit 5c85bc41f6
6 changed files with 216 additions and 0 deletions

View File

@@ -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.

View File

@@ -0,0 +1,6 @@
---
Is A: Note
---
# Refactoring Ideas
Collection of ideas for refactoring the codebase to improve maintainability and performance.

View File

@@ -0,0 +1,6 @@
---
Is A: Note
---
# Refactoring Key Ideas
Key takeaways from Martin Fowler's Refactoring book and other refactoring resources.

View File

@@ -0,0 +1,6 @@
---
Is A: Note
---
# Refactoring Patterns
Common refactoring patterns including Extract Method, Rename Variable, and Replace Conditional with Polymorphism.

View File

@@ -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 ---

View File

@@ -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<string> {
// 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')
})
})