feat: add Meilisearch backend integration with Docker Compose setup and Excalidraw support
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Excalidraw Integration', () => {
|
||||
test('should load and display Excalidraw editor', async ({ page }) => {
|
||||
await page.goto('http://localhost:4200');
|
||||
|
||||
// Wait for app to load
|
||||
await page.waitForSelector('body', { timeout: 10000 });
|
||||
|
||||
// Check if test drawing file exists in file list
|
||||
const testFile = page.locator('text=test-drawing');
|
||||
if (await testFile.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
await testFile.click();
|
||||
|
||||
// Wait for Excalidraw editor to load
|
||||
await page.waitForSelector('excalidraw-editor', { timeout: 10000 });
|
||||
|
||||
// Verify editor is present
|
||||
const editor = page.locator('excalidraw-editor');
|
||||
await expect(editor).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle file API with query params', async ({ page, request }) => {
|
||||
// Test GET endpoint with query param
|
||||
const response = await request.get('http://localhost:4200/api/files', {
|
||||
params: { path: 'test-drawing.excalidraw.md' }
|
||||
});
|
||||
|
||||
if (response.ok()) {
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty('elements');
|
||||
expect(Array.isArray(data.elements)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('should parse Obsidian format correctly', async ({ request }) => {
|
||||
// Test that the backend can parse Obsidian format
|
||||
const response = await request.get('http://localhost:4200/api/files', {
|
||||
params: { path: 'test-drawing.excalidraw.md' }
|
||||
});
|
||||
|
||||
if (response.ok()) {
|
||||
const data = await response.json();
|
||||
|
||||
// Verify structure
|
||||
expect(data).toHaveProperty('elements');
|
||||
expect(data).toHaveProperty('appState');
|
||||
expect(data).toHaveProperty('files');
|
||||
|
||||
// Verify types
|
||||
expect(Array.isArray(data.elements)).toBeTruthy();
|
||||
expect(typeof data.appState).toBe('object');
|
||||
expect(typeof data.files).toBe('object');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
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)`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user