hermes-test 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hermes-test.js +39 -0
- package/dist/harness.bundle.js +615 -0
- package/index.d.ts +231 -0
- package/package.json +65 -0
- package/src/expect.ts +354 -0
- package/src/fetch.ts +195 -0
- package/src/harness.ts +382 -0
- package/src/hooks.ts +226 -0
- package/src/index.ts +129 -0
- package/src/mock.ts +145 -0
- package/src/polyfills.js +334 -0
- package/src/shims/async-storage.js +54 -0
- package/src/shims/react-i18next.js +20 -0
- package/src/shims/react-native-launch-arguments.js +8 -0
- package/src/shims/react-native.js +168 -0
- package/src/shims/react-redux.js +12 -0
- package/src/shims/react.js +16 -0
- package/src/shims/reduxjs-toolkit.js +11 -0
- package/src/shims/rtk-query.js +44 -0
- package/src/shims/tanstack-query.js +68 -0
- package/src/spy.ts +160 -0
- package/src/store.ts +114 -0
- package/src/timers.ts +141 -0
- package/store.d.ts +43 -0
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// renderHook, act, waitFor — React hook testing primitives
|
|
2
|
+
// Uses react-reconciler to run hooks in a minimal React tree.
|
|
3
|
+
// No dependency on react-test-renderer (deprecated in React 19).
|
|
4
|
+
//
|
|
5
|
+
// React and ReactReconciler are NOT bundled with the harness — they come from
|
|
6
|
+
// the user's project via esbuild. The harness expects them on globalThis.
|
|
7
|
+
|
|
8
|
+
function getReact(): typeof import('react') {
|
|
9
|
+
const R = (globalThis as any).__HT_React;
|
|
10
|
+
if (!R) throw new Error('React not available. Make sure react is installed in your project.');
|
|
11
|
+
return R;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
import Reconciler from 'react-reconciler';
|
|
15
|
+
import { DefaultEventPriority, NoEventPriority } from 'react-reconciler/constants';
|
|
16
|
+
|
|
17
|
+
// Based on mdjastrzebski/test-renderer — the universal-test-renderer for React 19
|
|
18
|
+
// https://github.com/mdjastrzebski/test-renderer
|
|
19
|
+
let currentUpdatePriority: number = NoEventPriority;
|
|
20
|
+
|
|
21
|
+
const hostConfig = {
|
|
22
|
+
supportsMutation: true,
|
|
23
|
+
supportsPersistence: false,
|
|
24
|
+
supportsHydration: false,
|
|
25
|
+
supportsMicrotasks: true,
|
|
26
|
+
isPrimaryRenderer: true,
|
|
27
|
+
warnsIfNotActing: true,
|
|
28
|
+
createInstance() { return { children: [] }; },
|
|
29
|
+
createTextInstance() { return {}; },
|
|
30
|
+
appendInitialChild(p: any, c: any) { p.children.push(c); },
|
|
31
|
+
appendChild(p: any, c: any) { p.children.push(c); },
|
|
32
|
+
appendChildToContainer(p: any, c: any) { p.children.push(c); },
|
|
33
|
+
removeChild(p: any, c: any) { const i = p.children.indexOf(c); if (i !== -1) p.children.splice(i, 1); },
|
|
34
|
+
removeChildFromContainer(p: any, c: any) { const i = p.children.indexOf(c); if (i !== -1) p.children.splice(i, 1); },
|
|
35
|
+
insertBefore(p: any, c: any, b: any) { const i = p.children.indexOf(b); p.children.splice(i, 0, c); },
|
|
36
|
+
insertInContainerBefore(p: any, c: any, b: any) { const i = p.children.indexOf(b); p.children.splice(i, 0, c); },
|
|
37
|
+
commitUpdate() {},
|
|
38
|
+
commitTextUpdate() {},
|
|
39
|
+
commitMount() {},
|
|
40
|
+
prepareForCommit() { return null; },
|
|
41
|
+
resetAfterCommit() {},
|
|
42
|
+
resetTextContent() {},
|
|
43
|
+
finalizeInitialChildren() { return false; },
|
|
44
|
+
shouldSetTextContent() { return false; },
|
|
45
|
+
getRootHostContext() { return null; },
|
|
46
|
+
getChildHostContext(ctx: any) { return ctx; },
|
|
47
|
+
getPublicInstance(inst: any) { return inst; },
|
|
48
|
+
prepareUpdate() { return {}; },
|
|
49
|
+
clearContainer(c: any) { c.children = []; },
|
|
50
|
+
scheduleTimeout: (globalThis as any).setTimeout || ((fn: any) => fn()),
|
|
51
|
+
cancelTimeout: (globalThis as any).clearTimeout || (() => {}),
|
|
52
|
+
noTimeout: -1,
|
|
53
|
+
scheduleMicrotask: typeof queueMicrotask === 'function' ? queueMicrotask : (fn: any) => Promise.resolve().then(fn),
|
|
54
|
+
getCurrentEventPriority() { return DefaultEventPriority; },
|
|
55
|
+
setCurrentUpdatePriority(priority: number) { currentUpdatePriority = priority; },
|
|
56
|
+
getCurrentUpdatePriority() { return currentUpdatePriority; },
|
|
57
|
+
resolveUpdatePriority() { return currentUpdatePriority || DefaultEventPriority; },
|
|
58
|
+
shouldAttemptEagerTransition() { return false; },
|
|
59
|
+
trackSchedulerEvent() {},
|
|
60
|
+
resolveEventType() { return ''; },
|
|
61
|
+
resolveEventTimeStamp() { return -1.1; },
|
|
62
|
+
requestPostPaintCallback() {},
|
|
63
|
+
maySuspendCommit() { return false; },
|
|
64
|
+
preloadInstance() { return true; },
|
|
65
|
+
startSuspendingCommit() {},
|
|
66
|
+
suspendInstance() {},
|
|
67
|
+
waitForCommitToBeReady() { return null; },
|
|
68
|
+
NotPendingTransition: null,
|
|
69
|
+
resetFormInstance() {},
|
|
70
|
+
hideInstance() {},
|
|
71
|
+
unhideInstance() {},
|
|
72
|
+
hideTextInstance() {},
|
|
73
|
+
unhideTextInstance() {},
|
|
74
|
+
getInstanceFromNode() { return null; },
|
|
75
|
+
prepareScopeUpdate() {},
|
|
76
|
+
getInstanceFromScope() { return null; },
|
|
77
|
+
detachDeletedInstance() {},
|
|
78
|
+
beforeActiveInstanceBlur() {},
|
|
79
|
+
afterActiveInstanceBlur() {},
|
|
80
|
+
preparePortalMount() {},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function createReconciler() {
|
|
84
|
+
const create = typeof Reconciler === 'function' ? Reconciler : (Reconciler as any).default;
|
|
85
|
+
return create(hostConfig);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type HookResult<T> = {
|
|
89
|
+
readonly result: { readonly current: T };
|
|
90
|
+
readonly current: T;
|
|
91
|
+
readonly history: ReadonlyArray<T>;
|
|
92
|
+
readonly renderCount: number;
|
|
93
|
+
rerender(props?: any): void;
|
|
94
|
+
unmount(): void;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const drain = (globalThis as any).__HT_drain || (() => {});
|
|
98
|
+
|
|
99
|
+
function flush() {
|
|
100
|
+
drain();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Enable React.act() support
|
|
104
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
105
|
+
|
|
106
|
+
export function act(fn: () => void | Promise<void>): void {
|
|
107
|
+
const React = getReact();
|
|
108
|
+
const reactAct = (React as any).act || (React as any).unstable_act;
|
|
109
|
+
if (!reactAct) {
|
|
110
|
+
fn();
|
|
111
|
+
flush();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
reactAct(() => {
|
|
116
|
+
const result = fn();
|
|
117
|
+
if (result && typeof (result as any).then === 'function') {
|
|
118
|
+
let settled = false;
|
|
119
|
+
let error: any;
|
|
120
|
+
(result as Promise<void>).then(
|
|
121
|
+
() => { settled = true; },
|
|
122
|
+
(e: any) => { settled = true; error = e; }
|
|
123
|
+
);
|
|
124
|
+
for (let i = 0; i < 50 && !settled; i++) {
|
|
125
|
+
drain();
|
|
126
|
+
}
|
|
127
|
+
if (error) throw error;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
flush();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function renderHook<T>(
|
|
134
|
+
hookFn: (props?: any) => T,
|
|
135
|
+
options?: { initialProps?: any; wrapper?: any }
|
|
136
|
+
): HookResult<T> {
|
|
137
|
+
const history: T[] = [];
|
|
138
|
+
let currentValue: T;
|
|
139
|
+
|
|
140
|
+
const React = getReact();
|
|
141
|
+
const reconciler = createReconciler();
|
|
142
|
+
|
|
143
|
+
const container = { children: [] };
|
|
144
|
+
const root = reconciler.createContainer(
|
|
145
|
+
container,
|
|
146
|
+
0, // LegacyRoot — effects fire synchronously in act()
|
|
147
|
+
null, // hydrationCallbacks
|
|
148
|
+
false, // isStrictMode
|
|
149
|
+
false, // concurrentUpdatesByDefaultOverride
|
|
150
|
+
'', // identifierPrefix
|
|
151
|
+
(err: any) => { throw err; }, // onUncaughtError
|
|
152
|
+
(err: any) => { throw err; }, // onCaughtError
|
|
153
|
+
null, // onRecoverableError
|
|
154
|
+
() => {}, // onDefaultTransitionIndicator
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
function TestComponent({ hookProps }: { hookProps: any }) {
|
|
158
|
+
const value = hookFn(hookProps);
|
|
159
|
+
currentValue = value;
|
|
160
|
+
history.push(value);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createTree(props?: any) {
|
|
165
|
+
const testEl = React.createElement(TestComponent, { hookProps: props });
|
|
166
|
+
if (options?.wrapper) {
|
|
167
|
+
return React.createElement(options.wrapper, null, testEl);
|
|
168
|
+
}
|
|
169
|
+
return testEl;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
act(() => {
|
|
173
|
+
reconciler.updateContainer(createTree(options?.initialProps), root, null, null);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
result: {
|
|
178
|
+
get current() {
|
|
179
|
+
return currentValue!;
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
get current() {
|
|
183
|
+
return currentValue!;
|
|
184
|
+
},
|
|
185
|
+
get history() {
|
|
186
|
+
return history;
|
|
187
|
+
},
|
|
188
|
+
get renderCount() {
|
|
189
|
+
return history.length;
|
|
190
|
+
},
|
|
191
|
+
rerender(props?: any) {
|
|
192
|
+
act(() => {
|
|
193
|
+
reconciler.updateContainer(createTree(props), root, null, null);
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
unmount() {
|
|
197
|
+
act(() => {
|
|
198
|
+
reconciler.updateContainer(null, root, null, null);
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function waitFor<T>(
|
|
205
|
+
predicate: () => T | false | null | undefined,
|
|
206
|
+
options?: { timeout?: number; interval?: number }
|
|
207
|
+
): T {
|
|
208
|
+
const timeout = options?.timeout ?? 1000;
|
|
209
|
+
const start = Date.now();
|
|
210
|
+
|
|
211
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
212
|
+
act(() => { drain(); });
|
|
213
|
+
drain();
|
|
214
|
+
|
|
215
|
+
const result = predicate();
|
|
216
|
+
if (result !== false && result !== null && result !== undefined) {
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (Date.now() - start >= timeout) {
|
|
221
|
+
throw new Error(`waitFor timed out after ${timeout}ms`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw new Error(`waitFor exceeded max attempts`);
|
|
226
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Thin re-export from the hermes-test harness runtime.
|
|
2
|
+
// The harness is eval'd before the user bundle, so globalThis.__HT is always available.
|
|
3
|
+
// Users import from 'hermes-test' instead of accessing globalThis directly.
|
|
4
|
+
|
|
5
|
+
const ht = (globalThis as any).__HT;
|
|
6
|
+
|
|
7
|
+
// --- Redux test store factories ---
|
|
8
|
+
// withStore: quick identity-reducer store for any state shape
|
|
9
|
+
// withApiStore: configurable RTK Query store factory
|
|
10
|
+
|
|
11
|
+
function _withTestActions(reducer: (state: any, action: any) => any) {
|
|
12
|
+
return (state: any, action: any) => {
|
|
13
|
+
if (action.type === '__SET_STATE__') return action.payload;
|
|
14
|
+
if (action.type === '__PATCH__') return { ...state, ...action.payload };
|
|
15
|
+
return reducer(state, action);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _makeCtx(store: any) {
|
|
20
|
+
const React = require('react');
|
|
21
|
+
const { Provider } = require('react-redux');
|
|
22
|
+
|
|
23
|
+
const wrapper = ({ children }: { children: any }) =>
|
|
24
|
+
React.createElement(Provider, { store } as any, children);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
store,
|
|
28
|
+
wrapper,
|
|
29
|
+
dispatch: store.dispatch.bind(store),
|
|
30
|
+
getState: store.getState.bind(store),
|
|
31
|
+
setState(state: Record<string, any>) { store.dispatch({ type: '__SET_STATE__', payload: state }); },
|
|
32
|
+
patchState(partial: Record<string, any>) { store.dispatch({ type: '__PATCH__', payload: partial }); },
|
|
33
|
+
renderHookWithReduxStore<T>(hookFn: (props?: any) => T, options?: { initialProps?: any }) {
|
|
34
|
+
return ht.renderHook(hookFn, { ...options, wrapper });
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function withStore(initialState: Record<string, any> = {}) {
|
|
40
|
+
const { configureStore } = require('@reduxjs/toolkit');
|
|
41
|
+
return _makeCtx(configureStore({
|
|
42
|
+
reducer: _withTestActions((s: any = initialState) => s),
|
|
43
|
+
preloadedState: initialState,
|
|
44
|
+
middleware: (gdm: any) => gdm({ serializableCheck: false, immutableCheck: false }),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function withAppReducer(
|
|
49
|
+
reducer: (state: any, action: any) => any,
|
|
50
|
+
preloadedState?: Record<string, any>,
|
|
51
|
+
) {
|
|
52
|
+
const { configureStore } = require('@reduxjs/toolkit');
|
|
53
|
+
return _makeCtx(configureStore({
|
|
54
|
+
reducer: _withTestActions(reducer),
|
|
55
|
+
preloadedState,
|
|
56
|
+
middleware: (gdm: any) => gdm({ serializableCheck: false, immutableCheck: false }),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface RtkQueryApi {
|
|
61
|
+
reducer: any;
|
|
62
|
+
middleware: any;
|
|
63
|
+
reducerPath: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface SetupApiStoreOptions {
|
|
67
|
+
middleware?: {
|
|
68
|
+
prepend?: any[];
|
|
69
|
+
concat?: any[];
|
|
70
|
+
};
|
|
71
|
+
preloadedState?: Record<string, any>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function setupApiStore(
|
|
75
|
+
apis: RtkQueryApi[],
|
|
76
|
+
extraReducers?: Record<string, any>,
|
|
77
|
+
options?: SetupApiStoreOptions,
|
|
78
|
+
) {
|
|
79
|
+
const { configureStore } = require('@reduxjs/toolkit');
|
|
80
|
+
|
|
81
|
+
const reducerMap: Record<string, any> = {};
|
|
82
|
+
for (const api of apis) {
|
|
83
|
+
reducerMap[api.reducerPath] = api.reducer;
|
|
84
|
+
}
|
|
85
|
+
if (extraReducers) Object.assign(reducerMap, extraReducers);
|
|
86
|
+
|
|
87
|
+
const store = configureStore({
|
|
88
|
+
reducer: reducerMap,
|
|
89
|
+
preloadedState: options?.preloadedState,
|
|
90
|
+
middleware: (gdm: any) => {
|
|
91
|
+
let chain = gdm({ serializableCheck: false, immutableCheck: false });
|
|
92
|
+
for (const a of apis) chain = chain.concat(a.middleware);
|
|
93
|
+
for (const mw of (options?.middleware?.concat ?? [])) chain = chain.concat(mw);
|
|
94
|
+
for (const mw of (options?.middleware?.prepend ?? [])) chain = chain.prepend(mw);
|
|
95
|
+
return chain;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { ..._makeCtx(store), apis };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const test = ht.test;
|
|
103
|
+
export const group = ht.group;
|
|
104
|
+
export const expect = ht.expect;
|
|
105
|
+
export const spy = ht.spy;
|
|
106
|
+
export const spyOn = ht.spyOn;
|
|
107
|
+
export const clearAllMocks = ht.clearAllMocks;
|
|
108
|
+
export const beforeEach = ht.beforeEach;
|
|
109
|
+
export const afterEach = ht.afterEach;
|
|
110
|
+
export const beforeAll = ht.beforeAll;
|
|
111
|
+
export const afterAll = ht.afterAll;
|
|
112
|
+
export const renderHook = ht.renderHook;
|
|
113
|
+
export const act = ht.act;
|
|
114
|
+
export const waitFor = ht.waitFor;
|
|
115
|
+
export const mockModule = ht.mockModule;
|
|
116
|
+
export const useMock = ht.useMock;
|
|
117
|
+
export const mockFetch = ht.mockFetch;
|
|
118
|
+
export const mockFetchUse = ht.mockFetchUse;
|
|
119
|
+
export const mockFetchReset = ht.mockFetchReset;
|
|
120
|
+
export const mockFetchClear = ht.mockFetchClear;
|
|
121
|
+
export const http = ht.http;
|
|
122
|
+
export const HttpResponse = ht.HttpResponse;
|
|
123
|
+
export const flushAsync = ht.flushAsync;
|
|
124
|
+
export const useFakeTimers = ht.useFakeTimers;
|
|
125
|
+
export const useRealTimers = ht.useRealTimers;
|
|
126
|
+
export const advanceTimersByTime = ht.advanceTimersByTime;
|
|
127
|
+
export const runAllTimers = ht.runAllTimers;
|
|
128
|
+
export const getTimerCount = ht.getTimerCount;
|
|
129
|
+
export const advanceTimersToNextTimer = ht.advanceTimersToNextTimer;
|
package/src/mock.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// useMock — patches module exports for test mocking
|
|
2
|
+
// Works by replacing the getter functions on ESM namespace objects
|
|
3
|
+
//
|
|
4
|
+
// mockModule — jest.mock() equivalent: registers factory in global __HT_mocks
|
|
5
|
+
// so that externalized modules resolve through our mock layer at require() time.
|
|
6
|
+
|
|
7
|
+
import { spy, type Spy } from './spy';
|
|
8
|
+
|
|
9
|
+
type SavedDescriptor = { target: any; key: string; desc: PropertyDescriptor };
|
|
10
|
+
let savedDescriptors: SavedDescriptor[] = [];
|
|
11
|
+
|
|
12
|
+
// --- mockModule: jest.mock() equivalent ---
|
|
13
|
+
// Registers a mock factory for a module path, scoped to the current test file.
|
|
14
|
+
// The bundler wraps mocked module exports in Proxies that check the per-file
|
|
15
|
+
// mock registry at access time. This allows multiple files to mock the same
|
|
16
|
+
// module with different implementations in a single bundle.
|
|
17
|
+
const mockRegistry: Record<string, Record<string, any>> = (globalThis as any).__HT_mocks || {};
|
|
18
|
+
(globalThis as any).__HT_mocks = mockRegistry;
|
|
19
|
+
|
|
20
|
+
// Per-file mock scoping: __HT_file_mocks[filename][modulePath] = mock
|
|
21
|
+
const fileMocks: Record<string, Record<string, any>> =
|
|
22
|
+
(globalThis as any).__HT_file_mocks || ((globalThis as any).__HT_file_mocks = {});
|
|
23
|
+
|
|
24
|
+
// Track patches applied by mockModule so they can be undone between files
|
|
25
|
+
let mockModulePatches: { target: any; key: string; original: any }[] = [];
|
|
26
|
+
|
|
27
|
+
export function mockModule(
|
|
28
|
+
modulePath: string,
|
|
29
|
+
factory: () => Record<string, any>
|
|
30
|
+
): void {
|
|
31
|
+
const impl = factory();
|
|
32
|
+
const value = typeof impl === 'function' ? impl : wrapWithSpies(impl);
|
|
33
|
+
|
|
34
|
+
// Register in per-file scope only — no global registry pollution
|
|
35
|
+
const currentFile = (globalThis as any).__currentTestFile || '__global__';
|
|
36
|
+
if (!fileMocks[currentFile]) fileMocks[currentFile] = {};
|
|
37
|
+
fileMocks[currentFile][modulePath] = value;
|
|
38
|
+
|
|
39
|
+
// Also patch the real module's properties so module-level destructuring picks up
|
|
40
|
+
// the mock. Example: `const { fn } = api.endpoints` captures at init time — the
|
|
41
|
+
// Proxy can't intercept it. But if we overwrite `api.endpoints.fn` on the real
|
|
42
|
+
// object, the destructured `fn` still points to the same object's property.
|
|
43
|
+
// This covers the case where a hook destructures `{ endpoints }` from a Proxy:
|
|
44
|
+
// const { method } = endpoints; // captured at init
|
|
45
|
+
// If we patch `endpoints.method = spy`, the captured `method` is updated.
|
|
46
|
+
//
|
|
47
|
+
// We look up the real module via __HT_mocks (for externalized) or by requiring
|
|
48
|
+
// through the shadow wrapper's _getReal().
|
|
49
|
+
const globalMock = mockRegistry[modulePath];
|
|
50
|
+
if (globalMock && typeof globalMock === 'object' && typeof value === 'object') {
|
|
51
|
+
for (const key of Object.keys(value)) {
|
|
52
|
+
if (key === 'default' || key === '__esModule') continue;
|
|
53
|
+
try {
|
|
54
|
+
if (key in globalMock) {
|
|
55
|
+
mockModulePatches.push({ target: globalMock, key, original: globalMock[key] });
|
|
56
|
+
globalMock[key] = value[key];
|
|
57
|
+
}
|
|
58
|
+
} catch { /* frozen or non-configurable */ }
|
|
59
|
+
}
|
|
60
|
+
// Also patch 'default' export if mock provides it
|
|
61
|
+
if ('default' in value && 'default' in globalMock) {
|
|
62
|
+
const mockDefault = value['default'];
|
|
63
|
+
const realDefault = globalMock['default'];
|
|
64
|
+
if (realDefault && typeof realDefault === 'object' && typeof mockDefault === 'object') {
|
|
65
|
+
for (const key of Object.keys(mockDefault)) {
|
|
66
|
+
try {
|
|
67
|
+
if (key in realDefault) {
|
|
68
|
+
mockModulePatches.push({ target: realDefault, key, original: realDefault[key] });
|
|
69
|
+
realDefault[key] = mockDefault[key];
|
|
70
|
+
}
|
|
71
|
+
} catch { /* frozen */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Called between test files to undo mockModule patches and mark
|
|
79
|
+
// source modules for re-initialization (so they re-read from Proxies
|
|
80
|
+
// with the new __currentTestFile).
|
|
81
|
+
export function resetMockModulePatches(): void {
|
|
82
|
+
for (const { target, key, original } of mockModulePatches) {
|
|
83
|
+
try { target[key] = original; } catch { /* best effort */ }
|
|
84
|
+
}
|
|
85
|
+
mockModulePatches = [];
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function wrapWithSpies<T extends Record<string, any>>(impl: T): T {
|
|
90
|
+
const wrapped: any = {};
|
|
91
|
+
for (const key of Object.keys(impl)) {
|
|
92
|
+
const value = impl[key];
|
|
93
|
+
if (typeof value === 'function' && !(value as any)._isSpy) {
|
|
94
|
+
wrapped[key] = spy(value);
|
|
95
|
+
} else {
|
|
96
|
+
wrapped[key] = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return wrapped;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function useMock<T extends Record<string, any>>(
|
|
103
|
+
moduleExports: T,
|
|
104
|
+
implementation: Partial<T>
|
|
105
|
+
): { [K in keyof T]: T[K] extends (...args: any[]) => any ? Spy<T[K]> : T[K] } {
|
|
106
|
+
const wrapped = wrapWithSpies(implementation as Record<string, any>);
|
|
107
|
+
|
|
108
|
+
for (const key of Object.keys(wrapped)) {
|
|
109
|
+
// Save original descriptor for reset
|
|
110
|
+
const desc = Object.getOwnPropertyDescriptor(moduleExports, key);
|
|
111
|
+
if (desc) {
|
|
112
|
+
savedDescriptors.push({ target: moduleExports, key, desc });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Replace with a getter that returns our mock
|
|
116
|
+
const mockValue = wrapped[key];
|
|
117
|
+
try {
|
|
118
|
+
Object.defineProperty(moduleExports, key, {
|
|
119
|
+
get: () => mockValue,
|
|
120
|
+
configurable: true,
|
|
121
|
+
enumerable: true,
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
// If defineProperty fails, try direct assignment as fallback
|
|
125
|
+
try {
|
|
126
|
+
(moduleExports as any)[key] = mockValue;
|
|
127
|
+
} catch {
|
|
128
|
+
// Module is fully frozen — warn but continue
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return wrapped as any;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function resetMocks(): void {
|
|
137
|
+
for (const { target, key, desc } of savedDescriptors) {
|
|
138
|
+
try {
|
|
139
|
+
Object.defineProperty(target, key, desc);
|
|
140
|
+
} catch {
|
|
141
|
+
// best effort
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
savedDescriptors = [];
|
|
145
|
+
}
|