@unbound-app/api 0.11.0 → 1.1.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/ambient/image-modules.d.ts +58 -0
- package/package.json +27 -4
- package/src/_internal.d.ts +2771 -139
- package/src/{utils.d.ts → _types.d.ts} +10 -0
- package/src/assets.d.ts +64 -26
- package/src/builtins.d.ts +17 -0
- package/src/commands.d.ts +101 -25
- package/src/dialogs.d.ts +90 -25
- package/src/flux.d.ts +317 -0
- package/src/fonts.d.ts +51 -0
- package/src/fs.d.ts +15 -24
- package/src/global.d.ts +173 -11
- package/src/i18n.d.ts +33 -31
- package/src/iconPacks.d.ts +43 -0
- package/src/index.d.ts +14 -0
- package/src/marketplace.d.ts +50 -0
- package/src/metro/api.d.ts +8 -0
- package/src/metro/common.d.ts +34 -5
- package/src/metro/components.d.ts +34 -25
- package/src/metro/filters.d.ts +31 -16
- package/src/metro/index.d.ts +26079 -83
- package/src/metro/stores.d.ts +5 -0
- package/src/native.d.ts +77 -5
- package/src/patcher.d.ts +214 -57
- package/src/plugins.d.ts +55 -0
- package/src/settings.d.ts +8 -16
- package/src/shims.d.ts +5 -0
- package/src/storage.d.ts +80 -224
- package/src/sync.d.ts +51 -0
- package/src/themes.d.ts +43 -0
- package/src/toasts.d.ts +62 -9
- package/src/ui.d.ts +650 -0
- package/src/updates.d.ts +25 -0
- package/src/utils/index.d.ts +53 -0
- package/src/utils/react/index.d.ts +82 -0
- package/testing/fake-api.ts +256 -0
- package/testing/fake-components.ts +183 -0
- package/testing/fake-feedback.ts +258 -0
- package/testing/fake-fs.ts +54 -0
- package/testing/fake-java.ts +513 -0
- package/testing/fake-locales.ts +85 -0
- package/testing/fake-metro.ts +346 -0
- package/testing/fake-native-bridge.ts +98 -0
- package/testing/fake-native.ts +211 -0
- package/testing/fake-objc.ts +388 -0
- package/testing/fake-plugins.ts +176 -0
- package/testing/fake-react-native-modules.ts +362 -0
- package/testing/fake-react-native.ts +180 -0
- package/testing/fake-react.ts +54 -0
- package/testing/fake-settings-panel.ts +173 -0
- package/testing/fake-settings.ts +428 -0
- package/testing/fake-timers.ts +74 -0
- package/testing/fake-utils.ts +216 -0
- package/testing/icu-format.ts +132 -0
- package/testing/icu-parse.ts +277 -0
- package/testing/index.ts +42 -0
- package/testing/install-addon-globals.ts +260 -0
- package/testing/listener-registry.ts +39 -0
- package/testing/memory-storage.ts +149 -0
- package/testing/plugin-files.ts +155 -0
- package/testing/plugin-lifecycle.ts +149 -0
- package/testing/plugin-types.ts +34 -0
- package/testing/preset.ts +263 -0
- package/testing/react-patcher.ts +154 -0
- package/testing/react-value.ts +3 -0
- package/testing/recorders.ts +183 -0
- package/testing/recording-patcher.ts +173 -0
- package/testing/render.ts +84 -0
- package/testing/session.ts +77 -0
- package/testing/test-node.ts +163 -0
- package/testing/test-renderer.ts +753 -0
- package/testing/unfrozen-elements.ts +49 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { AnyObject } from '../src/patcher';
|
|
4
|
+
import type { ReactValue } from './react-value';
|
|
5
|
+
|
|
6
|
+
/** An element's props, whatever the code under test passed. */
|
|
7
|
+
export type Props = AnyObject;
|
|
8
|
+
|
|
9
|
+
type Hook = {
|
|
10
|
+
value?: ReactValue;
|
|
11
|
+
deps?: readonly ReactValue[];
|
|
12
|
+
ran?: boolean;
|
|
13
|
+
cleanup?: () => void;
|
|
14
|
+
reducer?: Reducer;
|
|
15
|
+
dispatch?: (action: ReactValue) => void;
|
|
16
|
+
subscribe?: Subscribe;
|
|
17
|
+
getSnapshot?: () => ReactValue;
|
|
18
|
+
stable?: (...args: ReactValue[]) => ReactValue;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Reducer = (state: ReactValue, action: ReactValue) => ReactValue;
|
|
22
|
+
|
|
23
|
+
type Subscribe = (listener: () => void) => () => void;
|
|
24
|
+
|
|
25
|
+
type Frame = { root: Root; fiber: Fiber; index: number };
|
|
26
|
+
|
|
27
|
+
type Slot = { type: ReactValue; key: string };
|
|
28
|
+
|
|
29
|
+
type RenderFunction = (props: Props, ref: ReactValue) => ReactValue;
|
|
30
|
+
|
|
31
|
+
/** What a fiber renders: text, a host element, a component, or a wrapper such as a Fragment or a Provider. */
|
|
32
|
+
export type FiberKind = 'root' | 'text' | 'host' | 'component' | 'wrapper';
|
|
33
|
+
|
|
34
|
+
/** One rendered node, kept across renders while its type and key stay the same, so its hooks keep their state. */
|
|
35
|
+
export type Fiber = {
|
|
36
|
+
kind: FiberKind;
|
|
37
|
+
/** The element type: a host name, a function, or one of React's exotic objects. */
|
|
38
|
+
type: ReactValue;
|
|
39
|
+
key: string;
|
|
40
|
+
name: string;
|
|
41
|
+
props: Props;
|
|
42
|
+
text: string;
|
|
43
|
+
hooks: Hook[];
|
|
44
|
+
children: Fiber[];
|
|
45
|
+
parent: Fiber | undefined;
|
|
46
|
+
unmounted: boolean;
|
|
47
|
+
/** The instance of a class component, kept across renders as its state is. */
|
|
48
|
+
instance?: ClassInstance;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type ClassInstance = AnyObject & {
|
|
52
|
+
props: Props;
|
|
53
|
+
state: AnyObject | null;
|
|
54
|
+
context: ReactValue;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type ClassComponent = new (props: Props, context: ReactValue) => ClassInstance;
|
|
58
|
+
|
|
59
|
+
/** A mounted tree: its root fiber, and the operations a test drives it with. */
|
|
60
|
+
export type TestRenderer = {
|
|
61
|
+
root: Fiber;
|
|
62
|
+
update: (element: React.ReactNode) => void;
|
|
63
|
+
unmount: () => void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type Root = {
|
|
67
|
+
fiber: Fiber;
|
|
68
|
+
element: React.ReactNode;
|
|
69
|
+
pending: boolean;
|
|
70
|
+
busy: boolean;
|
|
71
|
+
mounted: boolean;
|
|
72
|
+
layoutEffects: (() => void)[];
|
|
73
|
+
effects: (() => void)[];
|
|
74
|
+
contexts: Map<object, ReactValue[]>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type Exotic = { $$typeof?: symbol; type?: ReactValue; render?: ReactValue; _context?: ReactValue };
|
|
78
|
+
|
|
79
|
+
type Element = { type: ReactValue; key?: string | null; props: Props };
|
|
80
|
+
|
|
81
|
+
type Internals = { H: ReactValue };
|
|
82
|
+
|
|
83
|
+
const ELEMENT = new Set([Symbol.for('react.transitional.element'), Symbol.for('react.element')]);
|
|
84
|
+
const TEXT = Symbol('text');
|
|
85
|
+
const MEMO = Symbol.for('react.memo');
|
|
86
|
+
const FORWARD_REF = Symbol.for('react.forward_ref');
|
|
87
|
+
const CONTEXT = Symbol.for('react.context');
|
|
88
|
+
const PROVIDER = Symbol.for('react.provider');
|
|
89
|
+
const CONSUMER = Symbol.for('react.consumer');
|
|
90
|
+
const PASS_THROUGH = new Set<ReactValue>([
|
|
91
|
+
'Fragment',
|
|
92
|
+
React.Fragment,
|
|
93
|
+
React.StrictMode,
|
|
94
|
+
React.Profiler,
|
|
95
|
+
React.Suspense,
|
|
96
|
+
Symbol.for('react.activity'),
|
|
97
|
+
]);
|
|
98
|
+
const MEMO_SENTINEL = Symbol.for('react.memo_cache_sentinel');
|
|
99
|
+
const MAX_PASSES = 50;
|
|
100
|
+
|
|
101
|
+
const internals: Internals = Reflect.get(
|
|
102
|
+
React,
|
|
103
|
+
'__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE',
|
|
104
|
+
);
|
|
105
|
+
const roots = new Set<Root>();
|
|
106
|
+
let batch = 0;
|
|
107
|
+
let ids = 0;
|
|
108
|
+
let current: Frame | undefined;
|
|
109
|
+
|
|
110
|
+
function isObject(value: ReactValue): value is AnyObject {
|
|
111
|
+
return typeof value === 'object' && value !== null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A React element, or a plain `{ type, props }` from `createFakeReact`.
|
|
115
|
+
function isElement(value: ReactValue): value is Element {
|
|
116
|
+
if (!isObject(value) || !isObject(value.props) || !('type' in value)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return typeof value.$$typeof === 'symbol' ? ELEMENT.has(value.$$typeof) : true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function exotic(type: ReactValue): Exotic {
|
|
124
|
+
return isObject(type) ? type : {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @description Names an element type the way `findComponent` matches it: a host's name, else `displayName`, else the function's name.
|
|
129
|
+
* @param type The element type.
|
|
130
|
+
* @returns The name, or `''` when it has none.
|
|
131
|
+
*/
|
|
132
|
+
export function typeName(type: ReactValue): string {
|
|
133
|
+
if (typeof type === 'string') {
|
|
134
|
+
return type;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const named: ReactValue =
|
|
138
|
+
isObject(type) || typeof type === 'function' ? Reflect.get(type, 'displayName') : void 0;
|
|
139
|
+
|
|
140
|
+
if (typeof named === 'string') {
|
|
141
|
+
return named;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof type === 'function') {
|
|
145
|
+
return type.name;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { $$typeof, type: inner, render } = exotic(type);
|
|
149
|
+
|
|
150
|
+
if ($$typeof === MEMO) {
|
|
151
|
+
return typeName(inner);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if ($$typeof === FORWARD_REF) {
|
|
155
|
+
return typeName(render);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function newFiber(
|
|
162
|
+
kind: FiberKind,
|
|
163
|
+
type: ReactValue,
|
|
164
|
+
key: string,
|
|
165
|
+
parent: Fiber | undefined,
|
|
166
|
+
): Fiber {
|
|
167
|
+
return {
|
|
168
|
+
kind,
|
|
169
|
+
type,
|
|
170
|
+
key,
|
|
171
|
+
name: typeName(type),
|
|
172
|
+
props: {},
|
|
173
|
+
text: '',
|
|
174
|
+
hooks: [],
|
|
175
|
+
children: [],
|
|
176
|
+
parent,
|
|
177
|
+
unmounted: false,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sameDeps(
|
|
182
|
+
previous: readonly ReactValue[] | undefined,
|
|
183
|
+
next: readonly ReactValue[] | undefined,
|
|
184
|
+
) {
|
|
185
|
+
if (!previous || !next || previous.length !== next.length) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return previous.every((value, index) => Object.is(value, next[index]));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function schedule(root: Root) {
|
|
193
|
+
root.pending = true;
|
|
194
|
+
|
|
195
|
+
if (!root.busy && batch === 0 && root.mounted) {
|
|
196
|
+
flush(root);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function active() {
|
|
201
|
+
if (!current) {
|
|
202
|
+
throw new Error('A hook ran outside a component the test renderer is rendering.');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return current;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function nextHook(): Hook {
|
|
209
|
+
const frame = active();
|
|
210
|
+
const index = frame.index++;
|
|
211
|
+
|
|
212
|
+
return (frame.fiber.hooks[index] ??= {});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readContext(context: ReactValue): ReactValue {
|
|
216
|
+
const stack = active().root.contexts.get(exotic(context));
|
|
217
|
+
|
|
218
|
+
return stack?.length ? stack.at(-1) : Reflect.get(exotic(context), '_currentValue');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function useReducer(
|
|
222
|
+
reducer: Reducer,
|
|
223
|
+
initial: ReactValue,
|
|
224
|
+
init?: (value: ReactValue) => ReactValue,
|
|
225
|
+
) {
|
|
226
|
+
const { root, fiber } = active();
|
|
227
|
+
const hook = nextHook();
|
|
228
|
+
|
|
229
|
+
if (!hook.ran) {
|
|
230
|
+
hook.ran = true;
|
|
231
|
+
hook.value = init ? init(initial) : initial;
|
|
232
|
+
hook.dispatch = (action) => {
|
|
233
|
+
const next = hook.reducer?.(hook.value, action);
|
|
234
|
+
|
|
235
|
+
if (fiber.unmounted || Object.is(next, hook.value)) return;
|
|
236
|
+
|
|
237
|
+
hook.value = next;
|
|
238
|
+
schedule(root);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
hook.reducer = reducer;
|
|
243
|
+
|
|
244
|
+
return [hook.value, hook.dispatch];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function effect(queue: 'effects' | 'layoutEffects') {
|
|
248
|
+
return (create: () => ReactValue, deps?: readonly ReactValue[]) => {
|
|
249
|
+
const { root } = active();
|
|
250
|
+
const hook = nextHook();
|
|
251
|
+
|
|
252
|
+
if (hook.ran && deps && sameDeps(hook.deps, deps)) return;
|
|
253
|
+
|
|
254
|
+
hook.ran = true;
|
|
255
|
+
hook.deps = deps;
|
|
256
|
+
root[queue].push(() => {
|
|
257
|
+
hook.cleanup?.();
|
|
258
|
+
|
|
259
|
+
const cleanup = create();
|
|
260
|
+
|
|
261
|
+
hook.cleanup = typeof cleanup === 'function' ? () => void cleanup() : void 0;
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function useMemo(factory: () => ReactValue, deps?: readonly ReactValue[]) {
|
|
267
|
+
const hook = nextHook();
|
|
268
|
+
|
|
269
|
+
if (!hook.ran || !sameDeps(hook.deps, deps)) {
|
|
270
|
+
hook.ran = true;
|
|
271
|
+
hook.deps = deps;
|
|
272
|
+
hook.value = factory();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return hook.value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function useSyncExternalStore(subscribe: Subscribe, getSnapshot: () => ReactValue) {
|
|
279
|
+
const { root } = active();
|
|
280
|
+
const hook = nextHook();
|
|
281
|
+
|
|
282
|
+
hook.value = getSnapshot();
|
|
283
|
+
hook.getSnapshot = getSnapshot;
|
|
284
|
+
|
|
285
|
+
if (hook.subscribe !== subscribe) {
|
|
286
|
+
hook.subscribe = subscribe;
|
|
287
|
+
root.effects.push(() => {
|
|
288
|
+
hook.cleanup?.();
|
|
289
|
+
hook.cleanup = subscribe(() => {
|
|
290
|
+
if (!Object.is(hook.getSnapshot?.(), hook.value)) {
|
|
291
|
+
schedule(root);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return hook.value;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const dispatcher = {
|
|
301
|
+
readContext,
|
|
302
|
+
useContext: readContext,
|
|
303
|
+
use(usable: ReactValue) {
|
|
304
|
+
if (exotic(usable).$$typeof === CONTEXT) {
|
|
305
|
+
return readContext(usable);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (isObject(usable) && usable.status === 'fulfilled') {
|
|
309
|
+
return usable.value;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
throw new Error(
|
|
313
|
+
'The test renderer cannot suspend: use() needs a context or a settled promise.',
|
|
314
|
+
);
|
|
315
|
+
},
|
|
316
|
+
useReducer,
|
|
317
|
+
useState: (initial: ReactValue) =>
|
|
318
|
+
useReducer(
|
|
319
|
+
(state, action) => (typeof action === 'function' ? action(state) : action),
|
|
320
|
+
initial,
|
|
321
|
+
typeof initial === 'function' ? () => initial() : void 0,
|
|
322
|
+
),
|
|
323
|
+
useEffect: effect('effects'),
|
|
324
|
+
useLayoutEffect: effect('layoutEffects'),
|
|
325
|
+
useInsertionEffect: effect('layoutEffects'),
|
|
326
|
+
useMemo,
|
|
327
|
+
useCallback: (callback: ReactValue, deps?: readonly ReactValue[]) =>
|
|
328
|
+
useMemo(() => callback, deps),
|
|
329
|
+
useRef(initial: ReactValue) {
|
|
330
|
+
const hook = nextHook();
|
|
331
|
+
|
|
332
|
+
hook.value ??= { current: initial };
|
|
333
|
+
|
|
334
|
+
return hook.value;
|
|
335
|
+
},
|
|
336
|
+
useImperativeHandle(ref: ReactValue, create: () => ReactValue, deps?: readonly ReactValue[]) {
|
|
337
|
+
effect('layoutEffects')(() => {
|
|
338
|
+
const value = create();
|
|
339
|
+
|
|
340
|
+
if (typeof ref === 'function') {
|
|
341
|
+
ref(value);
|
|
342
|
+
|
|
343
|
+
return () => ref(null);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (isObject(ref)) {
|
|
347
|
+
ref.current = value;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return () => isObject(ref) && (ref.current = null);
|
|
351
|
+
}, deps);
|
|
352
|
+
},
|
|
353
|
+
useSyncExternalStore,
|
|
354
|
+
useId() {
|
|
355
|
+
const hook = nextHook();
|
|
356
|
+
|
|
357
|
+
hook.value ??= `«r${ids++}»`;
|
|
358
|
+
|
|
359
|
+
return hook.value;
|
|
360
|
+
},
|
|
361
|
+
useMemoCache(size: number) {
|
|
362
|
+
const hook = nextHook();
|
|
363
|
+
|
|
364
|
+
hook.value ??= Array.from({ length: size }, () => MEMO_SENTINEL);
|
|
365
|
+
|
|
366
|
+
return hook.value;
|
|
367
|
+
},
|
|
368
|
+
useEffectEvent(callback: (...args: ReactValue[]) => ReactValue) {
|
|
369
|
+
const hook = nextHook();
|
|
370
|
+
|
|
371
|
+
hook.value = callback;
|
|
372
|
+
hook.stable ??= (...args) => (hook.value as typeof callback)(...args);
|
|
373
|
+
|
|
374
|
+
return hook.stable;
|
|
375
|
+
},
|
|
376
|
+
useDebugValue: () => {},
|
|
377
|
+
useDeferredValue: (value: ReactValue) => value,
|
|
378
|
+
useTransition: () => [false, (callback: () => void) => callback()],
|
|
379
|
+
useOptimistic: (value: ReactValue) => [value, () => {}],
|
|
380
|
+
useActionState: (_action: ReactValue, initial: ReactValue) => [initial, () => {}, false],
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
function callComponent(root: Root, fiber: Fiber, render: () => ReactValue): ReactValue {
|
|
384
|
+
const previous = internals.H;
|
|
385
|
+
|
|
386
|
+
current = { root, fiber, index: 0 };
|
|
387
|
+
internals.H = dispatcher;
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
return render();
|
|
391
|
+
} finally {
|
|
392
|
+
internals.H = previous;
|
|
393
|
+
current = void 0;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Unwraps `memo` and `forwardRef` down to the function that renders.
|
|
398
|
+
function renderFunction(type: ReactValue): RenderFunction | undefined {
|
|
399
|
+
if (typeof type === 'function') {
|
|
400
|
+
return (props) => type(props);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const { $$typeof, type: inner, render } = exotic(type);
|
|
404
|
+
|
|
405
|
+
if ($$typeof === MEMO) {
|
|
406
|
+
return renderFunction(inner);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if ($$typeof === FORWARD_REF && typeof render === 'function') {
|
|
410
|
+
return (props, ref) => render(props, ref);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Unwraps `memo` down to a class component, which `renderFunction` cannot call.
|
|
417
|
+
function classOf(type: ReactValue): ClassComponent | undefined {
|
|
418
|
+
if (typeof type === 'function') {
|
|
419
|
+
return isObject(type.prototype) && 'isReactComponent' in type.prototype
|
|
420
|
+
? (type as ClassComponent)
|
|
421
|
+
: undefined;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const { $$typeof, type: inner } = exotic(type);
|
|
425
|
+
|
|
426
|
+
return $$typeof === MEMO ? classOf(inner) : undefined;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Creates the instance on mount, with the `setState` and `forceUpdate` React gives it.
|
|
430
|
+
function mountClass(root: Root, fiber: Fiber, Class: ClassComponent, props: Props): ClassInstance {
|
|
431
|
+
const { contextType } = Class as AnyObject;
|
|
432
|
+
const context = contextType ? readContext(contextType) : {};
|
|
433
|
+
const instance = new Class(props, context);
|
|
434
|
+
|
|
435
|
+
instance.props = props;
|
|
436
|
+
instance.context = context;
|
|
437
|
+
instance.state ??= null;
|
|
438
|
+
instance.setState = (partial: ReactValue, callback?: () => void) => {
|
|
439
|
+
const next =
|
|
440
|
+
typeof partial === 'function' ? partial(instance.state, instance.props) : partial;
|
|
441
|
+
|
|
442
|
+
if (fiber.unmounted || next === null || next === undefined) return;
|
|
443
|
+
|
|
444
|
+
instance.state = { ...instance.state, ...next };
|
|
445
|
+
|
|
446
|
+
if (callback) {
|
|
447
|
+
root.layoutEffects.push(callback);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
schedule(root);
|
|
451
|
+
};
|
|
452
|
+
instance.forceUpdate = (callback?: () => void) => {
|
|
453
|
+
if (fiber.unmounted) return;
|
|
454
|
+
|
|
455
|
+
if (callback) {
|
|
456
|
+
root.layoutEffects.push(callback);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
schedule(root);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
return instance;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function renderClass(root: Root, fiber: Fiber, Class: ClassComponent, props: Props): ReactValue {
|
|
466
|
+
return callComponent(root, fiber, () => {
|
|
467
|
+
const mounting = !fiber.instance;
|
|
468
|
+
const instance = (fiber.instance ??= mountClass(root, fiber, Class, props));
|
|
469
|
+
const previous = { props: instance.props, state: instance.state };
|
|
470
|
+
const derive = (Class as AnyObject).getDerivedStateFromProps;
|
|
471
|
+
|
|
472
|
+
instance.props = props;
|
|
473
|
+
|
|
474
|
+
if (typeof derive === 'function') {
|
|
475
|
+
const derived = derive(props, instance.state);
|
|
476
|
+
|
|
477
|
+
if (derived) {
|
|
478
|
+
instance.state = { ...instance.state, ...derived };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const output = instance.render();
|
|
483
|
+
|
|
484
|
+
root.layoutEffects.push(() =>
|
|
485
|
+
mounting
|
|
486
|
+
? instance.componentDidMount?.()
|
|
487
|
+
: instance.componentDidUpdate?.(previous.props, previous.state),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
return output;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function flatten(node: ReactValue, into: ReactValue[] = []): ReactValue[] {
|
|
495
|
+
if (Array.isArray(node)) {
|
|
496
|
+
for (const child of node) flatten(child, into);
|
|
497
|
+
} else if (node !== null && node !== undefined && typeof node !== 'boolean') {
|
|
498
|
+
into.push(node);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return into;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function slotOf(node: ReactValue, index: number): Slot {
|
|
505
|
+
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
|
|
506
|
+
return { type: TEXT, key: `#${index}` };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!isElement(node)) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`Objects are not valid as a React child (found ${Object.prototype.toString.call(node)}).`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
type: node.type,
|
|
517
|
+
key: node.key === null || node.key === undefined ? `#${index}` : `.${node.key}`,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function kindOf(type: ReactValue): FiberKind {
|
|
522
|
+
if (type === TEXT) {
|
|
523
|
+
return 'text';
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (PASS_THROUGH.has(type)) {
|
|
527
|
+
return 'wrapper';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (typeof type === 'string') {
|
|
531
|
+
return 'host';
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const { $$typeof } = exotic(type);
|
|
535
|
+
|
|
536
|
+
return $$typeof === CONTEXT || $$typeof === PROVIDER || $$typeof === CONSUMER
|
|
537
|
+
? 'wrapper'
|
|
538
|
+
: 'component';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function unmountFiber(fiber: Fiber) {
|
|
542
|
+
for (const child of fiber.children) unmountFiber(child);
|
|
543
|
+
|
|
544
|
+
fiber.unmounted = true;
|
|
545
|
+
fiber.instance?.componentWillUnmount?.();
|
|
546
|
+
|
|
547
|
+
for (const hook of fiber.hooks) hook.cleanup?.();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function reconcileChildren(root: Root, parent: Fiber, node: ReactValue) {
|
|
551
|
+
const previous = new Map(parent.children.map((child) => [child.key, child]));
|
|
552
|
+
|
|
553
|
+
parent.children = flatten(node).map((child, index) => {
|
|
554
|
+
const { type, key } = slotOf(child, index);
|
|
555
|
+
const kept = previous.get(key);
|
|
556
|
+
const fiber = kept && kept.type === type ? kept : newFiber(kindOf(type), type, key, parent);
|
|
557
|
+
|
|
558
|
+
if (kept === fiber) {
|
|
559
|
+
previous.delete(key);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
renderFiber(root, fiber, child);
|
|
563
|
+
|
|
564
|
+
return fiber;
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
for (const stale of previous.values()) unmountFiber(stale);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function withContext(root: Root, context: ReactValue, value: ReactValue, render: () => void) {
|
|
571
|
+
const key = exotic(context);
|
|
572
|
+
const stack = root.contexts.get(key) ?? [];
|
|
573
|
+
|
|
574
|
+
root.contexts.set(key, stack);
|
|
575
|
+
stack.push(value);
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
render();
|
|
579
|
+
} finally {
|
|
580
|
+
stack.pop();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function renderFiber(root: Root, fiber: Fiber, node: ReactValue) {
|
|
585
|
+
if (fiber.kind === 'text') {
|
|
586
|
+
fiber.text = String(node);
|
|
587
|
+
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (!isElement(node)) return;
|
|
592
|
+
|
|
593
|
+
const { type, props } = node;
|
|
594
|
+
|
|
595
|
+
fiber.props = props;
|
|
596
|
+
|
|
597
|
+
if (fiber.kind === 'host' || PASS_THROUGH.has(type)) {
|
|
598
|
+
reconcileChildren(root, fiber, props.children);
|
|
599
|
+
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const { $$typeof, _context } = exotic(type);
|
|
604
|
+
|
|
605
|
+
if ($$typeof === CONTEXT || $$typeof === PROVIDER) {
|
|
606
|
+
const context = $$typeof === CONTEXT ? type : _context;
|
|
607
|
+
|
|
608
|
+
withContext(root, context, props.value, () =>
|
|
609
|
+
reconcileChildren(root, fiber, props.children),
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if ($$typeof === CONSUMER) {
|
|
616
|
+
const render = props.children;
|
|
617
|
+
const output = callComponent(root, fiber, () =>
|
|
618
|
+
typeof render === 'function' ? render(readContext(_context)) : null,
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
reconcileChildren(root, fiber, output);
|
|
622
|
+
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const Class = classOf(type);
|
|
627
|
+
|
|
628
|
+
if (Class) {
|
|
629
|
+
reconcileChildren(root, fiber, renderClass(root, fiber, Class, props));
|
|
630
|
+
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const render = renderFunction(type);
|
|
635
|
+
|
|
636
|
+
if (!render) {
|
|
637
|
+
throw new Error(`The test renderer cannot render the element type ${String(type)}.`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
reconcileChildren(
|
|
641
|
+
root,
|
|
642
|
+
fiber,
|
|
643
|
+
callComponent(root, fiber, () => render(props, props.ref)),
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function unmountRoot(root: Root) {
|
|
648
|
+
if (!root.mounted) return;
|
|
649
|
+
|
|
650
|
+
root.mounted = false;
|
|
651
|
+
roots.delete(root);
|
|
652
|
+
unmountFiber(root.fiber);
|
|
653
|
+
root.fiber.children = [];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function flush(root: Root) {
|
|
657
|
+
let passes = 0;
|
|
658
|
+
|
|
659
|
+
root.busy = true;
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
while (root.pending && root.mounted) {
|
|
663
|
+
if (++passes > MAX_PASSES) {
|
|
664
|
+
throw new Error('Too many re-renders: a state update loops.');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
root.pending = false;
|
|
668
|
+
reconcileChildren(root, root.fiber, root.element);
|
|
669
|
+
|
|
670
|
+
for (const queue of [root.layoutEffects, root.effects]) {
|
|
671
|
+
for (const run of queue.splice(0)) run();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
} finally {
|
|
675
|
+
root.busy = false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* @description Runs `callback` with renders held back, then renders every tree it changed once.
|
|
681
|
+
* @param callback The interaction, such as a press or a store change; it may be async.
|
|
682
|
+
* @returns A promise when `callback` returns one.
|
|
683
|
+
*/
|
|
684
|
+
export function act(callback: () => void): void;
|
|
685
|
+
export function act(callback: () => Promise<void>): Promise<void>;
|
|
686
|
+
export function act(callback: () => void | Promise<void>): void | Promise<void> {
|
|
687
|
+
batch++;
|
|
688
|
+
|
|
689
|
+
function done() {
|
|
690
|
+
batch--;
|
|
691
|
+
|
|
692
|
+
if (batch > 0) return;
|
|
693
|
+
|
|
694
|
+
for (const root of roots)
|
|
695
|
+
if (root.pending) {
|
|
696
|
+
flush(root);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
let result: void | Promise<void>;
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
result = callback();
|
|
704
|
+
} catch (error) {
|
|
705
|
+
done();
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (result instanceof Promise) {
|
|
710
|
+
return result.finally(done);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
done();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* @description Renders an element with React's own hooks, but no native host: host components stay plain nodes
|
|
718
|
+
* a test can read. A state update renders the tree again at once, or when the enclosing {@link act} ends.
|
|
719
|
+
* @param element The element to render.
|
|
720
|
+
* @returns The mounted tree.
|
|
721
|
+
*/
|
|
722
|
+
export function createTestRenderer(element: React.ReactNode): TestRenderer {
|
|
723
|
+
const root: Root = {
|
|
724
|
+
fiber: newFiber('root', 'root', 'root', void 0),
|
|
725
|
+
element,
|
|
726
|
+
pending: true,
|
|
727
|
+
busy: false,
|
|
728
|
+
mounted: true,
|
|
729
|
+
layoutEffects: [],
|
|
730
|
+
effects: [],
|
|
731
|
+
contexts: new Map(),
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
roots.add(root);
|
|
735
|
+
act(() => {});
|
|
736
|
+
|
|
737
|
+
return {
|
|
738
|
+
root: root.fiber,
|
|
739
|
+
update(next) {
|
|
740
|
+
root.element = next;
|
|
741
|
+
schedule(root);
|
|
742
|
+
},
|
|
743
|
+
unmount: () => unmountRoot(root),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* @description Unmounts every tree still mounted, running their effects' cleanups.
|
|
749
|
+
*/
|
|
750
|
+
export function unmountAll(): void {
|
|
751
|
+
// Deleting the entry being visited is safe while iterating a Set.
|
|
752
|
+
for (const root of roots) unmountRoot(root);
|
|
753
|
+
}
|