@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,362 @@
|
|
|
1
|
+
import { createElement, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
type Listener = (...args: never[]) => void;
|
|
4
|
+
|
|
5
|
+
type Children = { children?: ReactNode };
|
|
6
|
+
|
|
7
|
+
type ValueState = { value: number };
|
|
8
|
+
|
|
9
|
+
type ValueListener = (state: ValueState) => void;
|
|
10
|
+
|
|
11
|
+
type AnimationConfig = { toValue?: number };
|
|
12
|
+
|
|
13
|
+
type InterpolationConfig = { inputRange: number[]; outputRange: (number | string)[] };
|
|
14
|
+
|
|
15
|
+
type Curve = (value: number) => number;
|
|
16
|
+
|
|
17
|
+
/** A subscription: `remove` stops it. */
|
|
18
|
+
export type EmitterSubscription = { remove: () => void };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @description `react-native`'s `NativeEventEmitter`: `addListener` subscribes, and a test calls `emit` to fire an event as the native module would.
|
|
22
|
+
*/
|
|
23
|
+
export class NativeEventEmitter {
|
|
24
|
+
private readonly listeners = new Map<string, Set<Listener>>();
|
|
25
|
+
|
|
26
|
+
addListener(event: string, listener: Listener): EmitterSubscription {
|
|
27
|
+
const set = this.listeners.get(event) ?? new Set<Listener>();
|
|
28
|
+
|
|
29
|
+
this.listeners.set(event, set);
|
|
30
|
+
set.add(listener);
|
|
31
|
+
|
|
32
|
+
return { remove: () => void set.delete(listener) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
emit(event: string, ...args: unknown[]): void {
|
|
36
|
+
for (const listener of this.listeners.get(event) ?? []) {
|
|
37
|
+
(listener as (...params: unknown[]) => void)(...args);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
listenerCount(event: string): number {
|
|
42
|
+
return this.listeners.get(event)?.size ?? 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
removeAllListeners(event?: string): void {
|
|
46
|
+
if (event === undefined) {
|
|
47
|
+
this.listeners.clear();
|
|
48
|
+
} else {
|
|
49
|
+
this.listeners.delete(event);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function channels(hex: string): [number, number, number, number] | undefined {
|
|
55
|
+
const digits =
|
|
56
|
+
hex.length === 3 || hex.length === 4
|
|
57
|
+
? Array.from(hex, (char) => char + char)
|
|
58
|
+
: hex.match(/../g);
|
|
59
|
+
|
|
60
|
+
if (!digits || (digits.length !== 3 && digits.length !== 4)) return;
|
|
61
|
+
|
|
62
|
+
const [red, green, blue, alpha = 255] = digits.map((digit) => Number.parseInt(digit, 16));
|
|
63
|
+
|
|
64
|
+
return [red, green, blue, alpha].some((value) => Number.isNaN(value))
|
|
65
|
+
? undefined
|
|
66
|
+
: [red, green, blue, alpha];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function functional(color: string): [number, number, number, number] | undefined {
|
|
70
|
+
const open = color.indexOf('(');
|
|
71
|
+
|
|
72
|
+
if (open < 0 || !color.endsWith(')')) return;
|
|
73
|
+
|
|
74
|
+
const parts = color
|
|
75
|
+
.slice(open + 1, -1)
|
|
76
|
+
.split(/[\s,/]+/)
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map(Number);
|
|
79
|
+
const [red, green, blue, alpha = 1] = parts;
|
|
80
|
+
|
|
81
|
+
if (parts.length < 3 || parts.some((value) => Number.isNaN(value))) return;
|
|
82
|
+
|
|
83
|
+
return [red, green, blue, Math.round(alpha * 255)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @description `react-native`'s `processColor` for hex and `rgb()`/`rgba()` strings: the colour as the unsigned `0xAARRGGBB` number native receives.
|
|
88
|
+
* @param color The colour, such as `#5865f2` or `rgba(0, 0, 0, 0.5)`.
|
|
89
|
+
* @returns The number, `undefined` for a string it cannot read, or `color` itself when it is `null` or `undefined`.
|
|
90
|
+
*/
|
|
91
|
+
export function processColor(color: string | number | null | undefined) {
|
|
92
|
+
if (color === null || color === undefined) {
|
|
93
|
+
return color;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof color === 'number') {
|
|
97
|
+
return color >>> 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const text = color.trim().toLowerCase();
|
|
101
|
+
const read = text.startsWith('#') ? channels(text.slice(1)) : functional(text);
|
|
102
|
+
|
|
103
|
+
if (!read) return;
|
|
104
|
+
|
|
105
|
+
const [red, green, blue, alpha] = read;
|
|
106
|
+
|
|
107
|
+
return ((alpha << 24) | (red << 16) | (green << 8) | blue) >>> 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** An animated number: reads and writes like `react-native`'s `Animated.Value`, with no frames. */
|
|
111
|
+
export class AnimatedValue {
|
|
112
|
+
private current: number;
|
|
113
|
+
private readonly listeners = new Set<ValueListener>();
|
|
114
|
+
|
|
115
|
+
constructor(value: number) {
|
|
116
|
+
this.current = value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setValue(value: number): void {
|
|
120
|
+
this.current = value;
|
|
121
|
+
|
|
122
|
+
for (const listener of this.listeners) listener({ value });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setOffset(): void {}
|
|
126
|
+
|
|
127
|
+
flattenOffset(): void {}
|
|
128
|
+
|
|
129
|
+
stopAnimation(callback?: (value: number) => void): void {
|
|
130
|
+
callback?.(this.current);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
addListener(listener: ValueListener): string {
|
|
134
|
+
this.listeners.add(listener);
|
|
135
|
+
|
|
136
|
+
return String(this.listeners.size);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
removeAllListeners(): void {
|
|
140
|
+
this.listeners.clear();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interpolate(config: InterpolationConfig) {
|
|
144
|
+
const { inputRange, outputRange } = config;
|
|
145
|
+
const index = inputRange.findLastIndex((input) => input <= this.current);
|
|
146
|
+
|
|
147
|
+
return outputRange[Math.max(index, 0)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
__getValue(): number {
|
|
151
|
+
return this.current;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type Finish = { finished: boolean };
|
|
156
|
+
|
|
157
|
+
type Completion = (result: Finish) => void;
|
|
158
|
+
|
|
159
|
+
type Animation = { start: (callback?: Completion) => void; stop: () => void; reset: () => void };
|
|
160
|
+
|
|
161
|
+
function instant(run: () => void): Animation {
|
|
162
|
+
return {
|
|
163
|
+
start(callback) {
|
|
164
|
+
run();
|
|
165
|
+
callback?.({ finished: true });
|
|
166
|
+
},
|
|
167
|
+
stop() {},
|
|
168
|
+
reset() {},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toValue(config: AnimationConfig) {
|
|
173
|
+
return config.toValue ?? 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function settle(animations: Animation[]): Animation {
|
|
177
|
+
return instant(() => {
|
|
178
|
+
for (const animation of animations) animation.start();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function component(name: string) {
|
|
183
|
+
return (props: Children) => createElement(name, props);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @description Creates the `Animated` of `react-native`: every animation finishes at once with its end value, and its views are host nodes `find` reaches by name.
|
|
188
|
+
* @returns The module.
|
|
189
|
+
*/
|
|
190
|
+
export function createFakeAnimated() {
|
|
191
|
+
return {
|
|
192
|
+
Value: AnimatedValue,
|
|
193
|
+
ValueXY: class AnimatedValueXY {
|
|
194
|
+
x = new AnimatedValue(0);
|
|
195
|
+
y = new AnimatedValue(0);
|
|
196
|
+
},
|
|
197
|
+
timing: (value: AnimatedValue, config: AnimationConfig) =>
|
|
198
|
+
instant(() => value.setValue(toValue(config))),
|
|
199
|
+
spring: (value: AnimatedValue, config: AnimationConfig) =>
|
|
200
|
+
instant(() => value.setValue(toValue(config))),
|
|
201
|
+
decay: () => instant(() => {}),
|
|
202
|
+
parallel: settle,
|
|
203
|
+
sequence: settle,
|
|
204
|
+
stagger: (_delay: number, animations: Animation[]) => settle(animations),
|
|
205
|
+
delay: () => instant(() => {}),
|
|
206
|
+
loop: (animation: Animation) => instant(() => animation.start()),
|
|
207
|
+
event: () => () => {},
|
|
208
|
+
add: (left: AnimatedValue, right: AnimatedValue) =>
|
|
209
|
+
new AnimatedValue(left.__getValue() + right.__getValue()),
|
|
210
|
+
subtract: (left: AnimatedValue, right: AnimatedValue) =>
|
|
211
|
+
new AnimatedValue(left.__getValue() - right.__getValue()),
|
|
212
|
+
multiply: (left: AnimatedValue, right: AnimatedValue) =>
|
|
213
|
+
new AnimatedValue(left.__getValue() * right.__getValue()),
|
|
214
|
+
divide: (left: AnimatedValue, right: AnimatedValue) =>
|
|
215
|
+
new AnimatedValue(left.__getValue() / right.__getValue()),
|
|
216
|
+
createAnimatedComponent: <T>(wrapped: T) => wrapped,
|
|
217
|
+
View: 'Animated.View',
|
|
218
|
+
Text: 'Animated.Text',
|
|
219
|
+
Image: 'Animated.Image',
|
|
220
|
+
ScrollView: 'Animated.ScrollView',
|
|
221
|
+
FlatList: component('Animated.FlatList'),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const identity = (value: number) => value;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @description Passes an easing curve through, since the fake draws no frames.
|
|
229
|
+
* @param curve The curve.
|
|
230
|
+
* @returns The same curve.
|
|
231
|
+
*/
|
|
232
|
+
function keep(curve: Curve): Curve {
|
|
233
|
+
return curve;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @description `Easing.bezier` for the fake: every curve is linear.
|
|
238
|
+
* @returns The linear curve.
|
|
239
|
+
*/
|
|
240
|
+
function bezier(): Curve {
|
|
241
|
+
return identity;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The `Easing` of `react-native`: every curve is linear, as there are no frames to draw. */
|
|
245
|
+
export const Easing = {
|
|
246
|
+
linear: identity,
|
|
247
|
+
ease: identity,
|
|
248
|
+
quad: identity,
|
|
249
|
+
cubic: identity,
|
|
250
|
+
sin: identity,
|
|
251
|
+
circle: identity,
|
|
252
|
+
exp: identity,
|
|
253
|
+
bounce: identity,
|
|
254
|
+
in: keep,
|
|
255
|
+
out: keep,
|
|
256
|
+
inOut: keep,
|
|
257
|
+
bezier,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @description `Share.share` for the fake: sharing succeeds at once.
|
|
262
|
+
* @returns The shared action.
|
|
263
|
+
*/
|
|
264
|
+
async function share() {
|
|
265
|
+
return { action: 'sharedAction' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The `Share` of `react-native`: sharing succeeds at once. */
|
|
269
|
+
export const Share = {
|
|
270
|
+
sharedAction: 'sharedAction',
|
|
271
|
+
dismissedAction: 'dismissedAction',
|
|
272
|
+
share,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const SUBSCRIPTION: EmitterSubscription = { remove: () => {} };
|
|
276
|
+
|
|
277
|
+
/** `react-native`'s `AccessibilityInfo`: every system setting reads as off, and no event fires. */
|
|
278
|
+
export const AccessibilityInfo = {
|
|
279
|
+
/**
|
|
280
|
+
* @description Reads whether reduce motion is on.
|
|
281
|
+
* @returns Always `false`.
|
|
282
|
+
*/
|
|
283
|
+
isReduceMotionEnabled: async () => false,
|
|
284
|
+
/**
|
|
285
|
+
* @description Reads whether reduce transparency is on.
|
|
286
|
+
* @returns Always `false`.
|
|
287
|
+
*/
|
|
288
|
+
isReduceTransparencyEnabled: async () => false,
|
|
289
|
+
/**
|
|
290
|
+
* @description Reads whether a screen reader is on.
|
|
291
|
+
* @returns Always `false`.
|
|
292
|
+
*/
|
|
293
|
+
isScreenReaderEnabled: async () => false,
|
|
294
|
+
/**
|
|
295
|
+
* @description Reads whether bold text is on.
|
|
296
|
+
* @returns Always `false`.
|
|
297
|
+
*/
|
|
298
|
+
isBoldTextEnabled: async () => false,
|
|
299
|
+
/**
|
|
300
|
+
* @description Reads whether grayscale is on.
|
|
301
|
+
* @returns Always `false`.
|
|
302
|
+
*/
|
|
303
|
+
isGrayscaleEnabled: async () => false,
|
|
304
|
+
/**
|
|
305
|
+
* @description Reads whether inverted colours is on.
|
|
306
|
+
* @returns Always `false`.
|
|
307
|
+
*/
|
|
308
|
+
isInvertColorsEnabled: async () => false,
|
|
309
|
+
/**
|
|
310
|
+
* @description Announces nothing.
|
|
311
|
+
* @returns Nothing.
|
|
312
|
+
*/
|
|
313
|
+
announceForAccessibility: () => {},
|
|
314
|
+
/**
|
|
315
|
+
* @description Focuses nothing.
|
|
316
|
+
* @returns Nothing.
|
|
317
|
+
*/
|
|
318
|
+
setAccessibilityFocus: () => {},
|
|
319
|
+
/**
|
|
320
|
+
* @description Subscribes to an event that never fires.
|
|
321
|
+
* @returns A subscription whose `remove` does nothing.
|
|
322
|
+
*/
|
|
323
|
+
addEventListener: () => SUBSCRIPTION,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** `react-native`'s `AppState`: the app is always `active`, and no event fires. */
|
|
327
|
+
export const AppState = {
|
|
328
|
+
currentState: 'active',
|
|
329
|
+
isAvailable: true,
|
|
330
|
+
/**
|
|
331
|
+
* @description Subscribes to an event that never fires.
|
|
332
|
+
* @returns A subscription whose `remove` does nothing.
|
|
333
|
+
*/
|
|
334
|
+
addEventListener: () => SUBSCRIPTION,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/** What `AppRegistry` keeps for a name: the component getter or runnable it was registered with. */
|
|
338
|
+
// UNKNOWN: a registered component getter or runnable returns whatever React Native's does.
|
|
339
|
+
type Registered = () => unknown;
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @description Creates `react-native`'s `AppRegistry`, which keeps each component registered under its name.
|
|
343
|
+
* @returns The registry; `runApplication` does nothing.
|
|
344
|
+
*/
|
|
345
|
+
export function createAppRegistry() {
|
|
346
|
+
const components = new Map<string, Registered>();
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
registerComponent(name: string, getComponent: Registered) {
|
|
350
|
+
components.set(name, getComponent);
|
|
351
|
+
|
|
352
|
+
return name;
|
|
353
|
+
},
|
|
354
|
+
registerRunnable(name: string, runnable: Registered) {
|
|
355
|
+
components.set(name, runnable);
|
|
356
|
+
|
|
357
|
+
return name;
|
|
358
|
+
},
|
|
359
|
+
getAppKeys: () => [...components.keys()],
|
|
360
|
+
runApplication: () => {},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivityIndicatorProps,
|
|
3
|
+
FlatListProps,
|
|
4
|
+
ImageProps,
|
|
5
|
+
ModalProps,
|
|
6
|
+
PressableProps,
|
|
7
|
+
ScrollViewProps,
|
|
8
|
+
SwitchProps,
|
|
9
|
+
TextInputProps,
|
|
10
|
+
TextProps,
|
|
11
|
+
TouchableOpacityProps,
|
|
12
|
+
ViewProps,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { createElement, Fragment, type ReactNode } from 'react';
|
|
15
|
+
|
|
16
|
+
import type { ReactValue } from './react-value';
|
|
17
|
+
import type { FakeModule } from './fake-metro';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
AccessibilityInfo,
|
|
21
|
+
AppState,
|
|
22
|
+
createAppRegistry,
|
|
23
|
+
createFakeAnimated,
|
|
24
|
+
Easing,
|
|
25
|
+
NativeEventEmitter,
|
|
26
|
+
processColor,
|
|
27
|
+
Share,
|
|
28
|
+
} from './fake-react-native-modules';
|
|
29
|
+
|
|
30
|
+
/** The props of each host component the fake `react-native` renders, by name, for `find('Text')`. */
|
|
31
|
+
export type HostPropsByName = {
|
|
32
|
+
View: ViewProps;
|
|
33
|
+
Text: TextProps;
|
|
34
|
+
Image: ImageProps;
|
|
35
|
+
ImageBackground: ImageProps;
|
|
36
|
+
ScrollView: ScrollViewProps;
|
|
37
|
+
Pressable: PressableProps;
|
|
38
|
+
TouchableOpacity: TouchableOpacityProps;
|
|
39
|
+
TouchableHighlight: TouchableOpacityProps;
|
|
40
|
+
TouchableWithoutFeedback: TouchableOpacityProps;
|
|
41
|
+
TextInput: TextInputProps;
|
|
42
|
+
Switch: SwitchProps;
|
|
43
|
+
ActivityIndicator: ActivityIndicatorProps;
|
|
44
|
+
Modal: ModalProps;
|
|
45
|
+
SafeAreaView: ViewProps;
|
|
46
|
+
KeyboardAvoidingView: ViewProps;
|
|
47
|
+
FlatList: Omit<FlatListProps<unknown>, 'data' | 'renderItem'>;
|
|
48
|
+
SvgXml: {
|
|
49
|
+
xml: string;
|
|
50
|
+
asset: string;
|
|
51
|
+
color?: string;
|
|
52
|
+
width?: number | string;
|
|
53
|
+
height?: number | string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** A host component's name. */
|
|
58
|
+
export type HostName = keyof HostPropsByName;
|
|
59
|
+
|
|
60
|
+
const HOSTS = [
|
|
61
|
+
'View',
|
|
62
|
+
'Text',
|
|
63
|
+
'Image',
|
|
64
|
+
'ImageBackground',
|
|
65
|
+
'ScrollView',
|
|
66
|
+
'Pressable',
|
|
67
|
+
'TouchableOpacity',
|
|
68
|
+
'TouchableHighlight',
|
|
69
|
+
'TouchableWithoutFeedback',
|
|
70
|
+
'TextInput',
|
|
71
|
+
'Switch',
|
|
72
|
+
'ActivityIndicator',
|
|
73
|
+
'Modal',
|
|
74
|
+
'SafeAreaView',
|
|
75
|
+
'KeyboardAvoidingView',
|
|
76
|
+
] as const satisfies readonly HostName[];
|
|
77
|
+
|
|
78
|
+
type Style = FakeModule;
|
|
79
|
+
|
|
80
|
+
type StyleValue = Style | false | null | undefined | StyleValue[];
|
|
81
|
+
|
|
82
|
+
function flattenStyle(style: StyleValue): Style {
|
|
83
|
+
if (Array.isArray(style)) {
|
|
84
|
+
return Object.assign({}, ...style.map(flattenStyle));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return style ? { ...style } : {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type ListItemInfo = { item: ReactValue; index: number };
|
|
91
|
+
|
|
92
|
+
type PlatformChoices<T> = { ios?: T; android?: T; native?: T; default?: T };
|
|
93
|
+
|
|
94
|
+
type ListProps = {
|
|
95
|
+
data?: readonly ReactValue[] | null;
|
|
96
|
+
renderItem?: (info: ListItemInfo) => ReactNode;
|
|
97
|
+
keyExtractor?: (item: ReactValue, index: number) => string;
|
|
98
|
+
ListHeaderComponent?: ReactNode;
|
|
99
|
+
ListFooterComponent?: ReactNode;
|
|
100
|
+
ListEmptyComponent?: ReactNode;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// A list renders every item at once, so `find` reaches each row.
|
|
104
|
+
function FlatList({ data, renderItem, keyExtractor, ...rest }: ListProps) {
|
|
105
|
+
const items = data ?? [];
|
|
106
|
+
const rows = items.map((item, index) =>
|
|
107
|
+
createElement(
|
|
108
|
+
Fragment,
|
|
109
|
+
{ key: keyExtractor?.(item, index) ?? String(index) },
|
|
110
|
+
renderItem?.({ item, index }),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return createElement(
|
|
115
|
+
'FlatList',
|
|
116
|
+
rest,
|
|
117
|
+
rest.ListHeaderComponent,
|
|
118
|
+
items.length ? rows : rest.ListEmptyComponent,
|
|
119
|
+
rest.ListFooterComponent,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @description Creates the `react-native` a test imports: each primitive a host component named after itself,
|
|
125
|
+
* and the plain modules (`StyleSheet`, `Platform`, `Dimensions`, `Animated`, `Share`, `AccessibilityInfo`, `AppRegistry`, `AppState`, `NativeModules`) plugins read at import.
|
|
126
|
+
* @param os The platform `Platform.OS` reports, read on each access so a test can switch it.
|
|
127
|
+
* @returns The module's exports.
|
|
128
|
+
*/
|
|
129
|
+
export function createFakeReactNative(os: () => 'ios' | 'android' = () => 'ios') {
|
|
130
|
+
const hosts = Object.fromEntries(HOSTS.map((name) => [name, name]));
|
|
131
|
+
// Empty: a test adds the modules a plugin reads.
|
|
132
|
+
const nativeModules: Record<string, FakeModule> = {};
|
|
133
|
+
const window = { width: 390, height: 844, scale: 3, fontScale: 1 };
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...hosts,
|
|
137
|
+
AccessibilityInfo,
|
|
138
|
+
AppRegistry: createAppRegistry(),
|
|
139
|
+
AppState,
|
|
140
|
+
NativeModules: nativeModules,
|
|
141
|
+
FlatList,
|
|
142
|
+
Animated: createFakeAnimated(),
|
|
143
|
+
Easing,
|
|
144
|
+
NativeEventEmitter,
|
|
145
|
+
Share,
|
|
146
|
+
processColor,
|
|
147
|
+
StyleSheet: {
|
|
148
|
+
create: <T>(styles: T) => styles,
|
|
149
|
+
flatten: flattenStyle,
|
|
150
|
+
compose: (left: StyleValue, right: StyleValue) => [left, right],
|
|
151
|
+
hairlineWidth: 1 / 3,
|
|
152
|
+
absoluteFill: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 },
|
|
153
|
+
absoluteFillObject: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 },
|
|
154
|
+
},
|
|
155
|
+
Platform: {
|
|
156
|
+
get OS() {
|
|
157
|
+
return os();
|
|
158
|
+
},
|
|
159
|
+
get Version() {
|
|
160
|
+
return os() === 'ios' ? '18.0' : 35;
|
|
161
|
+
},
|
|
162
|
+
select: <T>(choices: PlatformChoices<T>) =>
|
|
163
|
+
choices[os()] ?? choices.native ?? choices.default,
|
|
164
|
+
},
|
|
165
|
+
Dimensions: {
|
|
166
|
+
get: (_dimension?: 'window' | 'screen') => window,
|
|
167
|
+
set: (next: Partial<typeof window>) => void Object.assign(window, next),
|
|
168
|
+
addEventListener: () => ({ remove: () => {} }),
|
|
169
|
+
},
|
|
170
|
+
PixelRatio: { get: () => window.scale, getFontScale: () => window.fontScale },
|
|
171
|
+
Appearance: {
|
|
172
|
+
getColorScheme: () => 'dark',
|
|
173
|
+
addChangeListener: () => ({ remove: () => {} }),
|
|
174
|
+
},
|
|
175
|
+
Linking: { openURL: async () => {}, canOpenURL: async () => true },
|
|
176
|
+
Keyboard: { dismiss: () => {}, addListener: () => ({ remove: () => {} }) },
|
|
177
|
+
useWindowDimensions: () => window,
|
|
178
|
+
useColorScheme: () => 'dark',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AnyObject } from '../src/patcher';
|
|
2
|
+
|
|
3
|
+
/** What `createElement` returns: the element's type and its props with `children`. */
|
|
4
|
+
export type FakeElement = {
|
|
5
|
+
type: string | ((props: FakeProps) => FakeElement | null);
|
|
6
|
+
props: FakeProps;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type FakeProps = AnyObject & {
|
|
10
|
+
children?: FakeChild[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type Ref<T> = {
|
|
14
|
+
current: T;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type FakeChild = FakeElement | string | number | boolean | null | undefined;
|
|
18
|
+
|
|
19
|
+
type SetState<T> = (next: T | ((previous: T) => T)) => void;
|
|
20
|
+
|
|
21
|
+
/** A React without a renderer: elements are plain objects and hooks run once, as on a first render. */
|
|
22
|
+
export type FakeReact = {
|
|
23
|
+
Fragment: string;
|
|
24
|
+
createElement: (
|
|
25
|
+
type: FakeElement['type'],
|
|
26
|
+
props?: FakeProps | null,
|
|
27
|
+
...children: FakeChild[]
|
|
28
|
+
) => FakeElement;
|
|
29
|
+
useState: <T>(initial: T | (() => T)) => [T, SetState<T>];
|
|
30
|
+
useMemo: <T>(factory: () => T) => T;
|
|
31
|
+
useCallback: <T>(callback: T) => T;
|
|
32
|
+
useRef: <T>(initial: T) => Ref<T>;
|
|
33
|
+
useEffect: (effect: () => void | (() => void)) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @description Creates a React whose elements are plain objects, for tests that call components without rendering them.
|
|
38
|
+
* @returns The fake React.
|
|
39
|
+
*/
|
|
40
|
+
export function createFakeReact(): FakeReact {
|
|
41
|
+
return {
|
|
42
|
+
Fragment: 'Fragment',
|
|
43
|
+
createElement: (type, props, ...children) => ({ type, props: { ...props, children } }),
|
|
44
|
+
useState<T>(initial: T | (() => T)): [T, SetState<T>] {
|
|
45
|
+
const value = initial instanceof Function ? initial() : initial;
|
|
46
|
+
|
|
47
|
+
return [value, () => {}];
|
|
48
|
+
},
|
|
49
|
+
useMemo: (factory) => factory(),
|
|
50
|
+
useCallback: (callback) => callback,
|
|
51
|
+
useRef: (initial) => ({ current: initial }),
|
|
52
|
+
useEffect: () => {},
|
|
53
|
+
};
|
|
54
|
+
}
|