Files
mobiniqow 212074c5f5
CI / Backend (lint, types, tests) (push) Has been cancelled
CI / Frontend (lint, types, build) (push) Has been cancelled
CI / Docker images build (push) Has been cancelled
start project
2026-09-04 12:52:21 +03:30

120 lines
5.2 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { DEMO_ANALYST } from './support/env';
/**
* End-to-end regression test for the bug reported against the running app:
* ticking one value in a facet (e.g. "Job family") made every other value in
* that same checkbox list disappear, because the backend's aggregations were
* computed on the already-filtered result set. Fixed in
* apps/search/backends/{elastic,postgres}.py (self-exclusion aggregations)
* and covered at the unit/API level by test_query_builder.py and
* test_search_api.py. This spec drives the real browser against the real,
* Elasticsearch-backed stack (`docker compose up`) end to end, so it also
* catches anything a backend-only test cannot: wiring in FacetGroup/
* FilterPanel, the network round trip, and URL state.
*/
async function signIn(page: import('@playwright/test').Page) {
await page.goto('/login');
await page.getByLabel('Email').fill(DEMO_ANALYST.email);
await page.getByLabel('Password').fill(DEMO_ANALYST.password);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('button', { name: /^Job family/ })).toBeVisible();
}
/** The raw facet value lives in the label's `title` attribute (see FacetGroup.tsx). */
async function facetValues(group: import('@playwright/test').Locator): Promise<string[]> {
const labels = group.locator('.facet__label');
const count = await labels.count();
const values: string[] = [];
for (let i = 0; i < count; i += 1) {
values.push((await labels.nth(i).getAttribute('title')) ?? '');
}
return values;
}
test.describe('multi-select facets', () => {
test('ticking one job-family value keeps every sibling value in the list', async ({ page }) => {
await signIn(page);
const jobFamily = page.locator('.facet').filter({ has: page.getByRole('button', { name: /^Job family/ }) });
const before = await facetValues(jobFamily);
expect(before.length).toBeGreaterThan(1);
const firstCheckbox = jobFamily.getByRole('checkbox').first();
const firstValue = await jobFamily.locator('.facet__label').first().getAttribute('title');
const responsePromise = page.waitForResponse((response) => response.url().includes('/api/v1/search/'));
await firstCheckbox.check();
await responsePromise;
await expect(firstCheckbox).toBeChecked();
// The exact bug: this used to collapse to just the ticked value.
const after = await facetValues(jobFamily);
expect(after.sort()).toEqual(before.sort());
// The URL is the source of truth for the filter state.
expect(page.url()).toContain(`roles=${encodeURIComponent(firstValue ?? '')}`);
});
test('ticking a second job-family value accumulates instead of replacing the first', async ({ page }) => {
await signIn(page);
const jobFamily = page.locator('.facet').filter({ has: page.getByRole('button', { name: /^Job family/ }) });
const checkboxes = jobFamily.getByRole('checkbox');
const labels = jobFamily.locator('.facet__label');
const firstValue = await labels.nth(0).getAttribute('title');
const secondValue = await labels.nth(1).getAttribute('title');
let responsePromise = page.waitForResponse((response) => response.url().includes('/api/v1/search/'));
await checkboxes.nth(0).check();
await responsePromise;
responsePromise = page.waitForResponse((response) => response.url().includes('/api/v1/search/'));
await checkboxes.nth(1).check();
await responsePromise;
await expect(checkboxes.nth(0)).toBeChecked();
await expect(checkboxes.nth(1)).toBeChecked();
const url = new URL(page.url());
const roles = (url.searchParams.get('roles') ?? '').split(',');
expect(roles).toContain(firstValue);
expect(roles).toContain(secondValue);
// Ticking the second one must not have removed the first from the DOM.
const after = await facetValues(jobFamily);
expect(after).toContain(firstValue);
expect(after).toContain(secondValue);
// "Clear all" must be able to undo both selections in one click.
await page.getByRole('button', { name: /clear all/i }).click();
await expect(checkboxes.nth(0)).not.toBeChecked();
});
test('a different facet still narrows when job family is filtered (no over-relaxing)', async ({ page }) => {
await signIn(page);
const jobFamily = page.locator('.facet').filter({ has: page.getByRole('button', { name: /^Job family/ }) });
const country = page.locator('.facet').filter({ has: page.getByRole('button', { name: /^Country/ }) });
// Country is collapsed by default (only the first three facets start
// open) - expand it so its checkbox list is actually in the DOM.
await page.getByRole('button', { name: /^Country/ }).click();
const countryBefore = await facetValues(country);
expect(countryBefore.length).toBeGreaterThan(0);
const responsePromise = page.waitForResponse((response) => response.url().includes('/api/v1/search/'));
await jobFamily.getByRole('checkbox').first().check();
await responsePromise;
const countryAfter = await facetValues(country);
// A facet with no selection of its own must still be scoped by the
// active filter - it should not also stay untouched.
expect(countryAfter.length).toBeLessThanOrEqual(countryBefore.length);
});
});