Skip to content

Commit e313fdc

Browse files
authored
Merge pull request #1560 from microting/fix/b1m-multishift-accept-planned-flake
fix(playwright): stabilize b1m acceptPlanned editing-policy round-trip
2 parents 9435dcb + 5eebfb8 commit e313fdc

1 file changed

Lines changed: 58 additions & 14 deletions

File tree

eform-client/playwright/e2e/plugins/time-planning-pn/b1m/dashboard-edit-multishift.spec.ts

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -173,53 +173,97 @@ test.describe('Dashboard — multi-shift (3-5) round-trip regression guard (b1m,
173173
* Same editing-policy regression guard as `b/`: this test has no time
174174
* inputs so the flag-on context is incidental; included here so the b1m
175175
* multishift spec mirrors the legacy file structure.
176+
*
177+
* Stabilization (FU-E) — same family of fixes as FU-D applied to `b/`:
178+
* `#firstColumn3` triggers `getAssignedSite()` and only then opens the
179+
* dialog. On slow CI the dialog can become visible before the GET
180+
* commits, so the gated `*ngIf` content (editing-policy radios bound to
181+
* `data.entryMethod`) is rendered with a stale form snapshot. The
182+
* post-save reopen is the same race surface — the freshly-saved
183+
* `acceptPlanned` value only binds after the second GET commits, so the
184+
* default 5s `toBeChecked` retry can race the bind. Repro: stable run
185+
* 25302103911 attempt 1 — slug `b-3e2c2-n-acceptPlanned-is-selected` —
186+
* `expect(locator).toBeChecked failed — element(s) not found` on the
187+
* post-reopen radio at line ~217. Attempt 2 passed.
188+
*
189+
* Fix is timing-only: gate every `#firstColumn3` click on the assigned-
190+
* sites GET, swap `locator.waitFor({attached})` for
191+
* `expect().toBeAttached()`, and bump per-assertion timeouts on the
192+
* round-trip checks so reactive bindings have room to hydrate.
176193
*/
177194
test('editing-policy stays visible and persists when acceptPlanned is selected', async ({ page }) => {
178195
await page.locator('mat-nested-tree-node').filter({ hasText: 'Timeregistrering' }).click();
179-
const indexPromise = page.waitForResponse(r =>
180-
r.url().includes('/api/time-planning-pn/plannings/index') && r.request().method() === 'POST');
196+
const indexPromise = page.waitForResponse(
197+
r => r.url().includes('/api/time-planning-pn/plannings/index') && r.request().method() === 'POST',
198+
{ timeout: 30000 });
181199
await page.locator('mat-tree-node').filter({ hasText: 'Dashboard' }).click();
182200
await indexPromise;
183201
await waitForSpinner(page);
184202

203+
// Await the GET that hydrates the dialog model BEFORE the dialog even
204+
// opens. `onFirstColumnClick` fires getAssignedSite() and only then
205+
// calls dialog.open(...), so this response gates whether the *ngIf-
206+
// gated editing-policy radios bind to the persisted entry-method
207+
// value. Without this gate the dialog can become visible before the
208+
// GET commits and the radios render against a stale snapshot.
209+
const getAssignedSitePromise = page.waitForResponse(
210+
r => r.url().includes('/api/time-planning-pn/settings/assigned-sites')
211+
&& r.url().includes('siteId=')
212+
&& r.request().method() === 'GET',
213+
{ timeout: 30000 });
185214
await page.locator('#firstColumn3').click();
186-
await expect(page.locator('mat-dialog-container')).toBeVisible({ timeout: 10000 });
215+
await getAssignedSitePromise;
216+
await expect(page.locator('mat-dialog-container')).toBeVisible({ timeout: 30000 });
187217

188218
const personalCb = page.locator('#allowPersonalTimeRegistration input[type="checkbox"]');
189-
await personalCb.waitFor({ state: 'attached', timeout: 10000 });
219+
// expect.toBeAttached() retries continuously and emits a richer error
220+
// log than locator.waitFor() — same observable contract, better flake
221+
// diagnostics.
222+
await expect(personalCb).toBeAttached({ timeout: 30000 });
190223
if (!(await personalCb.isChecked())) {
191224
await page.locator('#allowPersonalTimeRegistration').click({ force: true });
192225
}
193-
await expect(personalCb).toBeChecked();
226+
await expect(personalCb).toBeChecked({ timeout: 10000 });
194227

195228
const acceptPlannedRadio = page.locator('mat-radio-button[value="acceptPlanned"]');
196229
await acceptPlannedRadio.scrollIntoViewIfNeeded();
197230
await acceptPlannedRadio.locator('label').first().click({ force: true });
198231

199-
await expect(page.locator('mat-radio-button[value="untilPayroll"]')).toBeVisible({ timeout: 5000 });
200-
await expect(page.locator('mat-radio-button[value="twoDaysRolling"]')).toBeVisible();
232+
await expect(page.locator('mat-radio-button[value="untilPayroll"]')).toBeVisible({ timeout: 15000 });
233+
await expect(page.locator('mat-radio-button[value="twoDaysRolling"]')).toBeVisible({ timeout: 15000 });
201234

202235
await page.locator('mat-radio-button[value="untilPayroll"]').locator('label').first()
203236
.click({ force: true });
204237

205238
const assignSitePromise = page.waitForResponse(
206-
r => r.url().includes('/api/time-planning-pn/settings/assigned-site') && r.request().method() === 'PUT');
239+
r => r.url().includes('/api/time-planning-pn/settings/assigned-site') && r.request().method() === 'PUT',
240+
{ timeout: 30000 });
207241
await page.locator('#saveButton').click({ force: true });
208242
await assignSitePromise;
209243
await waitForSpinner(page);
210-
await expect(page.locator('mat-dialog-container')).toHaveCount(0, { timeout: 10000 });
211-
244+
await expect(page.locator('mat-dialog-container')).toHaveCount(0, { timeout: 15000 });
245+
246+
// Re-open the dialog and assert both choices round-tripped. Wait for
247+
// the freshly-fetched assigned-site GET so the radios bind to the
248+
// persisted values before we assert (prior failure: line ~217
249+
// `toBeChecked` saw the pre-bind radio in a 5s window).
250+
const getAssignedSitePromise2 = page.waitForResponse(
251+
r => r.url().includes('/api/time-planning-pn/settings/assigned-sites')
252+
&& r.url().includes('siteId=')
253+
&& r.request().method() === 'GET',
254+
{ timeout: 30000 });
212255
await page.locator('#firstColumn3').click();
213-
await expect(page.locator('mat-dialog-container')).toBeVisible({ timeout: 10000 });
256+
await getAssignedSitePromise2;
257+
await expect(page.locator('mat-dialog-container')).toBeVisible({ timeout: 30000 });
214258

215259
await expect(
216260
page.locator('mat-radio-button[value="acceptPlanned"] input[type="radio"]'),
217-
).toBeChecked();
261+
).toBeChecked({ timeout: 15000 });
218262

219-
await expect(page.locator('mat-radio-button[value="untilPayroll"]')).toBeVisible();
263+
await expect(page.locator('mat-radio-button[value="untilPayroll"]')).toBeVisible({ timeout: 15000 });
220264
await expect(
221265
page.locator('mat-radio-button[value="untilPayroll"] input[type="radio"]'),
222-
).toBeChecked();
266+
).toBeChecked({ timeout: 15000 });
223267

224268
await page.locator('#cancelButton').click();
225269
});

0 commit comments

Comments
 (0)