start project
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user