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);
 | |
|     }
 | |
|   });
 | |
| });
 |