test(discovery): reconcile native journeys with filter drawer
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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."}
|
||||
+1
@@ -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."}
|
||||
+239
@@ -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);
|
||||
```
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
Binary file not shown.
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+239
@@ -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);
|
||||
```
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
Reference in New Issue
Block a user