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