@uniflowed/react-native-testing 0.0.0-alpha.41

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/index.js ADDED
@@ -0,0 +1,554 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/react-native-testing`: query helpers for React Native test trees.
4
+ //
5
+ // The native renderer itself is still intentionally absent: `uf test` refuses a
6
+ // React Native application target rather than running it against a document.
7
+ // These helpers are the first piece around that renderer boundary. A host
8
+ // config, React Test Renderer, or app-owned harness can hand us the JSON tree it
9
+ // produced, and tests can query that tree without pretending a DOM exists.
10
+
11
+ import type * as React from "react";
12
+
13
+ import { loadTestRenderer } from "./internal/test-renderer.js";
14
+
15
+ export type NativeText = string | number;
16
+
17
+ export type NativeCheckedState = boolean | "mixed";
18
+
19
+ export type NativeAccessibilityState = {
20
+ readonly busy?: ?boolean,
21
+ readonly checked?: ?NativeCheckedState,
22
+ readonly disabled?: ?boolean,
23
+ readonly expanded?: ?boolean,
24
+ readonly selected?: ?boolean,
25
+ readonly [key: string]: mixed,
26
+ };
27
+
28
+ export type NativeAccessibilityValue = {
29
+ readonly min?: ?number,
30
+ readonly max?: ?number,
31
+ readonly now?: ?number,
32
+ readonly text?: ?string,
33
+ readonly [key: string]: mixed,
34
+ };
35
+
36
+ export type NativeAccessibilityValueMatcher = {
37
+ readonly min?: number,
38
+ readonly max?: number,
39
+ readonly now?: number,
40
+ readonly text?: NativeMatcher,
41
+ };
42
+
43
+ export type NativeProps = {
44
+ readonly accessibilityLabel?: ?string,
45
+ readonly accessibilityRole?: ?string,
46
+ readonly accessibilityState?: ?NativeAccessibilityState,
47
+ readonly accessibilityValue?: ?NativeAccessibilityValue,
48
+ readonly disabled?: ?boolean,
49
+ readonly role?: ?string,
50
+ readonly testID?: ?string,
51
+ readonly [key: string]: mixed,
52
+ };
53
+
54
+ export type NativeElement = {
55
+ readonly type: string,
56
+ readonly props?: ?NativeProps,
57
+ readonly children?: ?$ReadOnlyArray<NativeNode>,
58
+ };
59
+
60
+ export type NativeNode = null | void | boolean | NativeText | NativeElement;
61
+
62
+ export type NativeTree = NativeNode | $ReadOnlyArray<NativeNode>;
63
+
64
+ export type NativeMatcher = string | RegExp | ((value: string, node: NativeElement) => boolean);
65
+
66
+ export type NativeQueryOptions = {
67
+ readonly exact?: boolean,
68
+ };
69
+
70
+ export type NativeRoleOptions = {
71
+ readonly name?: NativeMatcher,
72
+ readonly exact?: boolean,
73
+ readonly busy?: boolean,
74
+ readonly checked?: NativeCheckedState,
75
+ readonly disabled?: boolean,
76
+ readonly expanded?: boolean,
77
+ readonly selected?: boolean,
78
+ readonly value?: NativeAccessibilityValueMatcher,
79
+ };
80
+
81
+ export type NativeQueries = {
82
+ readonly getByText: (matcher: NativeMatcher, options?: NativeQueryOptions) => NativeElement,
83
+ readonly queryByText: (
84
+ matcher: NativeMatcher,
85
+ options?: NativeQueryOptions,
86
+ ) => NativeElement | null,
87
+ readonly getAllByText: (
88
+ matcher: NativeMatcher,
89
+ options?: NativeQueryOptions,
90
+ ) => $ReadOnlyArray<NativeElement>,
91
+ readonly getByLabelText: (matcher: NativeMatcher, options?: NativeQueryOptions) => NativeElement,
92
+ readonly queryByLabelText: (
93
+ matcher: NativeMatcher,
94
+ options?: NativeQueryOptions,
95
+ ) => NativeElement | null,
96
+ readonly getAllByLabelText: (
97
+ matcher: NativeMatcher,
98
+ options?: NativeQueryOptions,
99
+ ) => $ReadOnlyArray<NativeElement>,
100
+ readonly getByRole: (role: string, options?: NativeRoleOptions) => NativeElement,
101
+ readonly queryByRole: (role: string, options?: NativeRoleOptions) => NativeElement | null,
102
+ readonly getAllByRole: (
103
+ role: string,
104
+ options?: NativeRoleOptions,
105
+ ) => $ReadOnlyArray<NativeElement>,
106
+ readonly getByTestId: (testID: string) => NativeElement,
107
+ readonly queryByTestId: (testID: string) => NativeElement | null,
108
+ readonly getAllByTestId: (testID: string) => $ReadOnlyArray<NativeElement>,
109
+ };
110
+
111
+ export type NativeRenderResult = {
112
+ ...NativeQueries,
113
+ readonly rerender: (ui: React.Node) => void,
114
+ readonly unmount: () => void,
115
+ readonly toJSON: () => NativeTree,
116
+ };
117
+
118
+ export class NativeTestingUnsupportedError extends Error {
119
+ constructor(message: string) {
120
+ super(message);
121
+ this.name = "NativeTestingUnsupportedError";
122
+ }
123
+ }
124
+
125
+ export function render(ui: React.Node): NativeRenderResult {
126
+ const testRenderer = requireTestRenderer();
127
+ installReactActEnvironment();
128
+
129
+ let renderer: ReactTestRendererInstance | null = null;
130
+ testRenderer.act(() => {
131
+ renderer = testRenderer.create(ui);
132
+ });
133
+
134
+ const currentTree = () => {
135
+ const current = renderer;
136
+ return current == null ? null : nativeTreeOf(current.toJSON());
137
+ };
138
+
139
+ return {
140
+ ...liveWithin(currentTree),
141
+ rerender: (next: React.Node) => {
142
+ const current = renderer;
143
+ if (current == null) {
144
+ throw new Error("@uniflowed/react-native-testing: cannot rerender an unmounted tree");
145
+ }
146
+ testRenderer.act(() => {
147
+ current.update(next);
148
+ });
149
+ },
150
+ unmount: () => {
151
+ const current = renderer;
152
+ if (current == null) return;
153
+ testRenderer.act(() => {
154
+ current.unmount();
155
+ });
156
+ renderer = null;
157
+ },
158
+ toJSON: currentTree,
159
+ };
160
+ }
161
+
162
+ export function createNativeScreen(root: NativeTree): NativeQueries {
163
+ return within(root);
164
+ }
165
+
166
+ export function within(root: NativeTree): NativeQueries {
167
+ return liveWithin(() => root);
168
+ }
169
+
170
+ function liveWithin(root: () => NativeTree): NativeQueries {
171
+ return {
172
+ getByText: (matcher, options) => {
173
+ rejectUnknownOptions("getByText", options, NATIVE_QUERY_OPTION_KEYS);
174
+ return one(byText(root(), matcher, options), "text", describeMatcher(matcher));
175
+ },
176
+ queryByText: (matcher, options) => {
177
+ rejectUnknownOptions("queryByText", options, NATIVE_QUERY_OPTION_KEYS);
178
+ return optional(byText(root(), matcher, options), "text", describeMatcher(matcher));
179
+ },
180
+ getAllByText: (matcher, options) => {
181
+ rejectUnknownOptions("getAllByText", options, NATIVE_QUERY_OPTION_KEYS);
182
+ return many(byText(root(), matcher, options), "text", describeMatcher(matcher));
183
+ },
184
+ getByLabelText: (matcher, options) => {
185
+ rejectUnknownOptions("getByLabelText", options, NATIVE_QUERY_OPTION_KEYS);
186
+ return one(byLabelText(root(), matcher, options), "label", describeMatcher(matcher));
187
+ },
188
+ queryByLabelText: (matcher, options) => {
189
+ rejectUnknownOptions("queryByLabelText", options, NATIVE_QUERY_OPTION_KEYS);
190
+ return optional(byLabelText(root(), matcher, options), "label", describeMatcher(matcher));
191
+ },
192
+ getAllByLabelText: (matcher, options) => {
193
+ rejectUnknownOptions("getAllByLabelText", options, NATIVE_QUERY_OPTION_KEYS);
194
+ return many(byLabelText(root(), matcher, options), "label", describeMatcher(matcher));
195
+ },
196
+ getByRole: (role, options) => {
197
+ rejectUnknownRoleOptions("getByRole", options);
198
+ return one(byRole(root(), role, options), "role", role);
199
+ },
200
+ queryByRole: (role, options) => {
201
+ rejectUnknownRoleOptions("queryByRole", options);
202
+ return optional(byRole(root(), role, options), "role", role);
203
+ },
204
+ getAllByRole: (role, options) => {
205
+ rejectUnknownRoleOptions("getAllByRole", options);
206
+ return many(byRole(root(), role, options), "role", role);
207
+ },
208
+ getByTestId: (testID) => one(byTestId(root(), testID), "testID", testID),
209
+ queryByTestId: (testID) => optional(byTestId(root(), testID), "testID", testID),
210
+ getAllByTestId: (testID) => many(byTestId(root(), testID), "testID", testID),
211
+ };
212
+ }
213
+
214
+ const NATIVE_QUERY_OPTION_KEYS: $ReadOnlyArray<string> = ["exact"];
215
+ const NATIVE_ROLE_OPTION_KEYS: $ReadOnlyArray<string> = [
216
+ "busy",
217
+ "checked",
218
+ "disabled",
219
+ "exact",
220
+ "expanded",
221
+ "name",
222
+ "selected",
223
+ "value",
224
+ ];
225
+ const NATIVE_ROLE_VALUE_OPTION_KEYS: $ReadOnlyArray<string> = ["max", "min", "now", "text"];
226
+
227
+ type ReactTestJSONNode =
228
+ | string
229
+ | {
230
+ readonly type: string,
231
+ readonly props: NativeProps,
232
+ readonly children?: null | $ReadOnlyArray<ReactTestJSONNode>,
233
+ };
234
+
235
+ type ReactTestJSONTree = null | ReactTestJSONNode | $ReadOnlyArray<ReactTestJSONNode>;
236
+
237
+ type ReactTestRendererInstance = {|
238
+ readonly toJSON: () => ReactTestJSONTree,
239
+ update(ui: React.Node): void,
240
+ unmount(): void,
241
+ |};
242
+
243
+ type ReactTestRendererModule = {|
244
+ readonly act: <T>(() => T) => T,
245
+ readonly create: (ui: React.Node) => ReactTestRendererInstance,
246
+ |};
247
+
248
+ let testRendererModule: ReactTestRendererModule | null = null;
249
+
250
+ function requireTestRenderer(): ReactTestRendererModule {
251
+ if (testRendererModule == null) {
252
+ try {
253
+ testRendererModule = assertTestRendererModule(loadTestRenderer());
254
+ } catch (error) {
255
+ if (isMissingTestRenderer(error)) {
256
+ throw new NativeTestingUnsupportedError(
257
+ "@uniflowed/react-native-testing render() needs the optional peer " +
258
+ "`react-test-renderer`. Install matching `react` and " +
259
+ "`react-test-renderer` releases together, or pass an existing native test tree to " +
260
+ "createNativeScreen().",
261
+ );
262
+ }
263
+ throw error;
264
+ }
265
+ }
266
+ return testRendererModule;
267
+ }
268
+
269
+ function assertTestRendererModule(value: mixed): ReactTestRendererModule {
270
+ if (value == null || typeof value !== "object") {
271
+ throw new NativeTestingUnsupportedError(
272
+ "@uniflowed/react-native-testing expected react-test-renderer to export create() and act().",
273
+ );
274
+ }
275
+ const module: $FlowFixMe = value;
276
+ if (typeof module.create !== "function" || typeof module.act !== "function") {
277
+ throw new NativeTestingUnsupportedError(
278
+ "@uniflowed/react-native-testing expected react-test-renderer to export create() and act().",
279
+ );
280
+ }
281
+ return module;
282
+ }
283
+
284
+ function isMissingTestRenderer(error: mixed): boolean {
285
+ if (error == null || typeof error !== "object") return false;
286
+ const moduleError: $FlowFixMe = error;
287
+ return (
288
+ moduleError.code === "MODULE_NOT_FOUND" &&
289
+ typeof moduleError.message === "string" &&
290
+ moduleError.message.includes("react-test-renderer")
291
+ );
292
+ }
293
+
294
+ function installReactActEnvironment(): void {
295
+ const global: $FlowFixMe = globalThis;
296
+ global.IS_REACT_ACT_ENVIRONMENT = true;
297
+ }
298
+
299
+ function nativeTreeOf(tree: ReactTestJSONTree): NativeTree {
300
+ if (Array.isArray(tree)) {
301
+ return tree.map((child) => nativeNodeOf(child));
302
+ }
303
+ return nativeNodeOf(tree);
304
+ }
305
+
306
+ function nativeNodeOf(node: null | ReactTestJSONNode): NativeNode {
307
+ if (node == null || typeof node === "string") {
308
+ return node;
309
+ }
310
+ return {
311
+ type: node.type,
312
+ props: node.props,
313
+ children: node.children == null ? [] : node.children.map((child) => nativeNodeOf(child)),
314
+ };
315
+ }
316
+
317
+ export function textContent(node: NativeTree): string {
318
+ if (Array.isArray(node)) return node.map((child) => textContent(child)).join("");
319
+ if (typeof node === "string" || typeof node === "number") return String(node);
320
+ const element = elementOf(node);
321
+ if (element == null) return "";
322
+ return (element.children ?? []).map((child) => textContent(child)).join("");
323
+ }
324
+
325
+ export function accessibleName(node: NativeElement): string {
326
+ const label = node.props?.accessibilityLabel;
327
+ return typeof label === "string" ? label : textContent(node);
328
+ }
329
+
330
+ export function roleOf(node: NativeElement): string | null {
331
+ const explicit = node.props?.accessibilityRole ?? node.props?.role;
332
+ if (typeof explicit === "string" && explicit !== "") return explicit;
333
+ switch (node.type) {
334
+ case "Button":
335
+ case "Pressable":
336
+ return "button";
337
+ case "Image":
338
+ return "image";
339
+ case "Switch":
340
+ return "switch";
341
+ case "TextInput":
342
+ return "textbox";
343
+ case "Text":
344
+ return "text";
345
+ default:
346
+ return null;
347
+ }
348
+ }
349
+
350
+ export function accessibilityStateOf(node: NativeElement): NativeAccessibilityState {
351
+ const state = node.props?.accessibilityState ?? {};
352
+ const disabled = state.disabled ?? node.props?.disabled;
353
+ return {
354
+ busy: state.busy ?? false,
355
+ checked: state.checked ?? false,
356
+ disabled: disabled ?? false,
357
+ expanded: state.expanded ?? false,
358
+ selected: state.selected ?? false,
359
+ };
360
+ }
361
+
362
+ export function accessibilityValueOf(node: NativeElement): NativeAccessibilityValue {
363
+ const value = node.props?.accessibilityValue ?? {};
364
+ return {
365
+ min: value.min ?? null,
366
+ max: value.max ?? null,
367
+ now: value.now ?? null,
368
+ text: value.text ?? null,
369
+ };
370
+ }
371
+
372
+ function rejectUnknownOptions(query: string, options: mixed, known: $ReadOnlyArray<string>): void {
373
+ if (options == null) return;
374
+ if (typeof options !== "object") {
375
+ throw new Error(`${query}: the options are ${String(options)}, and an object was expected`);
376
+ }
377
+ for (const key of Object.keys(options)) {
378
+ if (!known.includes(key)) {
379
+ throw new Error(
380
+ `${query}: "${key}" is not an option this query takes. It takes ${known.join(", ")}.`,
381
+ );
382
+ }
383
+ }
384
+ }
385
+
386
+ function rejectUnknownRoleOptions(query: string, options: mixed): void {
387
+ rejectUnknownOptions(query, options, NATIVE_ROLE_OPTION_KEYS);
388
+ if (options == null || typeof options !== "object") return;
389
+ const roleOptions: $FlowFixMe = options;
390
+ if (Array.isArray(roleOptions.value)) {
391
+ throw new Error(`${query}.value: an object was expected`);
392
+ }
393
+ rejectUnknownOptions(`${query}.value`, roleOptions.value, NATIVE_ROLE_VALUE_OPTION_KEYS);
394
+ }
395
+
396
+ function byText(
397
+ root: NativeTree,
398
+ matcher: NativeMatcher,
399
+ options?: NativeQueryOptions,
400
+ ): $ReadOnlyArray<NativeElement> {
401
+ return allElements(root).filter((node) => {
402
+ const value = textContent(node);
403
+ if (value === "") return false;
404
+ if (!matches(matcher, value, node, options?.exact ?? true)) return false;
405
+ return !(node.children ?? []).some((child) => {
406
+ const element = elementOf(child);
407
+ return (
408
+ element != null && matches(matcher, textContent(element), element, options?.exact ?? true)
409
+ );
410
+ });
411
+ });
412
+ }
413
+
414
+ function byLabelText(
415
+ root: NativeTree,
416
+ matcher: NativeMatcher,
417
+ options?: NativeQueryOptions,
418
+ ): $ReadOnlyArray<NativeElement> {
419
+ const exact = options?.exact ?? true;
420
+ const found: Array<NativeElement> = [];
421
+ const visit = (node: NativeTree, insideMatch: boolean) => {
422
+ if (Array.isArray(node)) {
423
+ for (const child of node) visit(child, insideMatch);
424
+ return;
425
+ }
426
+ const element = elementOf(node);
427
+ if (element == null) return;
428
+ const value = accessibleName(element);
429
+ const matched = value !== "" && matches(matcher, value, element, exact);
430
+ const queryable = hasExplicitLabel(element) || roleOf(element) != null;
431
+ const current = matched && queryable && !insideMatch;
432
+ if (current) {
433
+ found.push(element);
434
+ }
435
+ for (const child of element.children ?? []) visit(child, insideMatch || current);
436
+ };
437
+ visit(root, false);
438
+ return found;
439
+ }
440
+
441
+ function byRole(
442
+ root: NativeTree,
443
+ role: string,
444
+ options?: NativeRoleOptions,
445
+ ): $ReadOnlyArray<NativeElement> {
446
+ return allElements(root).filter((node) => {
447
+ if (roleOf(node) !== role) return false;
448
+ if (!stateMatches(node, options)) return false;
449
+ if (!valueMatches(node, options?.value)) return false;
450
+ return options?.name == null
451
+ ? true
452
+ : matches(options.name, accessibleName(node), node, options.exact ?? true);
453
+ });
454
+ }
455
+
456
+ function byTestId(root: NativeTree, testID: string): $ReadOnlyArray<NativeElement> {
457
+ return allElements(root).filter((node) => node.props?.testID === testID);
458
+ }
459
+
460
+ function hasExplicitLabel(node: NativeElement): boolean {
461
+ const label = node.props?.accessibilityLabel;
462
+ return typeof label === "string" && label !== "";
463
+ }
464
+
465
+ function allElements(root: NativeTree): Array<NativeElement> {
466
+ const found: Array<NativeElement> = [];
467
+ const visit = (node: NativeTree) => {
468
+ if (Array.isArray(node)) {
469
+ for (const child of node) visit(child);
470
+ return;
471
+ }
472
+ const element = elementOf(node);
473
+ if (element == null) return;
474
+ found.push(element);
475
+ for (const child of element.children ?? []) visit(child);
476
+ };
477
+ visit(root);
478
+ return found;
479
+ }
480
+
481
+ function elementOf(node: NativeNode): NativeElement | null {
482
+ if (node === null || typeof node !== "object") return null;
483
+ const candidate: $FlowFixMe = node;
484
+ return typeof candidate.type === "string" ? candidate : null;
485
+ }
486
+
487
+ function matches(
488
+ matcher: NativeMatcher,
489
+ value: string,
490
+ node: NativeElement,
491
+ exact: boolean,
492
+ ): boolean {
493
+ if (typeof matcher === "function") return matcher(value, node);
494
+ if (matcher instanceof RegExp) return matcher.test(value);
495
+ return exact ? value === matcher : value.toLowerCase().includes(matcher.toLowerCase());
496
+ }
497
+
498
+ function stateMatches(node: NativeElement, options?: NativeRoleOptions): boolean {
499
+ if (options == null) return true;
500
+ const state = accessibilityStateOf(node);
501
+ return (
502
+ (options.busy == null || state.busy === options.busy) &&
503
+ (options.checked == null || state.checked === options.checked) &&
504
+ (options.disabled == null || state.disabled === options.disabled) &&
505
+ (options.expanded == null || state.expanded === options.expanded) &&
506
+ (options.selected == null || state.selected === options.selected)
507
+ );
508
+ }
509
+
510
+ function valueMatches(node: NativeElement, expected?: NativeAccessibilityValueMatcher): boolean {
511
+ if (expected == null) return true;
512
+ const value = accessibilityValueOf(node);
513
+ return (
514
+ (expected.min == null || value.min === expected.min) &&
515
+ (expected.max == null || value.max === expected.max) &&
516
+ (expected.now == null || value.now === expected.now) &&
517
+ (expected.text == null ||
518
+ (value.text != null && matches(expected.text, value.text, node, true)))
519
+ );
520
+ }
521
+
522
+ function one(values: $ReadOnlyArray<NativeElement>, kind: string, label: string): NativeElement {
523
+ if (values.length === 1) return values[0];
524
+ if (values.length === 0) {
525
+ throw new Error(`@uniflowed/react-native-testing: found nothing for ${kind} ${label}`);
526
+ }
527
+ throw new Error(
528
+ `@uniflowed/react-native-testing: found ${String(values.length)} nodes for ${kind} ${label}`,
529
+ );
530
+ }
531
+
532
+ function optional(
533
+ values: $ReadOnlyArray<NativeElement>,
534
+ kind: string,
535
+ label: string,
536
+ ): NativeElement | null {
537
+ if (values.length === 0) return null;
538
+ return one(values, kind, label);
539
+ }
540
+
541
+ function many(
542
+ values: $ReadOnlyArray<NativeElement>,
543
+ kind: string,
544
+ label: string,
545
+ ): $ReadOnlyArray<NativeElement> {
546
+ if (values.length > 0) return values;
547
+ throw new Error(`@uniflowed/react-native-testing: found nothing for ${kind} ${label}`);
548
+ }
549
+
550
+ function describeMatcher(matcher: NativeMatcher): string {
551
+ if (typeof matcher === "string") return matcher;
552
+ if (matcher instanceof RegExp) return String(matcher);
553
+ return "<predicate>";
554
+ }
@@ -0,0 +1,9 @@
1
+ // @flow
2
+ //
3
+ // Browser replacement for the Node-only optional renderer loader.
4
+
5
+ export function loadTestRenderer(): empty {
6
+ const error: $FlowFixMe = new Error("Cannot find module 'react-test-renderer'");
7
+ error.code = "MODULE_NOT_FOUND";
8
+ throw error;
9
+ }
@@ -0,0 +1,10 @@
1
+ // @flow
2
+ //
3
+ // Node-only loader for the optional React Test Renderer peer.
4
+
5
+ import { createRequire } from "node:module";
6
+
7
+ export function loadTestRenderer(): mixed {
8
+ const load = createRequire(import.meta.url);
9
+ return load("react-test-renderer");
10
+ }
package/native.js ADDED
@@ -0,0 +1,10 @@
1
+ // @flow
2
+ // The app owns the Testing Library and renderer versions. v14 uses the
3
+ // successor Test Renderer; its async API is re-exported without emulation.
4
+ export * from "@testing-library/react-native";
5
+ export type * from "@testing-library/react-native";
6
+ import { fireEvent } from "@testing-library/react-native";
7
+
8
+ export const press = fireEvent.press;
9
+ export const changeText = fireEvent.changeText;
10
+ export const scroll = fireEvent.scroll;
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@uniflowed/react-native-testing",
3
+ "version": "0.0.0-alpha.41",
4
+ "description": "Native component tests and renderer-independent tree queries for Flow.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ubugeeei-prod/uf.git",
11
+ "directory": "packages/react-native-testing"
12
+ },
13
+ "exports": {
14
+ ".": "./index.js",
15
+ "./native": "./native.js"
16
+ },
17
+ "browser": {
18
+ "./internal/test-renderer.js": "./internal/browser/test-renderer.js"
19
+ },
20
+ "files": [
21
+ "index.js",
22
+ "internal",
23
+ "native.js",
24
+ "!*.test.js"
25
+ ],
26
+ "peerDependencies": {
27
+ "react": ">=19.2.3",
28
+ "react-native": ">=0.86",
29
+ "react-test-renderer": ">=19.2.3",
30
+ "@testing-library/react-native": "^14.0.0"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "react-native": {
34
+ "optional": true
35
+ },
36
+ "react-test-renderer": {
37
+ "optional": true
38
+ },
39
+ "@testing-library/react-native": {
40
+ "optional": true
41
+ }
42
+ }
43
+ }