84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
|
|
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<T> {
|
||
|
|
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');
|
||
|
|
});
|
||
|
|
});
|