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

17 KiB
Raw Blame History

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"

- button "Avbryt"

Test source

  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);