@@ -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