test(discovery): reconcile native journeys with filter drawer

This commit is contained in:
cesnimda
2026-11-11 07:56:57 +01:00
parent 5a880a6ac7
commit df8f4dbbd3
33 changed files with 923 additions and 24 deletions
@@ -0,0 +1 @@
{"state":"native-delete-confirm-initial","focused":{"tag":"DIV","id":"","role":"presentation","label":null,"text":"Slette lagret søk?Denne handlingen kan være vanskelig å angre.Slette «Gjennomgått utviklersøk Æ Ø Å» og historikken over viste og avviste st"},"qualification":"Actual document.activeElement in isolated Chromium, not a human screen-reader or physical-device acceptance claim."}
@@ -0,0 +1 @@
{"state":"native-delete-confirm-initial","focused":{"tag":"DIV","id":"","role":"presentation","label":null,"text":"Slette lagret søk?Denne handlingen kan være vanskelig å angre.Slette «Gjennomgått utviklersøk Æ Ø Å» og historikken over viste og avviste st"},"qualification":"Actual document.activeElement in isolated Chromium, not a human screen-reader or physical-device acceptance claim."}
@@ -0,0 +1,239 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: phase7-discovery.spec.ts >> [P7-05 Discovery] keyboard native create and alert updates persist; delete Cancel restores focus and held delete starts once
- Location: e2e\phase7-discovery.spec.ts:354:1
# Error details
```
Error: expect(locator).toBeFocused() failed
Locator: getByRole('dialog').filter({ has: getByRole('heading', { name: 'Slette lagret søk?', exact: true }) }).getByRole('button', { name: 'Avbryt', exact: true })
Expected: focused
Received: inactive
Timeout: 5000ms
Call log:
- Expect "toBeFocused" with timeout 5000ms
- waiting for getByRole('dialog').filter({ has: getByRole('heading', { name: 'Slette lagret søk?', exact: true }) }).getByRole('button', { name: 'Avbryt', exact: true })
14 × locator resolved to <button tabindex="0" type="button" class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary css-v8jn2b-MuiButtonBase-root-MuiButton-root">…</button>
- unexpected value "inactive"
```
```yaml
- button "Avbryt"
```
# Test source
```ts
300 | const job = vacancy('mutation-current');
301 | await page.route(`${savedUrl}/${saved.id}/run`, async route => route.fulfill({ json: await runPayload(page, saved, [job]) }));
302 | let createFails = true; let alertFails = true; let deleteFails = true;
303 | await page.route(savedUrl, route => route.request().method() === 'POST' && createFails
304 | ? route.fulfill({ status: 500, json: { message: 'Controlled create failure' } }) : route.continue());
305 | await page.route(`${savedUrl}/${saved.id}`, route => (route.request().method() === 'PATCH' && alertFails) || (route.request().method() === 'DELETE' && deleteFails)
306 | ? route.fulfill({ status: 500, json: { message: 'Controlled mutation failure' } }) : route.continue());
307 | await enter(page, session.owner);
308 | await keyboardActivate(page, savedGroup(page, saved).getByRole('button', { name: t.jobDiscoveryRunSaved, exact: true }));
309 | await expect(article(page, job)).toBeVisible();
310 | await draftFilters(page, 'retained draft role', 'Ålesund');
311 | await page.getByLabel(t.jobDiscoverySavedName, { exact: true }).fill('Retained failed create draft');
312 | await keyboardActivate(page, page.getByRole('button', { name: t.jobDiscoverySaveSearch, exact: true }));
313 | await expect(page.getByText(t.jobDiscoverySaveSearchFailed, { exact: true })).toBeVisible();
314 | await expect(page.getByLabel(t.jobDiscoverySavedName, { exact: true })).toHaveValue('Retained failed create draft');
315 | await expect(page.getByLabel(t.jobDiscoveryRoleCompany, { exact: true })).toHaveValue('retained draft role');
316 | await expect(page.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
317 | expect(await nativeList(page.request)).toEqual([saved]);
318 | createFails = false;
319 | const createdResponse = response(page, 'POST', savedUrl);
320 | await keyboardActivate(page, page.getByRole('button', { name: t.jobDiscoverySaveSearch, exact: true }));
321 | const created = await json<SavedSearch>(await createdResponse, 201);
322 | expect(created).toMatchObject({ name: 'Retained failed create draft', query: 'retained draft role', location: 'Ålesund' });
323 | await keyboardActivate(page, savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true }), 'Space');
324 | await expect(page.getByText(t.jobDiscoveryUpdateAlertsFailed, { exact: true })).toBeVisible();
325 | await expect(savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true })).toBeChecked();
326 | await expect(article(page, job)).toBeVisible();
327 | await expect(page.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
328 | alertFails = false;
329 | const updatedResponse = response(page, 'PATCH', `${savedUrl}/${saved.id}`);
330 | await keyboardActivate(page, savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true }), 'Space');
331 | const updated = await json<SavedSearch>(await updatedResponse);
332 | expect(updated).toEqual({ ...saved, isActive: false });
333 | await keyboardActivate(page, savedGroup(page, saved).getByRole('button', { name: t.adminUsersDelete, exact: true }));
334 | await keyboardActivate(page, page.getByRole('dialog').getByRole('button', { name: t.adminUsersDelete, exact: true }));
335 | await expect(page.getByText(t.jobDiscoveryDeleteSavedFailed, { exact: true })).toBeVisible();
336 | await expect(savedGroup(page, saved)).toBeVisible();
337 | await expect(article(page, job)).toBeVisible();
338 | await expect(page.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
339 | // Native listing sorts by UpdatedAt, and pausing the older row updates that timestamp.
340 | // Compare every DTO value without inventing creation-order semantics for this readback.
341 | expect((await nativeList(page.request)).sort((left, right) => left.id - right.id)).toEqual([created, updated].sort((left, right) => left.id - right.id));
342 | expect(count(session.requests, 'GET', '/api/job-discovery/search')).toBe(0);
343 | await capture(page, info, desktop, 'mutation-failure-retains-context');
344 | deleteFails = false;
345 | const deletedResponse = response(page, 'DELETE', `${savedUrl}/${saved.id}`);
346 | await keyboardActivate(page, savedGroup(page, saved).getByRole('button', { name: t.adminUsersDelete, exact: true }));
347 | await keyboardActivate(page, page.getByRole('dialog').getByRole('button', { name: t.adminUsersDelete, exact: true }));
348 | expect((await deletedResponse).status()).toBe(204);
349 | await expect(savedGroup(page, saved)).toHaveCount(0);
350 | expect(await nativeList(page.request)).toEqual([created]);
351 | expect(count(session.requests, 'GET', '/api/job-discovery/search')).toBe(0);
352 | });
353 |
354 | test('[P7-05 Discovery] keyboard native create and alert updates persist; delete Cancel restores focus and held delete starts once', async ({ page, session }, info) => {
355 | test.setTimeout(120_000);
356 | const t = translations.nb; const sample: Sample = { language: 'nb', theme: 'light', width: 360, height: 844 };
357 | const currentJob = vacancy('native-delete-current-manual');
358 | await page.route(`${searchUrl}**`, route => route.fulfill({ json: [currentJob] }));
359 | const createGate = hold(); const deleteGate = hold(); let creates = 0; let deletes = 0;
360 | await page.route(savedUrl, async route => {
361 | if (route.request().method() === 'POST') { creates++; await createGate.pending; }
362 | return route.continue();
363 | });
364 | await enter(page, session.owner, sample);
365 | await expect(page.getByText(t.jobDiscoveryNoSavedSearches, { exact: true })).toBeVisible();
366 | await draftFilters(page, ' utvikler ', ' Tromsø ', 'nb', ' Gjennomgått utviklersøk Æ Ø Å ');
367 | const createdResponse = response(page, 'POST', savedUrl);
368 | try {
369 | await keyboardActivate(page, page.getByRole('button', { name: t.jobDiscoverySaveSearch, exact: true }));
370 | await expect.poll(() => creates).toBe(1);
371 | await locked(page, 'nb'); await page.keyboard.press('Enter'); expect(creates).toBe(1);
372 | await capture(page, info, sample, 'native-create-held');
373 | } finally { createGate.release(); }
374 | const saved = await json<SavedSearch>(await createdResponse, 201);
375 | expect(saved).toMatchObject({ name: 'Gjennomgått utviklersøk Æ Ø Å', query: 'utvikler', location: 'Tromsø', isActive: true, resultCount: 0, dismissedCount: 0 });
376 | await expect(savedGroup(page, saved)).toBeVisible();
377 | expect(await nativeList(page.request)).toEqual([saved]);
378 | await page.reload(); await openFilters(page, 'nb'); await expect(savedGroup(page, saved)).toBeVisible();
379 | for (const isActive of [false, true]) {
380 | const updatedResponse = response(page, 'PATCH', `${savedUrl}/${saved.id}`);
381 | await keyboardActivate(page, savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true }), 'Space');
382 | expect(await json<SavedSearch>(await updatedResponse)).toEqual({ ...saved, isActive });
383 | expect(await nativeList(page.request)).toEqual([{ ...saved, isActive }]);
384 | await page.reload();
385 | await openFilters(page, 'nb');
386 | await expect(savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true })).toBeChecked({ checked: isActive });
387 | }
388 | // Keep a legitimate results control after deleting the saved row so the next Tab
389 | // has a concrete destination inside the stable region rather than browser chrome.
390 | await keyboardActivate(page, page.getByRole('button', { name: t.jobDiscoverySearchNav, exact: true }));
391 | await expect(article(page, currentJob)).toBeVisible();
392 | await openFilters(page, 'nb');
393 | const deleteButton = savedGroup(page, saved).getByRole('button', { name: t.adminUsersDelete, exact: true });
394 | await keyboardActivate(page, deleteButton);
395 | const dialog = page.getByRole('dialog').filter({ has: page.getByRole('heading', { name: t.jobDiscoveryDeleteSavedTitle, exact: true }) });
396 | await expect(dialog.getByRole('heading', { name: t.jobDiscoveryDeleteSavedTitle, exact: true })).toBeVisible();
397 | await expect(dialog.getByText(t.jobDiscoveryDeleteSavedConfirm.replace('{name}', saved.name), { exact: true })).toBeVisible();
398 | const cancel = dialog.getByRole('button', { name: t.cancel, exact: true });
399 | await recordFocus(page, info, 'native-delete-confirm-initial');
> 400 | await expect(cancel).toBeFocused();
| ^ Error: expect(locator).toBeFocused() failed
401 | await page.keyboard.press('Shift+Tab');
402 | const messageRegion = dialog.getByRole('region', { name: await dialog.getByRole('heading').innerText(), exact: true });
403 | await expect(messageRegion).toBeFocused();
404 | await messageRegion.press('Shift+Tab');
405 | await expect(dialog.getByRole('button', { name: t.adminUsersDelete, exact: true })).toBeFocused();
406 | await page.keyboard.press('Tab'); await expect(messageRegion).toBeFocused();
407 | await page.keyboard.press('Tab'); await expect(cancel).toBeFocused();
408 | await page.keyboard.press('Enter');
409 | await expect(dialog).toHaveCount(0); await expect(deleteButton).toBeFocused();
410 | expect(count(session.requests, 'DELETE', `/api/job-discovery/saved-searches/${saved.id}`)).toBe(0);
411 | expect(await nativeList(page.request)).toEqual([saved]);
412 | await page.route(`${savedUrl}/${saved.id}`, async route => {
413 | if (route.request().method() === 'DELETE') { deletes++; await deleteGate.pending; }
414 | return route.continue();
415 | });
416 | const deletedResponse = response(page, 'DELETE', `${savedUrl}/${saved.id}`);
417 | try {
418 | await keyboardActivate(page, deleteButton);
419 | await keyboardActivate(page, dialog.getByRole('button', { name: t.adminUsersDelete, exact: true }));
420 | await expect.poll(() => deletes).toBe(1);
421 | await expect(dialog).toHaveCount(0);
422 | await locked(page, 'nb'); await page.keyboard.press('Enter'); expect(deletes).toBe(1);
423 | await capture(page, info, sample, 'native-delete-held');
424 | } finally { deleteGate.release(); }
425 | expect((await deletedResponse).status()).toBe(204);
426 | await expect(savedGroup(page, saved)).toHaveCount(0);
427 | await closeFilters(page, 'nb', true);
428 | await recordFocus(page, info, 'native-delete-control-removed');
429 | await expect(results(page, 'nb')).toBeFocused();
430 | await page.keyboard.press('Tab');
431 | await recordFocus(page, info, 'native-delete-next-tab');
432 | await expect(page.getByLabel(t.jobDiscoverySort, { exact: true })).toBeFocused();
433 | await expect(article(page, currentJob)).toBeVisible();
434 | expect(await nativeList(page.request)).toEqual([]);
435 | await page.reload(); await openFilters(page, 'nb'); await expect(page.getByText(t.jobDiscoveryNoSavedSearches, { exact: true })).toBeVisible();
436 | expect(creates).toBe(1); expect(deletes).toBe(1);
437 | });
438 |
439 | test('[P7-05 Discovery] contextual Dismiss preserves a row on real missing-result denial and qualifies intercepted UI recovery', async ({ page, session }, info) => {
440 | test.setTimeout(90_000);
441 | const t = translations.en; const sample = { ...desktop, theme: 'dark' as const, width: 390, height: 844 };
442 | const saved = await createSaved(page, session.owner, 'Recorded dismiss owner context');
443 | const job = vacancy('p7-dismiss-encoded Æ Ø Å');
444 | const movedFocusJob = vacancy('p7-dismiss-second-moved-focus');
445 | await page.route(`${savedUrl}/${saved.id}/run`, async route => route.fulfill({ json: await runPayload(page, saved, [job, movedFocusJob]) }));
446 | await enter(page, session.owner, sample);
447 | await keyboardActivate(page, savedGroup(page, saved).getByRole('button', { name: t.jobDiscoveryRunSaved, exact: true }));
448 | await expect(article(page, job)).toBeVisible();
449 | const dismissUrl = `${savedUrl}/${saved.id}/results/${encodeURIComponent(job.id)}/dismiss`;
450 | const deniedResponse = response(page, 'PATCH', dismissUrl);
451 | await keyboardActivate(page, article(page, job).getByRole('button', { name: t.jobDiscoveryDismiss, exact: true }));
452 | expect((await deniedResponse).status()).toBe(404);
453 | await expect(results(page).getByText(t.jobDiscoveryDismissFailed, { exact: true })).toBeVisible();
454 | await expect(article(page, job)).toBeVisible();
455 | await expect(page.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
456 | expect(await nativeList(page.request)).toEqual([saved]);
457 | await capture(page, info, sample, 'dismiss-native-missing-denied');
458 | const gate = hold(); let starts = 0;
459 | await page.route(dismissUrl, async route => {
460 | expect(route.request().method()).toBe('PATCH'); expect(route.request().postDataJSON()).toEqual({ isDismissed: true });
461 | starts++; await gate.pending; return route.fulfill({ status: 204 });
462 | });
463 | try {
464 | await keyboardActivate(page, article(page, job).getByRole('button', { name: t.jobDiscoveryDismiss, exact: true }));
465 | await expect.poll(() => starts).toBe(1); await locked(page); await page.keyboard.press('Enter'); expect(starts).toBe(1);
466 | } finally { gate.release(); }
467 | await expect(article(page, job)).toHaveCount(0);
468 | await expect(results(page).getByText(t.jobDiscoveryDismissFailed, { exact: true })).toHaveCount(0);
469 | await recordFocus(page, info, 'dismiss-control-removed');
470 | await expect(results(page)).toBeFocused();
471 | await page.keyboard.press('Tab');
472 | await recordFocus(page, info, 'dismiss-next-tab');
473 | await expect(page.getByLabel(t.jobDiscoverySort, { exact: true })).toBeFocused();
474 | await expect(article(page, movedFocusJob)).toBeVisible();
475 | const movedGate = hold();
476 | const movedUrl = `${savedUrl}/${saved.id}/results/${encodeURIComponent(movedFocusJob.id)}/dismiss`;
477 | let movedStarts = 0;
478 | await page.route(movedUrl, async route => {
479 | expect(route.request().method()).toBe('PATCH'); expect(route.request().postDataJSON()).toEqual({ isDismissed: true });
480 | movedStarts++; await movedGate.pending; return route.fulfill({ status: 204 });
481 | });
482 | const headerControl = page.locator('header').getByRole('button', { name: t.user, exact: true });
483 | try {
484 | await keyboardActivate(page, article(page, movedFocusJob).getByRole('button', { name: t.jobDiscoveryDismiss, exact: true }));
485 | await expect.poll(() => movedStarts).toBe(1); await locked(page);
486 | await expect(headerControl).toBeEnabled(); await headerControl.focus(); await expect(headerControl).toBeFocused();
487 | await recordFocus(page, info, 'held-dismiss-user-moved-focus');
488 | } finally { movedGate.release(); }
489 | await expect(article(page, movedFocusJob)).toHaveCount(0);
490 | await recordFocus(page, info, 'dismiss-complete-user-focus-preserved');
491 | await expect(headerControl).toBeFocused();
492 | expect(movedStarts).toBe(1);
493 | expect(await nativeList(page.request)).toEqual([saved]);
494 | expect(count(session.requests, 'GET', '/api/job-discovery/search')).toBe(0);
495 | const patches = session.requests.filter(request => request.path.endsWith('/dismiss'));
496 | expect(patches).toHaveLength(3);
497 | expect(patches.map(request => request.body)).toEqual([{ isDismissed: true }, { isDismissed: true }, { isDismissed: true }]);
498 | await capture(page, info, sample, 'dismiss-intercepted-ui-only');
499 | await page.reload();
500 | await openFilters(page);
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -0,0 +1,46 @@
{
"[P7-05 Discovery] held saved B removes A rows, locks all mutations and starts one request": {
"status": "passed",
"run": "final-stable11"
},
"[P7-05 Discovery] failed saved-run Retry posts the same ID with stable keyboard focus and held progress": {
"status": "passed",
"run": "final-stable11"
},
"[P7-05 Discovery] keyboard native create and alert updates persist; delete Cancel restores focus and held delete starts once": {
"status": "passed",
"run": "final-stable11"
},
"[P5-15-01R-01] saved-list faults retry a native GET without resetting discovery across EN/Bokmål, themes and widths": {
"status": "passed",
"run": "final-stable11"
},
"[P7-05 Discovery] saved A to manual search clears saved-result context and preserves native saved rows": {
"status": "passed",
"run": "final-stable11"
},
"[P7-05 Discovery] mutation failures retain drafts and current rows without offering an unrelated manual-search retry": {
"status": "passed",
"run": "final-stable11"
},
"[P7-05 Discovery] contextual Dismiss preserves a row on real missing-result denial and qualifies intercepted UI recovery": {
"status": "passed",
"run": "final-stable11"
},
"[P7-05 Discovery] eight finite responsive locale/theme states retain source labels and all-enabled accessibility vectors": {
"status": "passed",
"run": "final-stable11"
},
"[P7-05 Discovery] header locale change retains drafts, selected saved source and rows without discovery refetch": {
"status": "passed",
"run": "final-stable11"
},
"[P5-15-01R-01] native saved NAV search survives Jobs navigation, reload, apply, pause/resume and delete": {
"status": "passed",
"run": "final-stable11"
},
"[P5-15-01R-01] native owner filters and crafted saved-search mutations deny foreign and anonymous access": {
"status": "passed",
"run": "final-stable11"
}
}
@@ -0,0 +1,236 @@
{
"baseline": "ec57a24d77854bb416c9ea02b8b371be3e5554c0",
"rootAppFix": "df93bfb64eb80dbfa30f86c89b54a9021c43a9c6",
"localAppFixCherryPick": "a584fc0b",
"qualification": "Final combined11 binds exact final test source. Intermediate runs overlap; do not sum them. Exact unmodified native Playwright JSON/list stdout compressed with gzip.",
"finalSourceSha256": {
"job-tracker-ui/e2e/phase7-discovery.spec.ts": "f200418dc84e99abbe0eb435e1356668ef568ff668d6179b748d927e4df846e6",
"job-tracker-ui/e2e/saved-search-repair.spec.ts": "d15f1736fca5f35e4b6ce2070314e3691bc0bd9ea9289c23cff4423a05a6ad61",
"job-tracker-ui/src/confirm.tsx": "add83991431cf3639acfcdbeb9a76eb24037477141153dca3a93082ed6cb1bb2",
"job-tracker-ui/src/confirm.test.tsx": "00d27162d74a405892e889dc3ce436104df48d2504d8ae554b3699e25a946d9d"
},
"runStats": {
"pilot": {
"startTime": "2026-11-11T06:34:24.274Z",
"duration": 82933.107,
"expected": 3,
"skipped": 0,
"unexpected": 1,
"flaky": 0
},
"remainder": {
"startTime": "2026-11-11T06:36:21.659Z",
"duration": 106519.323,
"expected": 5,
"skipped": 0,
"unexpected": 2,
"flaky": 0
},
"focused": {
"startTime": "2026-11-11T06:38:41.263Z",
"duration": 48073.14,
"expected": 1,
"skipped": 0,
"unexpected": 2,
"flaky": 0
},
"dismiss": {
"startTime": "2026-11-11T06:40:37.672Z",
"duration": 34489.103,
"expected": 3,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"focus-preserved": {
"startTime": "2026-11-11T06:44:04.390Z",
"duration": 36403.344000000005,
"expected": 2,
"skipped": 0,
"unexpected": 1,
"flaky": 0
},
"confirm-fix": {
"startTime": "2026-11-11T06:45:15.340Z",
"duration": 39776.71599999999,
"expected": 1,
"skipped": 0,
"unexpected": 1,
"flaky": 0
},
"held-final": {
"startTime": "2026-11-11T06:46:26.787Z",
"duration": 22094.513,
"expected": 0,
"skipped": 0,
"unexpected": 1,
"flaky": 0
},
"held-stable": {
"startTime": "2026-11-11T06:48:03.414Z",
"duration": 19931.281,
"expected": 1,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"final11": {
"startTime": "2026-11-11T06:48:46.652Z",
"duration": 151275.851,
"expected": 10,
"skipped": 0,
"unexpected": 1,
"flaky": 0
},
"final-stable11": {
"startTime": "2026-11-11T06:52:07.557Z",
"duration": 143011.842,
"expected": 11,
"skipped": 0,
"unexpected": 0,
"flaky": 0
}
},
"receipts": [
{
"file": "a3-discovery-native-confirm-fix.json.gz",
"rawBytes": 125386,
"rawSha256": "2bcdef8d0839552976b8d88cf28dd1fe1f0aacb1de5c8bb6a350e29c8a49c4b7",
"compressedBytes": 71828,
"compressedSha256": "44de61301ce4b46f7f9ef717ac03d8831d6596740599cc251cc0b5699c70ef2c"
},
{
"file": "a3-discovery-native-confirm-fix.txt.gz",
"rawBytes": 5561,
"rawSha256": "de74ee370d62bfbfe299921310f306fed0e3131a06143f275c7226825445105b",
"compressedBytes": 1637,
"compressedSha256": "3b7cc65423e552d35ea23455a48a3db8ca0cdc7273a5b51bf197fdbc5ef1a484"
},
{
"file": "a3-discovery-native-dismiss.json.gz",
"rawBytes": 360692,
"rawSha256": "03260dd567161339ca1f09f43e1c14ba5f21ade92b66b9c6e2f539c618fae319",
"compressedBytes": 225572,
"compressedSha256": "e09195b5cebbc325cfa54eaf0f9485bc1fd9f030499fdf5f0dd730c336b431f3"
},
{
"file": "a3-discovery-native-dismiss.txt.gz",
"rawBytes": 2351,
"rawSha256": "cf287911d67a050b00cc7102f0f27fc52538c63ddb348d660f8ee908a63b67e8",
"compressedBytes": 1064,
"compressedSha256": "4df447eb2bf08d56c54f851d6cea0acd5dfb1ef76677fa3ddaed16723f383259"
},
{
"file": "a3-discovery-native-final-stable11.json.gz",
"rawBytes": 2196635,
"rawSha256": "5b5958a516fd8fb22493ef982fee4b6ca8d512d78a460f0562c60755ab35b54c",
"compressedBytes": 1411952,
"compressedSha256": "106363b7f6e51914e947222608a2874903f03fd025e22bd7d91bcd38396193ad"
},
{
"file": "a3-discovery-native-final-stable11.txt.gz",
"rawBytes": 3831,
"rawSha256": "7d0e1b4c0cefb6fbf2ebf584ea90f35327d3618324f30ced0c1f102b7e2826cb",
"compressedBytes": 1536,
"compressedSha256": "4051bf8f98abd94f288b4c2e4e298e7aa20a1024a06d94b33eebaaed9ae0414f"
},
{
"file": "a3-discovery-native-final11.json.gz",
"rawBytes": 2152219,
"rawSha256": "8d4ab84e092066f7426bda9ebf4e2b519f94b06fe263874029afb0144ef83a70",
"compressedBytes": 1378115,
"compressedSha256": "df44a04ca26d7f089578655ebd35df19fd861b15eb9c8accaf9cc11bcd24081c"
},
{
"file": "a3-discovery-native-final11.txt.gz",
"rawBytes": 7208,
"rawSha256": "4d55fd6f94b1f8904f862d5cc16467e63e788531f6c85da06dbbb12293c7cee2",
"compressedBytes": 2185,
"compressedSha256": "706ba274f2e3fba37374e4386a72d9c845905d9782eac2b1202112c0e1aacb08"
},
{
"file": "a3-discovery-native-focus-preserved.json.gz",
"rawBytes": 318096,
"rawSha256": "6b45faa989b563911545cc02f314e1974d451a8ebab65dc57810da0c98252b03",
"compressedBytes": 195869,
"compressedSha256": "1b69b95028598529c62d437a56b3de3cfd0a81ec7599816eee0444a2612e3006"
},
{
"file": "a3-discovery-native-focus-preserved.txt.gz",
"rawBytes": 5727,
"rawSha256": "a4eae73ce086c281ed6eac7139dd565a1a865923654ec65f1a67ec21a7b7599e",
"compressedBytes": 1701,
"compressedSha256": "339f8035d58faecd6b636852fe49229646e008eca30edc4e24af13363364ab62"
},
{
"file": "a3-discovery-native-focused.json.gz",
"rawBytes": 353623,
"rawSha256": "98560a59a7b5691688207a5f85e9882a8394ed5a98809d86fd4bda6dd96bfa93",
"compressedBytes": 228545,
"compressedSha256": "201b1c076c9fc64857d76d084ae6155b6c2e343170457c158ed4f5d64930b40f"
},
{
"file": "a3-discovery-native-focused.txt.gz",
"rawBytes": 9509,
"rawSha256": "033d9699c6429416b3dfe35bc35afa56b0e3c7bef4f2a276aa1e6a6d3f85a298",
"compressedBytes": 2281,
"compressedSha256": "3de431d125ca055bc83e915aa858697b91a936d68a83e8b073d9b18cb94f9cdb"
},
{
"file": "a3-discovery-native-held-final.json.gz",
"rawBytes": 11327,
"rawSha256": "ff9bed8033718bac06e5de5753a5f6635bd497a1e988bee8f17b7858bdb010ee",
"compressedBytes": 3041,
"compressedSha256": "da36500a93e15a1df50dbeb5a3d4628c8eb83dc4a3f01074e63a61071e480274"
},
{
"file": "a3-discovery-native-held-final.txt.gz",
"rawBytes": 5178,
"rawSha256": "92c155554b30460916eb5f56362be08fdebf0924a272a10150fef3f11e3390a6",
"compressedBytes": 1547,
"compressedSha256": "ab599279a3857eb172395c0ddf36018368b7c96c9e37af26842978e9ee946a36"
},
{
"file": "a3-discovery-native-held-stable.json.gz",
"rawBytes": 53932,
"rawSha256": "91f44f31c4886cd690380cae1bc3d1084e59a9a00bc8dd48f09121c0bbf7c9bb",
"compressedBytes": 32756,
"compressedSha256": "40a43e88eb039f22d7c1740b2983f9acff25c139379c3dc764cf3f826fc32269"
},
{
"file": "a3-discovery-native-held-stable.txt.gz",
"rawBytes": 1987,
"rawSha256": "097b998c48681b4a5a1ffdd2a787a75bf3d53b08d702bceb7ee3e84ad97fdb5c",
"compressedBytes": 926,
"compressedSha256": "ab67b2a599a1f1bcc24634a290b986df9a4c009fa77862ecb495d27db2d4e45d"
},
{
"file": "a3-discovery-native-pilot.json.gz",
"rawBytes": 326700,
"rawSha256": "b364c617f90202d8071ea9a3af4386815ba3950378381c7031cae821884ed004",
"compressedBytes": 144420,
"compressedSha256": "d1bd1e7a4a41888134d580af9114095278fbd81301720dbd2a2eda17571da186"
},
{
"file": "a3-discovery-native-pilot.txt.gz",
"rawBytes": 6282,
"rawSha256": "4f936c3d29287363bfd113af20353e2e907615e7a678e5c08992149d3241306a",
"compressedBytes": 1956,
"compressedSha256": "009133f8d0677ee890b0f60cbe02a83285c347fa5188dad92f7367da8e6c78e4"
},
{
"file": "a3-discovery-native-remainder.json.gz",
"rawBytes": 1488998,
"rawSha256": "5c506906896c47efd8480aa6ed50737df05b4898a5a7ae84fd85c6f262862a21",
"compressedBytes": 1004042,
"compressedSha256": "aa38ca22efe7712c1fdca00a730e897147341bb922695f40825ee455811cdad5"
},
{
"file": "a3-discovery-native-remainder.txt.gz",
"rawBytes": 11442,
"rawSha256": "65fd7f3dd7e7929dd482ec9850bcc13907cf5cf7eb03e7844fb5f825a04849f3",
"compressedBytes": 2639,
"compressedSha256": "35ddf2e494f161177701f5ea6eb6f55124dd391a6e9a903bfc87102e0bce7526"
}
]
}
@@ -0,0 +1,239 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: phase7-discovery.spec.ts >> [P7-05 Discovery] keyboard native create and alert updates persist; delete Cancel restores focus and held delete starts once
- Location: e2e\phase7-discovery.spec.ts:354:1
# Error details
```
Error: expect(locator).toBeFocused() failed
Locator: getByRole('dialog').filter({ has: getByRole('heading', { name: 'Slette lagret søk?', exact: true }) }).getByRole('button', { name: 'Avbryt', exact: true })
Expected: focused
Received: inactive
Timeout: 5000ms
Call log:
- Expect "toBeFocused" with timeout 5000ms
- waiting for getByRole('dialog').filter({ has: getByRole('heading', { name: 'Slette lagret søk?', exact: true }) }).getByRole('button', { name: 'Avbryt', exact: true })
14 × locator resolved to <button tabindex="0" type="button" class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary css-v8jn2b-MuiButtonBase-root-MuiButton-root">…</button>
- unexpected value "inactive"
```
```yaml
- button "Avbryt"
```
# Test source
```ts
299 | const saved = await createSaved(page, session.owner, 'Recorded mutation recovery');
300 | const job = vacancy('mutation-current');
301 | await page.route(`${savedUrl}/${saved.id}/run`, async route => route.fulfill({ json: await runPayload(page, saved, [job]) }));
302 | let createFails = true; let alertFails = true; let deleteFails = true;
303 | await page.route(savedUrl, route => route.request().method() === 'POST' && createFails
304 | ? route.fulfill({ status: 500, json: { message: 'Controlled create failure' } }) : route.continue());
305 | await page.route(`${savedUrl}/${saved.id}`, route => (route.request().method() === 'PATCH' && alertFails) || (route.request().method() === 'DELETE' && deleteFails)
306 | ? route.fulfill({ status: 500, json: { message: 'Controlled mutation failure' } }) : route.continue());
307 | await enter(page, session.owner);
308 | await keyboardActivate(page, savedGroup(page, saved).getByRole('button', { name: t.jobDiscoveryRunSaved, exact: true }));
309 | await expect(article(page, job)).toBeVisible();
310 | await draftFilters(page, 'retained draft role', 'Ålesund');
311 | await page.getByLabel(t.jobDiscoverySavedName, { exact: true }).fill('Retained failed create draft');
312 | await keyboardActivate(page, page.getByRole('button', { name: t.jobDiscoverySaveSearch, exact: true }));
313 | await expect(page.getByText(t.jobDiscoverySaveSearchFailed, { exact: true })).toBeVisible();
314 | await expect(page.getByLabel(t.jobDiscoverySavedName, { exact: true })).toHaveValue('Retained failed create draft');
315 | await expect(page.getByLabel(t.jobDiscoveryRoleCompany, { exact: true })).toHaveValue('retained draft role');
316 | await expect(page.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
317 | expect(await nativeList(page.request)).toEqual([saved]);
318 | createFails = false;
319 | const createdResponse = response(page, 'POST', savedUrl);
320 | await keyboardActivate(page, page.getByRole('button', { name: t.jobDiscoverySaveSearch, exact: true }));
321 | const created = await json<SavedSearch>(await createdResponse, 201);
322 | expect(created).toMatchObject({ name: 'Retained failed create draft', query: 'retained draft role', location: 'Ålesund' });
323 | await keyboardActivate(page, savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true }), 'Space');
324 | await expect(page.getByText(t.jobDiscoveryUpdateAlertsFailed, { exact: true })).toBeVisible();
325 | await expect(savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true })).toBeChecked();
326 | await expect(article(page, job)).toBeVisible();
327 | await expect(page.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
328 | alertFails = false;
329 | const updatedResponse = response(page, 'PATCH', `${savedUrl}/${saved.id}`);
330 | await keyboardActivate(page, savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true }), 'Space');
331 | const updated = await json<SavedSearch>(await updatedResponse);
332 | expect(updated).toEqual({ ...saved, isActive: false });
333 | await keyboardActivate(page, savedGroup(page, saved).getByRole('button', { name: t.adminUsersDelete, exact: true }));
334 | await keyboardActivate(page, page.getByRole('dialog').getByRole('button', { name: t.adminUsersDelete, exact: true }));
335 | await expect(page.getByText(t.jobDiscoveryDeleteSavedFailed, { exact: true })).toBeVisible();
336 | await expect(savedGroup(page, saved)).toBeVisible();
337 | await expect(article(page, job)).toBeVisible();
338 | await expect(page.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
339 | // Native listing sorts by UpdatedAt, and pausing the older row updates that timestamp.
340 | // Compare every DTO value without inventing creation-order semantics for this readback.
341 | expect((await nativeList(page.request)).sort((left, right) => left.id - right.id)).toEqual([created, updated].sort((left, right) => left.id - right.id));
342 | expect(count(session.requests, 'GET', '/api/job-discovery/search')).toBe(0);
343 | await capture(page, info, desktop, 'mutation-failure-retains-context');
344 | deleteFails = false;
345 | const deletedResponse = response(page, 'DELETE', `${savedUrl}/${saved.id}`);
346 | await keyboardActivate(page, savedGroup(page, saved).getByRole('button', { name: t.adminUsersDelete, exact: true }));
347 | await keyboardActivate(page, page.getByRole('dialog').getByRole('button', { name: t.adminUsersDelete, exact: true }));
348 | expect((await deletedResponse).status()).toBe(204);
349 | await expect(savedGroup(page, saved)).toHaveCount(0);
350 | expect(await nativeList(page.request)).toEqual([created]);
351 | expect(count(session.requests, 'GET', '/api/job-discovery/search')).toBe(0);
352 | });
353 |
354 | test('[P7-05 Discovery] keyboard native create and alert updates persist; delete Cancel restores focus and held delete starts once', async ({ page, session }, info) => {
355 | test.setTimeout(120_000);
356 | const t = translations.nb; const sample: Sample = { language: 'nb', theme: 'light', width: 360, height: 844 };
357 | const currentJob = vacancy('native-delete-current-manual');
358 | await page.route(`${searchUrl}**`, route => route.fulfill({ json: [currentJob] }));
359 | const createGate = hold(); const deleteGate = hold(); let creates = 0; let deletes = 0;
360 | await page.route(savedUrl, async route => {
361 | if (route.request().method() === 'POST') { creates++; await createGate.pending; }
362 | return route.continue();
363 | });
364 | await enter(page, session.owner, sample);
365 | await expect(page.getByText(t.jobDiscoveryNoSavedSearches, { exact: true })).toBeVisible();
366 | await draftFilters(page, ' utvikler ', ' Tromsø ', 'nb', ' Gjennomgått utviklersøk Æ Ø Å ');
367 | const createdResponse = response(page, 'POST', savedUrl);
368 | try {
369 | await keyboardActivate(page, page.getByRole('button', { name: t.jobDiscoverySaveSearch, exact: true }));
370 | await expect.poll(() => creates).toBe(1);
371 | await locked(page, 'nb'); await page.keyboard.press('Enter'); expect(creates).toBe(1);
372 | await capture(page, info, sample, 'native-create-held');
373 | } finally { createGate.release(); }
374 | const saved = await json<SavedSearch>(await createdResponse, 201);
375 | expect(saved).toMatchObject({ name: 'Gjennomgått utviklersøk Æ Ø Å', query: 'utvikler', location: 'Tromsø', isActive: true, resultCount: 0, dismissedCount: 0 });
376 | await expect(savedGroup(page, saved)).toBeVisible();
377 | expect(await nativeList(page.request)).toEqual([saved]);
378 | await page.reload(); await openFilters(page, 'nb'); await expect(savedGroup(page, saved)).toBeVisible();
379 | for (const isActive of [false, true]) {
380 | const updatedResponse = response(page, 'PATCH', `${savedUrl}/${saved.id}`);
381 | await keyboardActivate(page, savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true }), 'Space');
382 | expect(await json<SavedSearch>(await updatedResponse)).toEqual({ ...saved, isActive });
383 | expect(await nativeList(page.request)).toEqual([{ ...saved, isActive }]);
384 | await page.reload();
385 | await openFilters(page, 'nb');
386 | await expect(savedGroup(page, saved).getByRole('switch', { name: t.jobDiscoveryAutomaticAlerts, exact: true })).toBeChecked({ checked: isActive });
387 | }
388 | // Keep a legitimate results control after deleting the saved row so the next Tab
389 | // has a concrete destination inside the stable region rather than browser chrome.
390 | await keyboardActivate(page, page.getByRole('button', { name: t.jobDiscoverySearchNav, exact: true }));
391 | await expect(article(page, currentJob)).toBeVisible();
392 | await openFilters(page, 'nb');
393 | const deleteButton = savedGroup(page, saved).getByRole('button', { name: t.adminUsersDelete, exact: true });
394 | await keyboardActivate(page, deleteButton);
395 | const dialog = page.getByRole('dialog').filter({ has: page.getByRole('heading', { name: t.jobDiscoveryDeleteSavedTitle, exact: true }) });
396 | await expect(dialog.getByRole('heading', { name: t.jobDiscoveryDeleteSavedTitle, exact: true })).toBeVisible();
397 | await expect(dialog.getByText(t.jobDiscoveryDeleteSavedConfirm.replace('{name}', saved.name), { exact: true })).toBeVisible();
398 | const cancel = dialog.getByRole('button', { name: t.cancel, exact: true });
> 399 | await expect(cancel).toBeFocused();
| ^ Error: expect(locator).toBeFocused() failed
400 | await page.keyboard.press('Shift+Tab');
401 | const messageRegion = dialog.getByRole('region', { name: await dialog.getByRole('heading').innerText(), exact: true });
402 | await expect(messageRegion).toBeFocused();
403 | await messageRegion.press('Shift+Tab');
404 | await expect(dialog.getByRole('button', { name: t.adminUsersDelete, exact: true })).toBeFocused();
405 | await page.keyboard.press('Tab'); await expect(messageRegion).toBeFocused();
406 | await page.keyboard.press('Tab'); await expect(cancel).toBeFocused();
407 | await page.keyboard.press('Enter');
408 | await expect(dialog).toHaveCount(0); await expect(deleteButton).toBeFocused();
409 | expect(count(session.requests, 'DELETE', `/api/job-discovery/saved-searches/${saved.id}`)).toBe(0);
410 | expect(await nativeList(page.request)).toEqual([saved]);
411 | await page.route(`${savedUrl}/${saved.id}`, async route => {
412 | if (route.request().method() === 'DELETE') { deletes++; await deleteGate.pending; }
413 | return route.continue();
414 | });
415 | const deletedResponse = response(page, 'DELETE', `${savedUrl}/${saved.id}`);
416 | try {
417 | await keyboardActivate(page, deleteButton);
418 | await keyboardActivate(page, dialog.getByRole('button', { name: t.adminUsersDelete, exact: true }));
419 | await expect.poll(() => deletes).toBe(1);
420 | await expect(dialog).toHaveCount(0);
421 | await locked(page, 'nb'); await page.keyboard.press('Enter'); expect(deletes).toBe(1);
422 | await capture(page, info, sample, 'native-delete-held');
423 | } finally { deleteGate.release(); }
424 | expect((await deletedResponse).status()).toBe(204);
425 | await expect(savedGroup(page, saved)).toHaveCount(0);
426 | await closeFilters(page, 'nb', true);
427 | await recordFocus(page, info, 'native-delete-control-removed');
428 | await expect(results(page, 'nb')).toBeFocused();
429 | await page.keyboard.press('Tab');
430 | await recordFocus(page, info, 'native-delete-next-tab');
431 | await expect(page.getByLabel(t.jobDiscoverySort, { exact: true })).toBeFocused();
432 | await expect(article(page, currentJob)).toBeVisible();
433 | expect(await nativeList(page.request)).toEqual([]);
434 | await page.reload(); await openFilters(page, 'nb'); await expect(page.getByText(t.jobDiscoveryNoSavedSearches, { exact: true })).toBeVisible();
435 | expect(creates).toBe(1); expect(deletes).toBe(1);
436 | });
437 |
438 | test('[P7-05 Discovery] contextual Dismiss preserves a row on real missing-result denial and qualifies intercepted UI recovery', async ({ page, session }, info) => {
439 | test.setTimeout(90_000);
440 | const t = translations.en; const sample = { ...desktop, theme: 'dark' as const, width: 390, height: 844 };
441 | const saved = await createSaved(page, session.owner, 'Recorded dismiss owner context');
442 | const job = vacancy('p7-dismiss-encoded Æ Ø Å');
443 | const movedFocusJob = vacancy('p7-dismiss-second-moved-focus');
444 | await page.route(`${savedUrl}/${saved.id}/run`, async route => route.fulfill({ json: await runPayload(page, saved, [job, movedFocusJob]) }));
445 | await enter(page, session.owner, sample);
446 | await keyboardActivate(page, savedGroup(page, saved).getByRole('button', { name: t.jobDiscoveryRunSaved, exact: true }));
447 | await expect(article(page, job)).toBeVisible();
448 | const dismissUrl = `${savedUrl}/${saved.id}/results/${encodeURIComponent(job.id)}/dismiss`;
449 | const deniedResponse = response(page, 'PATCH', dismissUrl);
450 | await keyboardActivate(page, article(page, job).getByRole('button', { name: t.jobDiscoveryDismiss, exact: true }));
451 | expect((await deniedResponse).status()).toBe(404);
452 | await expect(page.getByText(t.jobDiscoveryDismissFailed, { exact: true })).toBeVisible();
453 | await expect(article(page, job)).toBeVisible();
454 | await expect(page.getByRole('button', { name: t.retry, exact: true })).toHaveCount(0);
455 | expect(await nativeList(page.request)).toEqual([saved]);
456 | await capture(page, info, sample, 'dismiss-native-missing-denied');
457 | const gate = hold(); let starts = 0;
458 | await page.route(dismissUrl, async route => {
459 | expect(route.request().method()).toBe('PATCH'); expect(route.request().postDataJSON()).toEqual({ isDismissed: true });
460 | starts++; await gate.pending; return route.fulfill({ status: 204 });
461 | });
462 | try {
463 | await keyboardActivate(page, article(page, job).getByRole('button', { name: t.jobDiscoveryDismiss, exact: true }));
464 | await expect.poll(() => starts).toBe(1); await locked(page); await page.keyboard.press('Enter'); expect(starts).toBe(1);
465 | } finally { gate.release(); }
466 | await expect(article(page, job)).toHaveCount(0);
467 | await expect(page.getByText(t.jobDiscoveryDismissFailed, { exact: true })).toHaveCount(0);
468 | await recordFocus(page, info, 'dismiss-control-removed');
469 | await expect(results(page)).toBeFocused();
470 | await page.keyboard.press('Tab');
471 | await recordFocus(page, info, 'dismiss-next-tab');
472 | await expect(page.getByLabel(t.jobDiscoverySort, { exact: true })).toBeFocused();
473 | await expect(article(page, movedFocusJob)).toBeVisible();
474 | const movedGate = hold();
475 | const movedUrl = `${savedUrl}/${saved.id}/results/${encodeURIComponent(movedFocusJob.id)}/dismiss`;
476 | let movedStarts = 0;
477 | await page.route(movedUrl, async route => {
478 | expect(route.request().method()).toBe('PATCH'); expect(route.request().postDataJSON()).toEqual({ isDismissed: true });
479 | movedStarts++; await movedGate.pending; return route.fulfill({ status: 204 });
480 | });
481 | const headerControl = page.locator('header').getByRole('button', { name: 'Norsk', exact: true });
482 | try {
483 | await keyboardActivate(page, article(page, movedFocusJob).getByRole('button', { name: t.jobDiscoveryDismiss, exact: true }));
484 | await expect.poll(() => movedStarts).toBe(1); await locked(page);
485 | await expect(headerControl).toBeEnabled(); await headerControl.focus(); await expect(headerControl).toBeFocused();
486 | await recordFocus(page, info, 'held-dismiss-user-moved-focus');
487 | } finally { movedGate.release(); }
488 | await expect(article(page, movedFocusJob)).toHaveCount(0);
489 | await recordFocus(page, info, 'dismiss-complete-user-focus-preserved');
490 | await expect(headerControl).toBeFocused();
491 | expect(movedStarts).toBe(1);
492 | expect(await nativeList(page.request)).toEqual([saved]);
493 | expect(count(session.requests, 'GET', '/api/job-discovery/search')).toBe(0);
494 | const patches = session.requests.filter(request => request.path.endsWith('/dismiss'));
495 | expect(patches).toHaveLength(3);
496 | expect(patches.map(request => request.body)).toEqual([{ isDismissed: true }, { isDismissed: true }, { isDismissed: true }]);
497 | await capture(page, info, sample, 'dismiss-intercepted-ui-only');
498 | await page.reload();
499 | await openFilters(page);
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB