239 lines
17 KiB
Markdown
239 lines
17 KiB
Markdown
# 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);
|
||
``` |