// Playwright E2E tests for Advanced Search UI const { test, expect } = require('@playwright/test'); async function loginAndSetTokens(page) { // Read pre-generated access token const fs = require('fs'); const path = require('path'); const tokenPath = path.resolve(__dirname, '..', '.e2e-token'); const access = fs.readFileSync(tokenPath, 'utf-8').trim(); const refresh = ''; await page.addInitScript((a, r) => { try { window.localStorage.setItem('auth_token', a); } catch (_) {} try { if (r) window.localStorage.setItem('refresh_token', r); } catch (_) {} }, access, refresh); return access; } async function apiCreateCustomer(page, payload, token) { // Use import endpoint to avoid multiple writes and simplify schema const req = await page.request.post('/api/import/customers', { data: { customers: [payload] }, headers: token ? { Authorization: `Bearer ${token}` } : {}, }); expect(req.ok()).toBeTruthy(); // Return id directly return payload.id; } async function apiCreateFile(page, payload, token) { const req = await page.request.post('/api/import/files', { data: { files: [payload] }, headers: token ? { Authorization: `Bearer ${token}` } : {}, }); expect(req.ok()).toBeTruthy(); return payload.file_no; } test.describe('Advanced Search UI', () => { test.beforeEach(async ({ page }) => { // no-op here; call per test to capture token }); test('returns highlighted results and enforces XSS safety', async ({ page }) => { const token = `E2E-${Date.now()}`; const accessToken = await loginAndSetTokens(page); const malicious = `${token} `; await apiCreateCustomer(page, { id: `E2E-CUST-${Date.now()}`, first: 'Alice', last: malicious, email: `alice.${Date.now()}@example.com`, city: 'Austin', abrev: 'TX', }, accessToken); await page.goto('/search'); await page.fill('#searchQuery', token); await page.click('#advancedSearchForm button[type="submit"]'); await page.waitForResponse(res => res.url().includes('/api/search/advanced') && res.request().method() === 'POST'); const results = page.locator('#searchResults .search-result-item'); await expect(results.first()).toBeVisible({ timeout: 10000 }); const matchHtml = page.locator('#searchResults .search-result-item .text-sm.text-info-600'); if (await matchHtml.count()) { const html = await matchHtml.first().innerHTML(); expect(html).toContain(''); expect(html).not.toContain('onerror'); expect(html).not.toContain(' { const token = `E2E-PAGE-${Date.now()}`; const accessToken = await loginAndSetTokens(page); const today = new Date().toISOString().slice(0, 10); const ownerId = await apiCreateCustomer(page, { id: `E2E-P-OWNER-${Date.now()}`, first: 'Bob', last: 'Pagination', email: `bob.${Date.now()}@example.com`, city: 'Austin', abrev: 'TX', }, accessToken); for (let i = 0; i < 60; i++) { await apiCreateFile(page, { file_no: `E2E-F-${Date.now()}-${i}`, id: ownerId, regarding: `About ${token} #${i}`, empl_num: 'E01', file_type: 'CIVIL', opened: today, status: 'ACTIVE', rate_per_hour: 150, memo: 'seeded', }, accessToken); } await page.goto('/search'); await page.fill('#searchQuery', token); await page.click('#advancedSearchForm button[type="submit"]'); await page.waitForResponse(res => res.url().includes('/api/search/advanced') && res.request().method() === 'POST'); const pager = page.locator('#searchPagination'); await expect(pager).toBeVisible({ timeout: 10000 }); const firstPageActive = page.locator('#searchPagination button.bg-primary-600'); await expect(firstPageActive).toContainText('1'); const next = page.locator('#searchPagination button', { hasText: 'Next' }); await Promise.all([ page.waitForResponse((res) => res.url().includes('/api/search/advanced') && res.request().method() === 'POST'), next.click(), ]); const active = page.locator('#searchPagination button.bg-primary-600'); await expect(active).not.toContainText('1'); }); test('suggestions dropdown renders safely and clicking populates input and triggers search', async ({ page }) => { const token = `E2E-SUG-${Date.now()}`; await loginAndSetTokens(page); const suggestionOne = `${token} first`; const suggestionTwo = `${token} second`; // Stub the suggestions endpoint for our token await page.route('**/api/search/suggestions*', async (route) => { try { const url = new URL(route.request().url()); const q = url.searchParams.get('q') || ''; if (q.includes(token)) { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ suggestions: [ { text: suggestionOne, category: 'customer', description: 'Name match' }, { text: suggestionTwo, category: 'file', description: 'File regarding' }, ], }), }); } } catch (_) {} return route.fallback(); }); // Stub the advanced search to assert it gets triggered with clicked suggestion let receivedQuery = null; await page.route('**/api/search/advanced', async (route) => { try { const body = route.request().postDataJSON(); receivedQuery = body?.query || null; } catch (_) {} return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ total_results: 0, stats: { search_execution_time: 0.001 }, facets: { customer: {}, file: {}, ledger: {}, qdro: {}, document: {}, phone: {} }, results: [], page_info: { current_page: 1, total_pages: 0, has_previous: false, has_next: false }, }), }); }); await page.goto('/search'); // Type to trigger suggestions (debounced) await page.fill('#searchQuery', token); const dropdown = page.locator('#searchSuggestions'); const items = dropdown.locator('a'); await expect(items).toHaveCount(2, { timeout: 5000 }); await expect(dropdown).toBeVisible(); // Basic safety check — ensure no script tags ended up in suggestions markup const dropdownHtml = await dropdown.innerHTML(); expect(dropdownHtml).not.toContain(' res.url().includes('/api/search/advanced') && res.request().method() === 'POST'), items.first().click(), ]); await expect(page.locator('#searchQuery')).toHaveValue(new RegExp(`^${suggestionOne}`)); expect(receivedQuery || '').toContain(suggestionOne); }); test('Escape hides suggestions dropdown without triggering a search', async ({ page }) => { const token = `E2E-ESC-${Date.now()}`; await loginAndSetTokens(page); // Track whether advanced search is called let calledAdvanced = false; await page.route('**/api/search/advanced', async (route) => { calledAdvanced = true; return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ total_results: 0, stats: { search_execution_time: 0.001 }, facets: { customer: {}, file: {}, ledger: {}, qdro: {}, document: {}, phone: {} }, results: [], page_info: { current_page: 1, total_pages: 0, has_previous: false, has_next: false }, }), }); }); // Stub suggestions so they appear await page.route('**/api/search/suggestions*', async (route) => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ suggestions: [ { text: `${token} foo`, category: 'customer', description: '' }, { text: `${token} bar`, category: 'file', description: '' }, ], }), }); }); await page.goto('/search'); await page.fill('#searchQuery', token); const dropdown = page.locator('#searchSuggestions'); await expect(dropdown.locator('a')).toHaveCount(2, { timeout: 5000 }); await expect(dropdown).toBeVisible(); // Press Escape: should hide dropdown and not trigger search await page.keyboard.press('Escape'); await expect(dropdown).toHaveClass(/hidden/); expect(calledAdvanced).toBeFalsy(); }); });