import { test, expect } from '@playwright/test'; test.describe('Meilisearch Search Integration', () => { test.beforeEach(async ({ page }) => { // Navigate to the app await page.goto('http://localhost:4000'); // Wait for the app to load await page.waitForLoadState('networkidle'); }); test('should perform search via Meilisearch backend', async ({ page }) => { // Open search panel (assuming there's a search button or input) const searchInput = page.locator('input[type="text"]').first(); await searchInput.click(); // Type search query await searchInput.fill('test'); // Wait for debounce (300ms) + network request await page.waitForTimeout(500); // Check that search results are displayed const results = page.locator('[class*="search-result"]').first(); await expect(results).toBeVisible({ timeout: 5000 }); }); test('should not freeze UI during search typing', async ({ page }) => { const searchInput = page.locator('input[type="text"]').first(); await searchInput.click(); // Type quickly to test debounce await searchInput.type('angular', { delay: 50 }); // UI should remain responsive const isEnabled = await searchInput.isEnabled(); expect(isEnabled).toBe(true); // Wait for debounced search await page.waitForTimeout(400); }); test('should use Meilisearch API endpoint', async ({ page }) => { // Listen for API calls const apiCalls: string[] = []; page.on('request', request => { if (request.url().includes('/api/search')) { apiCalls.push(request.url()); } }); // Perform search const searchInput = page.locator('input[type="text"]').first(); await searchInput.click(); await searchInput.fill('note'); await searchInput.press('Enter'); // Wait for API call await page.waitForTimeout(1000); // Verify API was called expect(apiCalls.length).toBeGreaterThan(0); expect(apiCalls[0]).toContain('/api/search?q=note'); }); test('should display search results quickly', async ({ page }) => { const searchInput = page.locator('input[type="text"]').first(); await searchInput.click(); const startTime = Date.now(); // Perform search await searchInput.fill('test'); await searchInput.press('Enter'); // Wait for results await page.waitForSelector('[class*="search-result"]', { timeout: 2000 }); const endTime = Date.now(); const duration = endTime - startTime; // Search should complete in less than 2 seconds expect(duration).toBeLessThan(2000); console.log(`Search completed in ${duration}ms`); }); test('should handle empty search gracefully', async ({ page }) => { const searchInput = page.locator('input[type="text"]').first(); await searchInput.click(); await searchInput.fill('xyzabc123notfound'); await searchInput.press('Enter'); // Wait for response await page.waitForTimeout(1000); // Should show "no results" message const noResults = page.locator('text=/No results|Aucun résultat/i').first(); await expect(noResults).toBeVisible({ timeout: 3000 }); }); test('should support Obsidian search operators', async ({ page }) => { const searchInput = page.locator('input[type="text"]').first(); await searchInput.click(); // Test path: operator await searchInput.fill('path:docs'); await searchInput.press('Enter'); await page.waitForTimeout(500); // Clear and test tag: operator await searchInput.clear(); await searchInput.fill('tag:important'); await searchInput.press('Enter'); await page.waitForTimeout(500); // Should not crash const isEnabled = await searchInput.isEnabled(); expect(isEnabled).toBe(true); }); test('should debounce live search during typing', async ({ page }) => { // Listen for API calls let apiCallCount = 0; page.on('request', request => { if (request.url().includes('/api/search')) { apiCallCount++; } }); const searchInput = page.locator('input[type="text"]').first(); await searchInput.click(); // Type slowly (each char triggers potential search) await searchInput.type('angular', { delay: 100 }); // Wait for debounce to settle await page.waitForTimeout(500); // Should have made fewer API calls than characters typed (due to debounce) // "angular" = 7 chars, but debounce should reduce calls expect(apiCallCount).toBeLessThan(7); console.log(`API calls made: ${apiCallCount} (debounced from 7 chars)`); }); }); test.describe('Meilisearch Backend API', () => { test('should return search results from /api/search', async ({ request }) => { const response = await request.get('http://localhost:4000/api/search?q=test&limit=5'); expect(response.ok()).toBeTruthy(); const data = await response.json(); expect(data).toHaveProperty('hits'); expect(data).toHaveProperty('processingTimeMs'); expect(data).toHaveProperty('query'); expect(data.query).toBe('test'); console.log(`Meilisearch processed search in ${data.processingTimeMs}ms`); }); test('should support highlighting', async ({ request }) => { const response = await request.get('http://localhost:4000/api/search?q=note&highlight=true&limit=3'); expect(response.ok()).toBeTruthy(); const data = await response.json(); if (data.hits.length > 0) { // Check if highlighting is present const firstHit = data.hits[0]; expect(firstHit).toHaveProperty('_formatted'); } }); test('should handle pagination', async ({ request }) => { const response1 = await request.get('http://localhost:4000/api/search?q=note&limit=2&offset=0'); const response2 = await request.get('http://localhost:4000/api/search?q=note&limit=2&offset=2'); expect(response1.ok()).toBeTruthy(); expect(response2.ok()).toBeTruthy(); const data1 = await response1.json(); const data2 = await response2.json(); // Results should be different (different pages) if (data1.hits.length > 0 && data2.hits.length > 0) { expect(data1.hits[0].id).not.toBe(data2.hits[0].id); } }); test('should be fast (< 100ms)', async ({ request }) => { const startTime = Date.now(); const response = await request.get('http://localhost:4000/api/search?q=angular&limit=10'); const endTime = Date.now(); const duration = endTime - startTime; expect(response.ok()).toBeTruthy(); const data = await response.json(); // Total time (network + processing) should be < 100ms for localhost expect(duration).toBeLessThan(100); console.log(`Total search time: ${duration}ms (Meili: ${data.processingTimeMs}ms)`); }); });