From 1f8fc26019f858a72ef8834e91af7fdc078a438a Mon Sep 17 00:00:00 2001 From: Connor Byrne Date: Mon, 11 May 2026 18:18:55 -0700 Subject: [PATCH] test(extension-api-v2): extract shared world mock factories to harness/worldMocks.ts F3: bc-05.v2, bc-05.migration, bc-11.v2, bc-11.migration each duplicated the same vi.mock('@/world/...') block (worldInstance + widgetComponents + entityIds + componentKey + 3 extension-api stubs). Centralise the factory bodies in src/extension-api-v2/__tests__/harness/worldMocks.ts and have the four files consume them. vi.hoisted handles stay inline because the hoisted factory runs before imports resolve; vi.mock factories wrap the imported helpers in arrows so the import binding is read lazily. Verified: 4/4 files pass under vitest run with the shared module. --- .../__tests__/bc-05.migration.test.ts | 67 +++++++------ .../__tests__/bc-05.v2.test.ts | 73 ++++++++------ .../__tests__/bc-11.migration.test.ts | 84 ++++++++++------ .../__tests__/bc-11.v2.test.ts | 77 ++++++++------- .../__tests__/harness/worldMocks.ts | 99 +++++++++++++++++++ 5 files changed, 278 insertions(+), 122 deletions(-) create mode 100644 src/extension-api-v2/__tests__/harness/worldMocks.ts diff --git a/src/extension-api-v2/__tests__/bc-05.migration.test.ts b/src/extension-api-v2/__tests__/bc-05.migration.test.ts index 83ea822184..08eee454fc 100644 --- a/src/extension-api-v2/__tests__/bc-05.migration.test.ts +++ b/src/extension-api-v2/__tests__/bc-05.migration.test.ts @@ -8,35 +8,34 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' // ── Mock world (same pattern as bc-01.migration.test.ts) ────────────────────── -const mockGetComponent = vi.fn() -const mockEntitiesWith = vi.fn(() => []) - -vi.mock('@/world/worldInstance', () => ({ - getWorld: () => ({ - getComponent: mockGetComponent, - entitiesWith: mockEntitiesWith, - setComponent: vi.fn(), - removeComponent: vi.fn() - }) +// vi.hoisted factory runs before imports — keep handle creation inline. +const { mockGetComponent, mockEntitiesWith } = vi.hoisted(() => ({ + mockGetComponent: vi.fn(), + mockEntitiesWith: vi.fn(() => [] as unknown[]) })) -vi.mock('@/world/widgets/widgetComponents', () => ({ - WidgetComponentContainer: Symbol('WidgetComponentContainer'), - WidgetComponentDisplay: Symbol('WidgetComponentDisplay'), - WidgetComponentSchema: Symbol('WidgetComponentSchema'), - WidgetComponentSerialize: Symbol('WidgetComponentSerialize'), - WidgetComponentValue: Symbol('WidgetComponentValue') -})) +import { + componentKeyMockFactory, + emptyMockFactory, + widgetComponentsMockFactory, + worldInstanceMockFactory +} from './harness/worldMocks' -vi.mock('@/world/entityIds', () => ({})) +// vi.mock factories are hoisted; keep imported helpers behind arrows so +// the import binding is read lazily at factory invocation time. +vi.mock('@/world/worldInstance', () => + worldInstanceMockFactory({ mockGetComponent, mockEntitiesWith }) +) -vi.mock('@/world/componentKey', () => ({ - defineComponentKey: (name: string) => ({ name }) -})) +vi.mock('@/world/widgets/widgetComponents', () => widgetComponentsMockFactory()) -vi.mock('@/extension-api/node', () => ({})) -vi.mock('@/extension-api/widget', () => ({})) -vi.mock('@/extension-api/lifecycle', () => ({})) +vi.mock('@/world/entityIds', () => emptyMockFactory()) + +vi.mock('@/world/componentKey', () => componentKeyMockFactory()) + +vi.mock('@/extension-api/node', () => emptyMockFactory()) +vi.mock('@/extension-api/widget', () => emptyMockFactory()) +vi.mock('@/extension-api/lifecycle', () => emptyMockFactory()) import { _clearExtensionsForTesting, @@ -111,7 +110,10 @@ function stubNodeType(id: NodeEntityId, comfyClass = 'TestNode') { function makeDiv(height = 120): HTMLElement { const el = document.createElement('div') - Object.defineProperty(el, 'offsetHeight', { value: height, configurable: true }) + Object.defineProperty(el, 'offsetHeight', { + value: height, + configurable: true + }) return el } @@ -172,14 +174,20 @@ describe('BC.05 migration — custom DOM widgets and node sizing', () => { // v1: getHeight callback const v1Node = createV1Node(2) - v1Node.addDOMWidget('widget', 'custom', el, { getHeight: () => reportedHeight }) + v1Node.addDOMWidget('widget', 'custom', el, { + getHeight: () => reportedHeight + }) const v1Height = v1Node.domWidgets[0].height // v2: explicit height option defineNodeExtension({ name: 'bc05.mig.height-parity', nodeCreated(handle) { - handle.addDOMWidget({ name: 'widget', element: el, height: reportedHeight }) + handle.addDOMWidget({ + name: 'widget', + element: el, + height: reportedHeight + }) } }) const id = makeNodeId(2) @@ -244,7 +252,10 @@ describe('BC.05 migration — custom DOM widgets and node sizing', () => { mountExtensionsForNode(id) const heightCmd = dispatchedCommands.find( - (c) => c.type === 'SetWidgetOption' && c.key === '__domHeight' && c.value === newHeight + (c) => + c.type === 'SetWidgetOption' && + c.key === '__domHeight' && + c.value === newHeight ) // v1 needed a computeSize override; v2 achieves the same via SetWidgetOption dispatch diff --git a/src/extension-api-v2/__tests__/bc-05.v2.test.ts b/src/extension-api-v2/__tests__/bc-05.v2.test.ts index 26393a6c71..3e554c5975 100644 --- a/src/extension-api-v2/__tests__/bc-05.v2.test.ts +++ b/src/extension-api-v2/__tests__/bc-05.v2.test.ts @@ -8,35 +8,34 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' // ── Mock world (same pattern as bc-01.v2.test.ts) ──────────────────────────── -const mockGetComponent = vi.fn() -const mockEntitiesWith = vi.fn(() => []) - -vi.mock('@/world/worldInstance', () => ({ - getWorld: () => ({ - getComponent: mockGetComponent, - entitiesWith: mockEntitiesWith, - setComponent: vi.fn(), - removeComponent: vi.fn() - }) +// vi.hoisted factory runs before imports — keep handle creation inline. +const { mockGetComponent, mockEntitiesWith } = vi.hoisted(() => ({ + mockGetComponent: vi.fn(), + mockEntitiesWith: vi.fn(() => [] as unknown[]) })) -vi.mock('@/world/widgets/widgetComponents', () => ({ - WidgetComponentContainer: Symbol('WidgetComponentContainer'), - WidgetComponentDisplay: Symbol('WidgetComponentDisplay'), - WidgetComponentSchema: Symbol('WidgetComponentSchema'), - WidgetComponentSerialize: Symbol('WidgetComponentSerialize'), - WidgetComponentValue: Symbol('WidgetComponentValue') -})) +import { + componentKeyMockFactory, + emptyMockFactory, + widgetComponentsMockFactory, + worldInstanceMockFactory +} from './harness/worldMocks' -vi.mock('@/world/entityIds', () => ({})) +// vi.mock factories are hoisted; keep imported helpers behind arrows so +// the import binding is read lazily at factory invocation time. +vi.mock('@/world/worldInstance', () => + worldInstanceMockFactory({ mockGetComponent, mockEntitiesWith }) +) -vi.mock('@/world/componentKey', () => ({ - defineComponentKey: (name: string) => ({ name }) -})) +vi.mock('@/world/widgets/widgetComponents', () => widgetComponentsMockFactory()) -vi.mock('@/extension-api/node', () => ({})) -vi.mock('@/extension-api/widget', () => ({})) -vi.mock('@/extension-api/lifecycle', () => ({})) +vi.mock('@/world/entityIds', () => emptyMockFactory()) + +vi.mock('@/world/componentKey', () => componentKeyMockFactory()) + +vi.mock('@/extension-api/node', () => emptyMockFactory()) +vi.mock('@/extension-api/widget', () => emptyMockFactory()) +vi.mock('@/extension-api/lifecycle', () => emptyMockFactory()) import { _clearExtensionsForTesting, @@ -63,7 +62,10 @@ function stubNodeType(id: NodeEntityId, comfyClass = 'TestNode') { function makeDiv(height = 120): HTMLElement { const el = document.createElement('div') - Object.defineProperty(el, 'offsetHeight', { value: height, configurable: true }) + Object.defineProperty(el, 'offsetHeight', { + value: height, + configurable: true + }) return el } @@ -123,7 +125,10 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { defineNodeExtension({ name: 'bc05.v2.handle-name', nodeCreated(handle) { - const wh = handle.addDOMWidget({ name: 'preview', element: makeDiv() }) + const wh = handle.addDOMWidget({ + name: 'preview', + element: makeDiv() + }) handleName = wh.name } }) @@ -163,7 +168,11 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { defineNodeExtension({ name: 'bc05.v2.custom-height', nodeCreated(handle) { - handle.addDOMWidget({ name: 'editor', element: el, height: customHeight }) + handle.addDOMWidget({ + name: 'editor', + element: el, + height: customHeight + }) } }) @@ -227,7 +236,10 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { defineNodeExtension({ name: 'bc05.v2.set-height', nodeCreated(handle) { - const wh = handle.addDOMWidget({ name: 'resizable', element: makeDiv(100) }) + const wh = handle.addDOMWidget({ + name: 'resizable', + element: makeDiv(100) + }) wh.setHeight(300) } }) @@ -237,7 +249,10 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { mountExtensionsForNode(id) const setCmd = dispatchedCommands.find( - (c) => c.type === 'SetWidgetOption' && c.key === '__domHeight' && c.value === 300 + (c) => + c.type === 'SetWidgetOption' && + c.key === '__domHeight' && + c.value === 300 ) expect(setCmd).toBeDefined() diff --git a/src/extension-api-v2/__tests__/bc-11.migration.test.ts b/src/extension-api-v2/__tests__/bc-11.migration.test.ts index cee16c3972..566a8f3629 100644 --- a/src/extension-api-v2/__tests__/bc-11.migration.test.ts +++ b/src/extension-api-v2/__tests__/bc-11.migration.test.ts @@ -8,35 +8,34 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' // ── Mock world (same pattern as bc-01.migration.test.ts) ────────────────────── -const mockGetComponent = vi.fn() -const mockEntitiesWith = vi.fn(() => []) - -vi.mock('@/world/worldInstance', () => ({ - getWorld: () => ({ - getComponent: mockGetComponent, - entitiesWith: mockEntitiesWith, - setComponent: vi.fn(), - removeComponent: vi.fn() - }) +// vi.hoisted factory runs before imports — keep handle creation inline. +const { mockGetComponent, mockEntitiesWith } = vi.hoisted(() => ({ + mockGetComponent: vi.fn(), + mockEntitiesWith: vi.fn(() => [] as unknown[]) })) -vi.mock('@/world/widgets/widgetComponents', () => ({ - WidgetComponentContainer: Symbol('WidgetComponentContainer'), - WidgetComponentDisplay: Symbol('WidgetComponentDisplay'), - WidgetComponentSchema: Symbol('WidgetComponentSchema'), - WidgetComponentSerialize: Symbol('WidgetComponentSerialize'), - WidgetComponentValue: Symbol('WidgetComponentValue') -})) +import { + componentKeyMockFactory, + emptyMockFactory, + widgetComponentsMockFactory, + worldInstanceMockFactory +} from './harness/worldMocks' -vi.mock('@/world/entityIds', () => ({})) +// vi.mock factories are hoisted; keep imported helpers behind arrows so +// the import binding is read lazily at factory invocation time. +vi.mock('@/world/worldInstance', () => + worldInstanceMockFactory({ mockGetComponent, mockEntitiesWith }) +) -vi.mock('@/world/componentKey', () => ({ - defineComponentKey: (name: string) => ({ name }) -})) +vi.mock('@/world/widgets/widgetComponents', () => widgetComponentsMockFactory()) -vi.mock('@/extension-api/node', () => ({})) -vi.mock('@/extension-api/widget', () => ({})) -vi.mock('@/extension-api/lifecycle', () => ({})) +vi.mock('@/world/entityIds', () => emptyMockFactory()) + +vi.mock('@/world/componentKey', () => componentKeyMockFactory()) + +vi.mock('@/extension-api/node', () => emptyMockFactory()) +vi.mock('@/extension-api/widget', () => emptyMockFactory()) +vi.mock('@/extension-api/lifecycle', () => emptyMockFactory()) import { _clearExtensionsForTesting, @@ -65,7 +64,11 @@ function createV1Widget(name: string, value: unknown): V1Widget { return { name, value, callback: undefined } } -function createV1ComboWidget(name: string, value: string, values: string[]): V1Widget { +function createV1ComboWidget( + name: string, + value: string, + values: string[] +): V1Widget { return { name, value, callback: undefined, options: { values } } } @@ -177,7 +180,10 @@ describe('BC.11 migration — widget imperative state writes', () => { const newValues = ['euler', 'dpm_2', 'lcm'] // v1: direct options mutation - const v1Widget = createV1ComboWidget('sampler', 'euler', ['euler', 'dpm_2']) + const v1Widget = createV1ComboWidget('sampler', 'euler', [ + 'euler', + 'dpm_2' + ]) v1Widget.options!.values = newValues expect(v1Widget.options!.values).toEqual(newValues) @@ -185,7 +191,9 @@ describe('BC.11 migration — widget imperative state writes', () => { defineNodeExtension({ name: 'bc11.mig.set-options', nodeCreated(handle) { - const wh = handle.addWidget('COMBO', 'sampler', 'euler', { values: ['euler', 'dpm_2'] }) + const wh = handle.addWidget('COMBO', 'sampler', 'euler', { + values: ['euler', 'dpm_2'] + }) wh.setOption('values', newValues) } }) @@ -203,8 +211,14 @@ describe('BC.11 migration — widget imperative state writes', () => { it('both v1 and v2 option-set operations are independent per widget', () => { // v1: two widgets, each with independent options mutation - const v1WidgetA = createV1ComboWidget('schedulerA', 'karras', ['karras', 'normal']) - const v1WidgetB = createV1ComboWidget('schedulerB', 'karras', ['karras', 'normal']) + const v1WidgetA = createV1ComboWidget('schedulerA', 'karras', [ + 'karras', + 'normal' + ]) + const v1WidgetB = createV1ComboWidget('schedulerB', 'karras', [ + 'karras', + 'normal' + ]) v1WidgetA.options!.values = ['karras', 'exponential'] // B is unaffected expect(v1WidgetB.options!.values).toEqual(['karras', 'normal']) @@ -214,8 +228,12 @@ describe('BC.11 migration — widget imperative state writes', () => { defineNodeExtension({ name: 'bc11.mig.option-independence', nodeCreated(handle) { - const whA = handle.addWidget('COMBO', 'schedulerA', 'karras', { values: ['karras', 'normal'] }) - handle.addWidget('COMBO', 'schedulerB', 'karras', { values: ['karras', 'normal'] }) + const whA = handle.addWidget('COMBO', 'schedulerA', 'karras', { + values: ['karras', 'normal'] + }) + handle.addWidget('COMBO', 'schedulerB', 'karras', { + values: ['karras', 'normal'] + }) whA.setOption('values', ['karras', 'exponential']) } }) @@ -223,7 +241,9 @@ describe('BC.11 migration — widget imperative state writes', () => { stubNodeType(id) mountExtensionsForNode(id) - const optCmds = dispatchedCommands.filter((c) => c.type === 'SetWidgetOption' && c.key === 'values') + const optCmds = dispatchedCommands.filter( + (c) => c.type === 'SetWidgetOption' && c.key === 'values' + ) // Only one setOption dispatch — for whA expect(optCmds).toHaveLength(1) }) diff --git a/src/extension-api-v2/__tests__/bc-11.v2.test.ts b/src/extension-api-v2/__tests__/bc-11.v2.test.ts index 9770b4845c..d7e8bdc2d6 100644 --- a/src/extension-api-v2/__tests__/bc-11.v2.test.ts +++ b/src/extension-api-v2/__tests__/bc-11.v2.test.ts @@ -8,35 +8,34 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' // ── Mock world (same pattern as bc-01.v2.test.ts) ──────────────────────────── -const mockGetComponent = vi.fn() -const mockEntitiesWith = vi.fn(() => []) - -vi.mock('@/world/worldInstance', () => ({ - getWorld: () => ({ - getComponent: mockGetComponent, - entitiesWith: mockEntitiesWith, - setComponent: vi.fn(), - removeComponent: vi.fn() - }) +// vi.hoisted factory runs before imports — keep handle creation inline. +const { mockGetComponent, mockEntitiesWith } = vi.hoisted(() => ({ + mockGetComponent: vi.fn(), + mockEntitiesWith: vi.fn(() => [] as unknown[]) })) -vi.mock('@/world/widgets/widgetComponents', () => ({ - WidgetComponentContainer: Symbol('WidgetComponentContainer'), - WidgetComponentDisplay: Symbol('WidgetComponentDisplay'), - WidgetComponentSchema: Symbol('WidgetComponentSchema'), - WidgetComponentSerialize: Symbol('WidgetComponentSerialize'), - WidgetComponentValue: Symbol('WidgetComponentValue') -})) +import { + componentKeyMockFactory, + emptyMockFactory, + widgetComponentsMockFactory, + worldInstanceMockFactory +} from './harness/worldMocks' -vi.mock('@/world/entityIds', () => ({})) +// vi.mock factories are hoisted; keep imported helpers behind arrows so +// the import binding is read lazily at factory invocation time. +vi.mock('@/world/worldInstance', () => + worldInstanceMockFactory({ mockGetComponent, mockEntitiesWith }) +) -vi.mock('@/world/componentKey', () => ({ - defineComponentKey: (name: string) => ({ name }) -})) +vi.mock('@/world/widgets/widgetComponents', () => widgetComponentsMockFactory()) -vi.mock('@/extension-api/node', () => ({})) -vi.mock('@/extension-api/widget', () => ({})) -vi.mock('@/extension-api/lifecycle', () => ({})) +vi.mock('@/world/entityIds', () => emptyMockFactory()) + +vi.mock('@/world/componentKey', () => componentKeyMockFactory()) + +vi.mock('@/extension-api/node', () => emptyMockFactory()) +vi.mock('@/extension-api/widget', () => emptyMockFactory()) +vi.mock('@/extension-api/lifecycle', () => emptyMockFactory()) import { _clearExtensionsForTesting, @@ -127,9 +126,9 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { stubNodeType(id) mountExtensionsForNode(id) - const setCmd = dispatchedCommands.find((c) => c.type === 'SetWidgetValue') as - | { widgetId: string; value: unknown } - | undefined + const setCmd = dispatchedCommands.find( + (c) => c.type === 'SetWidgetValue' + ) as { widgetId: string; value: unknown } | undefined expect(setCmd).toBeDefined() expect(setCmd?.widgetId).toBe(capturedWidgetId[0]) @@ -151,7 +150,9 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { stubNodeType(id) mountExtensionsForNode(id) - const setCmds = dispatchedCommands.filter((c) => c.type === 'SetWidgetValue') + const setCmds = dispatchedCommands.filter( + (c) => c.type === 'SetWidgetValue' + ) expect(setCmds).toHaveLength(3) expect(setCmds.map((c) => c.value)).toEqual([1, 2, 3]) }) @@ -172,7 +173,8 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { mountExtensionsForNode(id) const cmd = dispatchedCommands.find( - (c) => c.type === 'SetWidgetOption' && c.key === 'hidden' && c.value === true + (c) => + c.type === 'SetWidgetOption' && c.key === 'hidden' && c.value === true ) expect(cmd).toBeDefined() }) @@ -191,7 +193,10 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { mountExtensionsForNode(id) const cmd = dispatchedCommands.find( - (c) => c.type === 'SetWidgetOption' && c.key === 'disabled' && c.value === true + (c) => + c.type === 'SetWidgetOption' && + c.key === 'disabled' && + c.value === true ) expect(cmd).toBeDefined() }) @@ -202,7 +207,9 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { defineNodeExtension({ name: 'bc11.v2.set-option', nodeCreated(handle) { - const wh = handle.addWidget('COMBO', 'sampler_name', 'euler', { values: ['euler', 'dpm_2'] }) + const wh = handle.addWidget('COMBO', 'sampler_name', 'euler', { + values: ['euler', 'dpm_2'] + }) wh.setOption('values', ['euler', 'dpm_2', 'lcm']) } }) @@ -233,7 +240,9 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { stubNodeType(id) mountExtensionsForNode(id) - const optCmds = dispatchedCommands.filter((c) => c.type === 'SetWidgetOption') + const optCmds = dispatchedCommands.filter( + (c) => c.type === 'SetWidgetOption' + ) const keys = optCmds.map((c) => c.key) expect(keys).toContain('placeholder') expect(keys).toContain('maxLength') @@ -276,7 +285,9 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { stubNodeType(id) mountExtensionsForNode(id) - const createCmds = dispatchedCommands.filter((c) => c.type === 'CreateWidget') + const createCmds = dispatchedCommands.filter( + (c) => c.type === 'CreateWidget' + ) const names = createCmds.map((c) => c.name) expect(names).toContain('steps') expect(names).toContain('cfg') diff --git a/src/extension-api-v2/__tests__/harness/worldMocks.ts b/src/extension-api-v2/__tests__/harness/worldMocks.ts new file mode 100644 index 0000000000..539c37c00e --- /dev/null +++ b/src/extension-api-v2/__tests__/harness/worldMocks.ts @@ -0,0 +1,99 @@ +/** + * Shared `vi.mock(...)` payloads for tests that exercise + * `@/services/extension-api-service` against a stubbed World. + * + * Why this exists: BC.05 / BC.11 (and any future ECS-touching BC tests) + * had identical, copy-pasted blocks of: + * + * const mockGetComponent = vi.fn() + * const mockEntitiesWith = vi.fn(() => []) + * vi.mock('@/world/worldInstance', ...) + * vi.mock('@/world/widgets/widgetComponents', ...) + * vi.mock('@/world/entityIds', () => ({})) + * vi.mock('@/world/componentKey', ...) + * vi.mock('@/extension-api/node', () => ({})) + * vi.mock('@/extension-api/widget', () => ({})) + * vi.mock('@/extension-api/lifecycle', () => ({})) + * + * `vi.mock` is statically hoisted, so the *call sites* must remain in + * the consumer file. What we centralise here is the factory *bodies* + * plus a handle-creation helper that pairs cleanly with `vi.hoisted`. + * + * @example + * import { vi } from 'vitest' + * import { + * createWorldMockHandles, + * emptyMockFactory, + * componentKeyMockFactory, + * widgetComponentsMockFactory, + * worldInstanceMockFactory + * } from './harness/worldMocks' + * + * const { mockGetComponent, mockEntitiesWith } = vi.hoisted( + * createWorldMockHandles + * ) + * + * vi.mock('@/world/worldInstance', () => + * worldInstanceMockFactory({ mockGetComponent, mockEntitiesWith }) + * ) + * vi.mock('@/world/widgets/widgetComponents', widgetComponentsMockFactory) + * vi.mock('@/world/entityIds', emptyMockFactory) + * vi.mock('@/world/componentKey', componentKeyMockFactory) + * vi.mock('@/extension-api/node', emptyMockFactory) + * vi.mock('@/extension-api/widget', emptyMockFactory) + * vi.mock('@/extension-api/lifecycle', emptyMockFactory) + */ +import { vi } from 'vitest' + +export interface WorldMockHandles { + mockGetComponent: ReturnType + mockEntitiesWith: ReturnType +} + +/** + * Hoist-safe factory for the per-test mock function handles. + * Wrap with `vi.hoisted(createWorldMockHandles)` at the top of the + * test file so the resulting handles are available inside the + * `vi.mock(...)` factory closures. + */ +export function createWorldMockHandles(): WorldMockHandles { + return { + mockGetComponent: vi.fn(), + mockEntitiesWith: vi.fn(() => []) + } +} + +/** Factory body for `@/world/worldInstance`. */ +export function worldInstanceMockFactory(handles: WorldMockHandles) { + return { + getWorld: () => ({ + getComponent: handles.mockGetComponent, + entitiesWith: handles.mockEntitiesWith, + setComponent: vi.fn(), + removeComponent: vi.fn() + }) + } +} + +/** Factory body for `@/world/widgets/widgetComponents`. */ +export function widgetComponentsMockFactory() { + return { + WidgetComponentContainer: Symbol('WidgetComponentContainer'), + WidgetComponentDisplay: Symbol('WidgetComponentDisplay'), + WidgetComponentSchema: Symbol('WidgetComponentSchema'), + WidgetComponentSerialize: Symbol('WidgetComponentSerialize'), + WidgetComponentValue: Symbol('WidgetComponentValue') + } +} + +/** Factory body for `@/world/componentKey`. */ +export function componentKeyMockFactory() { + return { + defineComponentKey: (name: string) => ({ name }) + } +} + +/** Factory body for modules that need to be mocked but contribute nothing. */ +export function emptyMockFactory() { + return {} +}