ObsiViewer/e2e/search.spec.ts

426 lines
14 KiB
TypeScript

import { test, expect, Page } from '@playwright/test';
const SEARCH_QUEUE_KEY = 'obsiviewer.searchdiag.queue';
async function clearDiagnostics(page: Page): Promise<void> {
await page.evaluate(key => {
localStorage.removeItem(key);
}, SEARCH_QUEUE_KEY);
}
async function readDiagnostics(page: Page): Promise<any[]> {
const raw = await page.evaluate(key => localStorage.getItem(key), SEARCH_QUEUE_KEY);
if (!raw) {
return [];
}
try {
return JSON.parse(raw);
} catch {
return [];
}
}
function findStage(events: any[], stage: string): any | undefined {
return events.find(event => event?.stage === stage);
}
test.describe('Search Functionality', () => {
test.beforeEach(async ({ page }) => {
// Navigate to the app
await page.goto('/');
// Wait for the app to load
await page.waitForLoadState('networkidle');
});
test('should display search panel', async ({ page }) => {
// Open search panel (adjust selector based on your UI)
const searchPanel = page.locator('app-search-panel');
await expect(searchPanel).toBeVisible();
});
test('should perform basic content search', async ({ page }) => {
// Find search input
const searchInput = page.locator('input[type="text"]').first();
// Type search query
await searchInput.fill('content:test');
await searchInput.press('Enter');
// Wait for results
await page.waitForSelector('.search-results', { timeout: 5000 });
// Verify results are displayed
const resultsCount = page.locator('text=/\\d+ results?/');
await expect(resultsCount).toBeVisible();
});
test('should filter by file name', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('file:readme.md');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Results should only contain readme.md files
const fileNames = page.locator('.file-name');
const count = await fileNames.count();
if (count > 0) {
for (let i = 0; i < count; i++) {
const text = await fileNames.nth(i).textContent();
expect(text?.toLowerCase()).toContain('readme');
}
}
});
test('should filter by path', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('path:"Daily notes"');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Results should only contain files from Daily notes path
const filePaths = page.locator('.file-path');
const count = await filePaths.count();
if (count > 0) {
const text = await filePaths.first().textContent();
expect(text?.toLowerCase()).toContain('daily');
}
});
test('should filter by tag', async ({ page }) => {
await clearDiagnostics(page);
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('tag:#work');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Should show results with #work tag
const results = page.locator('.search-result');
await expect(results.first()).toBeVisible({ timeout: 5000 });
const events = await readDiagnostics(page);
const summary = findStage(events, 'SEARCH_DIAG_SUMMARY');
expect(summary).toBeTruthy();
expect(summary.data?.counts?.displayed).toBeGreaterThan(0);
expect(summary.data?.userVisible?.emptyStateShown).toBe(false);
});
test('should record diagnostics when tag search misses index', async ({ page }) => {
await clearDiagnostics(page);
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('tag:#home');
await searchInput.press('Enter');
await page.waitForTimeout(500);
const events = await readDiagnostics(page);
const summary = findStage(events, 'SEARCH_DIAG_SUMMARY');
expect(summary).toBeTruthy();
expect(summary.data?.counts?.displayed ?? 0).toBe(0);
expect(summary.data?.userVisible?.emptyStateShown).toBe(true);
const resultMap = findStage(events, 'SEARCH_DIAG_RESULT_MAP');
expect(resultMap).toBeTruthy();
expect(resultMap.data?.reasonsEmpty).toEqual(expect.arrayContaining(['tagNotInIndex']));
});
test('should toggle case sensitivity', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
const caseButton = page.locator('button:has-text("Aa")');
// Search with case insensitive (default)
await searchInput.fill('TEST');
await searchInput.press('Enter');
await page.waitForTimeout(500);
const resultsInsensitive = page.locator('text=/\\d+ results?/');
const insensitiveText = await resultsInsensitive.textContent();
// Clear and toggle case sensitivity
await searchInput.clear();
await caseButton.click();
// Search with case sensitive
await searchInput.fill('TEST');
await searchInput.press('Enter');
await page.waitForTimeout(500);
const resultsSensitive = page.locator('text=/\\d+ results?/');
const sensitiveText = await resultsSensitive.textContent();
// Results should be different (assuming there are lowercase 'test' matches)
// This test assumes the vault has both 'test' and 'TEST'
});
test('should toggle collapse results', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
// Perform search
await searchInput.fill('content:test');
await searchInput.press('Enter');
// Wait for results
await page.waitForSelector('text=/\\d+ results?/', { timeout: 5000 });
// Find collapse toggle
const collapseToggle = page.locator('text=Collapse results').locator('..').locator('input[type="checkbox"]');
if (await collapseToggle.isVisible()) {
// Toggle collapse
await collapseToggle.click();
await page.waitForTimeout(300);
// Verify results are collapsed
const expandedGroups = page.locator('.result-group.expanded');
const count = await expandedGroups.count();
expect(count).toBe(0);
}
});
test('should toggle show more context', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
// Perform search
await searchInput.fill('content:test');
await searchInput.press('Enter');
// Wait for results
await page.waitForSelector('text=/\\d+ results?/', { timeout: 5000 });
// Find show more context toggle
const contextToggle = page.locator('text=Show more context').locator('..').locator('input[type="checkbox"]');
if (await contextToggle.isVisible()) {
// Get initial context length
const matchContext = page.locator('.match-context').first();
const initialText = await matchContext.textContent();
const initialLength = initialText?.length || 0;
// Toggle show more context
await contextToggle.click();
await page.waitForTimeout(1000); // Wait for re-search
// Get new context length
const newText = await matchContext.textContent();
const newLength = newText?.length || 0;
// Context should be longer (more lines)
expect(newLength).toBeGreaterThanOrEqual(initialLength);
}
});
test('should highlight matches in results', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('test');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Check for highlighted text
const highlights = page.locator('mark');
const count = await highlights.count();
expect(count).toBeGreaterThan(0);
// Verify highlight contains search term
if (count > 0) {
const highlightText = await highlights.first().textContent();
expect(highlightText?.toLowerCase()).toContain('test');
}
});
test('should handle complex queries with AND operator', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('test AND example');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Results should contain both terms
const results = page.locator('.search-result');
if (await results.count() > 0) {
const firstResult = await results.first().textContent();
expect(firstResult?.toLowerCase()).toContain('test');
expect(firstResult?.toLowerCase()).toContain('example');
}
});
test('should handle complex queries with OR operator', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('test OR example');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Results should contain at least one term
const results = page.locator('.search-result');
await expect(results.first()).toBeVisible({ timeout: 5000 });
});
test('should handle negation operator', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('test -deprecated');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Results should not contain 'deprecated'
const results = page.locator('.search-result');
const count = await results.count();
if (count > 0) {
for (let i = 0; i < Math.min(count, 5); i++) {
const text = await results.nth(i).textContent();
expect(text?.toLowerCase()).not.toContain('deprecated');
}
}
});
test('should handle regex search', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
const regexButton = page.locator('button:has-text(".*")');
// Enable regex mode
await regexButton.click();
// Search with regex pattern
await searchInput.fill('test\\d+');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Should find matches like test1, test2, etc.
const results = page.locator('.search-result');
if (await results.count() > 0) {
await expect(results.first()).toBeVisible();
}
});
test('should handle property search', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('[status]:draft');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Should find notes with status: draft
const results = page.locator('.search-result');
if (await results.count() > 0) {
await expect(results.first()).toBeVisible();
}
});
test('should handle task search', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('task-todo:review');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Should find incomplete tasks containing 'review'
const results = page.locator('.search-result');
if (await results.count() > 0) {
await expect(results.first()).toBeVisible();
}
});
test('should expand and collapse result groups', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('test');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Find first result group
const firstGroup = page.locator('.result-group').first();
const expandButton = firstGroup.locator('.expand-button, .collapse-button, svg').first();
if (await expandButton.isVisible()) {
// Click to collapse
await expandButton.click();
await page.waitForTimeout(300);
// Matches should be hidden
const matches = firstGroup.locator('.match-item');
await expect(matches.first()).not.toBeVisible();
// Click to expand
await expandButton.click();
await page.waitForTimeout(300);
// Matches should be visible
await expect(matches.first()).toBeVisible();
}
});
test('should sort results by different criteria', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
await searchInput.fill('test');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Find sort dropdown
const sortSelect = page.locator('select').filter({ hasText: /Relevance|Name|Modified/ });
if (await sortSelect.isVisible()) {
// Sort by name
await sortSelect.selectOption('name');
await page.waitForTimeout(300);
// Verify sorting (check first two results are alphabetically ordered)
const fileNames = page.locator('.file-name');
if (await fileNames.count() >= 2) {
const first = await fileNames.nth(0).textContent();
const second = await fileNames.nth(1).textContent();
expect(first?.localeCompare(second || '') || 0).toBeLessThanOrEqual(0);
}
}
});
test('should persist search preferences', async ({ page }) => {
const searchInput = page.locator('input[type="text"]').first();
// Perform search and toggle collapse
await searchInput.fill('test');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
const collapseToggle = page.locator('text=Collapse results').locator('..').locator('input[type="checkbox"]');
if (await collapseToggle.isVisible()) {
await collapseToggle.click();
await page.waitForTimeout(300);
// Reload page
await page.reload();
await page.waitForLoadState('networkidle');
// Perform search again
await searchInput.fill('test');
await searchInput.press('Enter');
await page.waitForTimeout(1000);
// Collapse preference should be persisted
const isChecked = await collapseToggle.isChecked();
expect(isChecked).toBe(true);
}
});
});