426 lines
14 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|