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