ObsiViewer/e2e/search-meilisearch.spec.ts

209 lines
6.8 KiB
TypeScript

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