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.
This commit is contained in:
Johannes Millan 2026-05-30 14:14:52 +02:00
parent 24725c9360
commit 59a7bd03b2

View file

@ -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<void> => {
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);