From 8b0517047a71524bf9340dc5c3790fa156fe2a6f 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 76c264e3aa49..2ae29b583f04 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 8bc94713becc..1d7b0832c14f 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 018b9a38d8f4..5a4fed5dcc2e 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 09bc7bcb20d9..11645e71829e 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 90c3b501862c652fba903fe0e3a2ffb0b37ea24b 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(() => {