From 8dada6f1427207a553e7748659088f083806f539 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Wed, 5 Aug 2026 20:46:24 +0200 Subject: [PATCH 1/2] Mock Pragmatic lazily in the Stimulus specs The Angular unit-test builder bundles each spec into a __commonJS wrapper, so Vitest's hoist validator walks that wrapper instead of our source and reports top-level vi.mock calls as nested. Only mock, unmock and hoisted are hoist-checked, so vi.doMock sidesteps it. These specs already import the mocked packages and the controller under test lazily in beforeAll, which is the ordering doMock needs. A note above each call records that, since doMock's position above the imports no longer carries the guarantee vi.mock's hoisting gave it. The root spec also claimed it read back the combine, prevent-unhandled and drag-preview options. It never did; they keep the mounted item controller's side effects inert. --- .../dynamic/sortable-lists.controller.spec.ts | 19 ++++++++++--------- .../sortable-lists/item.controller.spec.ts | 10 ++++++---- .../sortable-lists/list.controller.spec.ts | 4 +++- .../scrollable.controller.spec.ts | 2 +- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.spec.ts b/frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.spec.ts index 5de5ccdc9fbd..7faa53bb682b 100644 --- a/frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.spec.ts +++ b/frontend/src/stimulus/controllers/dynamic/sortable-lists.controller.spec.ts @@ -26,31 +26,32 @@ // See COPYRIGHT and LICENSE files for more details. //++ -vi.mock('@atlaskit/pragmatic-drag-and-drop/element/adapter', () => ({ +// vi.doMock is not hoisted above imports, unlike vi.mock, so the subject +// below is imported dynamically further down, after these calls run. +vi.doMock('@atlaskit/pragmatic-drag-and-drop/element/adapter', () => ({ draggable: vi.fn(() => vi.fn()), dropTargetForElements: vi.fn(() => vi.fn()), monitorForElements: vi.fn(() => vi.fn()), })); -vi.mock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => ({ autoScrollForElements: vi.fn(() => vi.fn()), })); -// This spec mounts the real item controller, which pulls in these modules. -// Tests share one module registry (the runner does not isolate spec files), -// so importing the real versions here would leak into the item controller -// spec and break its spies. Mock them to keep the shared cache inert. -vi.mock('@atlaskit/pragmatic-drag-and-drop/combine', () => ({ +// This spec mounts the real item controller, which pulls in these three +// modules. Stub them so its Pragmatic side effects stay inert; nothing +// here reads them back. +vi.doMock('@atlaskit/pragmatic-drag-and-drop/combine', () => ({ combine: vi.fn((...cleanups:(() => void)[]) => vi.fn(() => { cleanups.forEach((cleanup) => cleanup()); })), })); -vi.mock('@atlaskit/pragmatic-drag-and-drop/prevent-unhandled', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop/prevent-unhandled', () => ({ preventUnhandled: { start: vi.fn(), stop: vi.fn() }, })); -vi.mock('@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview', () => ({ setCustomNativeDragPreview: vi.fn(), })); diff --git a/frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.spec.ts b/frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.spec.ts index c9019b173eb3..ccd45ecf37fb 100644 --- a/frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.spec.ts +++ b/frontend/src/stimulus/controllers/dynamic/sortable-lists/item.controller.spec.ts @@ -26,26 +26,28 @@ // See COPYRIGHT and LICENSE files for more details. //++ -vi.mock('@atlaskit/pragmatic-drag-and-drop/combine', () => ({ +// vi.doMock is not hoisted above imports, unlike vi.mock, so the subject +// below is imported dynamically further down, after these calls run. +vi.doMock('@atlaskit/pragmatic-drag-and-drop/combine', () => ({ combine: vi.fn((...cleanups:(() => void)[]) => vi.fn(() => { cleanups.forEach((cleanup) => cleanup()); })), })); -vi.mock('@atlaskit/pragmatic-drag-and-drop/element/adapter', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop/element/adapter', () => ({ draggable: vi.fn(() => vi.fn()), dropTargetForElements: vi.fn(() => vi.fn()), monitorForElements: vi.fn(() => vi.fn()), })); -vi.mock('@atlaskit/pragmatic-drag-and-drop/prevent-unhandled', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop/prevent-unhandled', () => ({ preventUnhandled: { start: vi.fn(), stop: vi.fn(), }, })); -vi.mock('@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview', () => ({ setCustomNativeDragPreview: vi.fn(), })); diff --git a/frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.spec.ts b/frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.spec.ts index e1eaa25599f0..5593dd5ee549 100644 --- a/frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.spec.ts +++ b/frontend/src/stimulus/controllers/dynamic/sortable-lists/list.controller.spec.ts @@ -26,7 +26,9 @@ // See COPYRIGHT and LICENSE files for more details. //++ -vi.mock('@atlaskit/pragmatic-drag-and-drop/element/adapter', () => ({ +// vi.doMock is not hoisted above imports, unlike vi.mock, so the subject +// below is imported dynamically further down, after these calls run. +vi.doMock('@atlaskit/pragmatic-drag-and-drop/element/adapter', () => ({ draggable: vi.fn(() => vi.fn()), dropTargetForElements: vi.fn(() => vi.fn()), monitorForElements: vi.fn(() => vi.fn()), diff --git a/frontend/src/stimulus/controllers/dynamic/sortable-lists/scrollable.controller.spec.ts b/frontend/src/stimulus/controllers/dynamic/sortable-lists/scrollable.controller.spec.ts index 8729e01054bc..3aee914532e2 100644 --- a/frontend/src/stimulus/controllers/dynamic/sortable-lists/scrollable.controller.spec.ts +++ b/frontend/src/stimulus/controllers/dynamic/sortable-lists/scrollable.controller.spec.ts @@ -31,7 +31,7 @@ import { setupStimulusTest, type StimulusTestContext } from 'core-stimulus/test- import type ScrollableControllerType from './scrollable.controller'; import type { sortableItemData as sortableItemDataFn, SortableListsRoot } from './drag-and-drop'; -vi.mock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => ({ autoScrollForElements: vi.fn(() => vi.fn()), })); From d5bd2789cc984385f98daea1390de3b7bfa73998 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Wed, 5 Aug 2026 20:46:31 +0200 Subject: [PATCH 2/2] Load the sortable engine after its mock vi.doMock only affects later imports, so the statically imported createSortableRoot would have pulled in the real auto-scroll module first and the mock would have stopped applying silently. A type import plus a lazy binding restores the ordering in both engine specs, and drops vi.hoisted with it. The preview spec also loses the file-split rationale that cited a shared module registry, which isolate:true removed. The split itself stands: the sibling renders previews for real. --- .../sortable-lists-engine.preview.spec.ts | 26 ++++++++------ .../sortable-lists-engine.spec.ts | 34 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/frontend/src/common/drag-and-drop/sortable-lists-engine.preview.spec.ts b/frontend/src/common/drag-and-drop/sortable-lists-engine.preview.spec.ts index 22ead4d7e03f..ce1b72862165 100644 --- a/frontend/src/common/drag-and-drop/sortable-lists-engine.preview.spec.ts +++ b/frontend/src/common/drag-and-drop/sortable-lists-engine.preview.spec.ts @@ -29,21 +29,21 @@ // The drag preview's `getOffset` decides where the pointer sits on the // preview, and Pragmatic only hands it to the real `setCustomNativeDragPreview` // — nothing observable from the outside. So this file mocks that module and -// reads the options back, which the sibling engine spec cannot do: it renders -// previews for real. Separate file rather than a mock added there, because the -// suite runs with `isolate: false` and shares one module registry. +// reads the options back. It stays a separate file from the sibling engine +// spec because that one renders previews for real, and one file cannot both +// stub and exercise the same module. import { vi } from 'vitest'; import { NativeDragSimulation } from 'core-common/drag-and-drop/testing/native-drag-simulation'; -import { createSortableRoot } from './sortable-lists-engine'; +import type { createSortableRoot as createSortableRootFn } from './sortable-lists-engine'; -const { previewCalls } = vi.hoisted(() => ({ - previewCalls: [] as { - getOffset?:(args:{ container:HTMLElement }) => { x:number; y:number }; - }[], -})); +// `doMock` is not hoisted, so this initialises before the factory below runs +// and a plain const does the job `vi.hoisted()` used to. +const previewCalls:{ + getOffset?:(args:{ container:HTMLElement }) => { x:number; y:number }; +}[] = []; -vi.mock('@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview', () => ({ setCustomNativeDragPreview: (options:{ getOffset?:(args:{ container:HTMLElement }) => { x:number; y:number }; }) => { @@ -51,9 +51,15 @@ vi.mock('@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-previe }, })); +let createSortableRoot:typeof createSortableRootFn; + describe('createSortableRoot drag preview offset', () => { let cleanupFns:(() => void)[] = []; + beforeAll(async () => { + ({ createSortableRoot } = await import('./sortable-lists-engine')); + }); + beforeEach(() => { previewCalls.length = 0; }); afterEach(() => { diff --git a/frontend/src/common/drag-and-drop/sortable-lists-engine.spec.ts b/frontend/src/common/drag-and-drop/sortable-lists-engine.spec.ts index f515b3b28b43..ed0ddcf6e350 100644 --- a/frontend/src/common/drag-and-drop/sortable-lists-engine.spec.ts +++ b/frontend/src/common/drag-and-drop/sortable-lists-engine.spec.ts @@ -32,23 +32,23 @@ import { centerOf, towardsEdgeOf, } from 'core-common/drag-and-drop/testing/native-drag-simulation'; -import { - createSortableRoot, - type SortableDropIntent, - type SortableDropTransaction, - type SortableSource, +import type { + createSortableRoot as createSortableRootFn, + SortableDropIntent, + SortableDropTransaction, + SortableSource, } from './sortable-lists-engine'; -const { autoScrollRegistrations } = vi.hoisted(() => ({ - autoScrollRegistrations: [] as { - element:Element; - getAllowedAxis:() => string; - cleanup:() => void; - cleaned:boolean; - }[], -})); +// `doMock` is not hoisted, so this initialises before the factory below runs +// and a plain const does the job `vi.hoisted()` used to. +const autoScrollRegistrations:{ + element:Element; + getAllowedAxis:() => string; + cleanup:() => void; + cleaned:boolean; +}[] = []; -vi.mock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => ({ +vi.doMock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => ({ autoScrollForElements: (args:{ element:Element; getAllowedAxis:() => string }) => { const entry = { element: args.element, @@ -62,6 +62,8 @@ vi.mock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => ({ }, })); +let createSortableRoot:typeof createSortableRootFn; + const liveRegistrations = () => autoScrollRegistrations.filter((r) => !r.cleaned); function buildList(items:string[]):{ root:HTMLElement; rows:HTMLElement[] } { @@ -145,6 +147,10 @@ function buildCardGrid(items:string[], columns:number):{ root:HTMLElement; cards describe('createSortableRoot', () => { let cleanupFns:(() => void)[] = []; + beforeAll(async () => { + ({ createSortableRoot } = await import('./sortable-lists-engine')); + }); + beforeEach(() => { autoScrollRegistrations.length = 0; }); afterEach(() => {