import { test, expect } from '@chromatic-com/playwright'; import { CONFIG } from './helpers'; /** * v1.0.9 W4 Day 18 — faceted search E2E. * * Two layers tested : * * 36. Backend gate — POST query params for bpm_min/bpm_max are * echoed into the SQL filter ; out-of-range values produce 400. * 37. UI — open /search, type a query, set BPM 120-130 in the * FacetSidebar, assert URL params updated. * * The "results restreints" verification depends on seed data carrying * tracks with measurable BPM values. The seed inserts placeholder * tracks without BPM today, so test 37 only verifies the URL/state * roundtrip ; once seed BPM data is in place (W5+), un-skip the * results-narrowing assertion at the bottom. */ interface ApiEnvelope { success?: boolean; data: T; error?: { code?: string; message?: string }; } test.describe('SEARCH — faceted filters (v1.0.9 W4 Day 18)', () => { test('36. backend rejects invalid bpm range with 400', async ({ request }) => { // bpm_min > bpm_max — handler should refuse the combination. const resp = await request.get( `${CONFIG.apiURL}/api/v1/search?q=test&bpm_min=200&bpm_max=100`, ); expect(resp.status(), 'invalid bpm range must be rejected client-side').toBe(400); }); test('37. backend rejects out-of-range BPM values', async ({ request }) => { const resp = await request.get( `${CONFIG.apiURL}/api/v1/search?q=test&bpm_min=99999`, ); expect(resp.status()).toBe(400); }); test('38. backend accepts a valid bpm range and returns 200', async ({ request }) => { const resp = await request.get( `${CONFIG.apiURL}/api/v1/search?q=test&bpm_min=80&bpm_max=130`, ); // Either 200 (results returned) or 200 with empty arrays — both // are valid as long as the filter parses. expect(resp.status(), 'valid faceted search must succeed').toBe(200); const body = (await resp.json()) as ApiEnvelope<{ tracks: unknown[]; artists: unknown[]; playlists: unknown[]; }>; const data = body.data ?? (body as unknown as { tracks: unknown[] }); expect(data.tracks).toBeInstanceOf(Array); }); test('39. UI — typing in the sidebar updates URL params', async ({ page }) => { test.setTimeout(20_000); await page.goto(`${CONFIG.baseURL}/search?q=rock`, { waitUntil: 'domcontentloaded' }); // Sidebar mounts only when the query is non-empty (see SearchPage). const sidebar = page.getByTestId('facet-sidebar'); await expect(sidebar).toBeVisible({ timeout: 5_000 }); await page.getByTestId('facet-bpm-min').fill('120'); await page.getByTestId('facet-bpm-max').fill('130'); // Debounce window for facets is 300 ms ; URL update is paired // with the search call, so wait for it. await page.waitForFunction( () => window.location.search.includes('bpm_min=120') && window.location.search.includes('bpm_max=130'), { timeout: 5_000 }, ); const url = new URL(page.url()); expect(url.searchParams.get('bpm_min')).toBe('120'); expect(url.searchParams.get('bpm_max')).toBe('130'); }); });