Files
jobtrackingapp/docs/ui-modernisation/evidence/discovery-native-drawer/pilot-focus-failure/error-context.md
T

239 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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);
```