From 59a7bd03b2a3b21ff0fb2cdec49f447d8a96f48a Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sat, 30 May 2026 14:14:52 +0200 Subject: [PATCH] test(sections): de-flake intra-section drag reorder e2e The "reorders tasks within a section via drag and drop" test intermittently failed with "drag source/target has no bounding box" (e.g. CI run 26681989260). Each CDK drop re-creates the moved task as a fresh @for :enter, replaying the expandInOnly animation (height: 0 -> *), so its layout box collapses to zero height mid-gesture and boundingBox() returns null. Disable animations for this test via the app's own isDisableAnimations config (toggles @HostBinding('@.disabled')), which removes the height:0 window. Verified 5/5 green locally. Scoped to the reorder test only; the cross-list "drops a task into a section" test keeps animations on, since disabling them makes the empty drop target reflow instantly and miss. --- e2e/tests/work-view/sections.spec.ts | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/e2e/tests/work-view/sections.spec.ts b/e2e/tests/work-view/sections.spec.ts index 7b2593440f..13724b3a8a 100644 --- a/e2e/tests/work-view/sections.spec.ts +++ b/e2e/tests/work-view/sections.spec.ts @@ -129,6 +129,60 @@ test.describe('Sections', () => { await page.mouse.up(); }; + /** + * Disable Angular animations for the current session before driving CDK + * drag gestures that reorder tasks *already inside* a section. Each drop + * re-creates the moved task as a fresh `@for` `:enter`, so it replays the + * `expandInOnly` animation (`height: 0 -> *`, expand.ani.ts) and has a + * zero-height layout box mid-animation. `boundingBox()` returns `null` for + * that, which is the root cause of the "drag source/target has no bounding + * box" flake (and why the cross-section tests below are fixme'd). + * + * Flipping the app's own `isDisableAnimations` config toggles the + * `@HostBinding('@.disabled')` on the root component (app.component.ts), + * which disables every `@trigger` including `expandInOnly` — so dropped + * tasks render at full height instantly and their box never collapses. + * + * NOTE: only the intra-section reorder test uses this. The "drops a task + * into a section" test intentionally keeps animations on: it drags out of + * the no-section list into an *empty* section, and with animations off the + * source-removal reflow is instant, so the empty drop target can jump out + * from under the in-flight pointer and the drop misses. + */ + const disableAnimations = async ( + page: import('@playwright/test').Page, + ): Promise => { + await expect + .poll( + () => + page.evaluate(() => { + const store = ( + window as unknown as { + __e2eTestHelpers?: { store?: { dispatch: (a: unknown) => void } }; + } + ).__e2eTestHelpers?.store; + if (!store) return false; + store.dispatch({ + // updateGlobalConfigSection + // (src/app/features/config/store/global-config.actions.ts) + type: '[Global Config] Update Global Config Section', + sectionKey: 'misc', + sectionCfg: { isDisableAnimations: true }, + isSkipSnack: true, + }); + return true; + }), + { timeout: 10000 }, + ) + .toBe(true); + // `global-theme.service` mirrors the config onto a body class via an + // `effect()`. Waiting for it confirms the `@.disabled` binding has flipped + // before the first gesture reads layout. + await page + .locator('body.isDisableAnimations') + .waitFor({ state: 'attached', timeout: 5000 }); + }; + test('creates a section via the project context menu', async ({ page, workViewPage, @@ -286,6 +340,7 @@ test.describe('Sections', () => { projectPage, }) => { await setupTestProject(workViewPage, projectPage); + await disableAnimations(page); await openProjectContextMenu(page); await clickAddSection(page);