@ng-native/testing 0.3.0 → 0.4.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.
Files changed (41) hide show
  1. package/dist/compile-css.d.ts +17 -0
  2. package/dist/compile-css.js +14 -0
  3. package/dist/compile-css.js.map +1 -0
  4. package/dist/events.d.ts +9 -0
  5. package/dist/events.js +14 -0
  6. package/dist/events.js.map +1 -1
  7. package/dist/gesture-handler.d.ts +3 -39
  8. package/dist/gesture-handler.js +2 -41
  9. package/dist/gesture-handler.js.map +1 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/inject-service.d.ts +11 -2
  14. package/dist/inject-service.js +26 -6
  15. package/dist/inject-service.js.map +1 -1
  16. package/dist/memory-database.d.ts +23 -0
  17. package/dist/memory-database.js +60 -0
  18. package/dist/memory-database.js.map +1 -0
  19. package/dist/queries.d.ts +5 -0
  20. package/dist/queries.js +21 -3
  21. package/dist/queries.js.map +1 -1
  22. package/dist/reanimated-library.d.ts +5 -59
  23. package/dist/reanimated-library.js +5 -92
  24. package/dist/reanimated-library.js.map +1 -1
  25. package/dist/reanimated.d.ts +3 -0
  26. package/dist/reanimated.js +24 -7
  27. package/dist/reanimated.js.map +1 -1
  28. package/dist/render.js +49 -4
  29. package/dist/render.js.map +1 -1
  30. package/dist/test-utils.d.ts +6 -0
  31. package/dist/test-utils.js +6 -4
  32. package/dist/test-utils.js.map +1 -1
  33. package/dist/worklets-library.d.ts +2 -10
  34. package/dist/worklets-library.js +2 -22
  35. package/dist/worklets-library.js.map +1 -1
  36. package/package.json +8 -7
  37. package/runner/compile.mjs +10 -1
  38. package/runner/register.mjs +14 -0
  39. package/runner/setup.mjs +13 -0
  40. package/runner/vitest.d.mts +6 -0
  41. package/runner/vitest.mjs +22 -14
@@ -0,0 +1,17 @@
1
+ import type { StyleSheet } from '@ng-native/fabric';
2
+ export interface CompileCssOptions {
3
+ /**
4
+ * Told of each declaration or rule native cannot express, which is then left out. With none, the
5
+ * first one throws, so a test never passes on a sheet that quietly lost a rule.
6
+ */
7
+ readonly onUnsupported?: (message: string) => void;
8
+ }
9
+ /**
10
+ * A stylesheet compiled as the build compiles an app's, for `render()`'s `globalStyles`:
11
+ * `render(Badge, { globalStyles: compileCss(':root { --brand: red }') })`.
12
+ *
13
+ * The compiler is `@ng-native/metro`'s, which this package depends on and a test file in a
14
+ * workspace library cannot always resolve for itself. Loaded when called, and through Node's own
15
+ * `require`: it is CommonJS, and nothing but a test needs it.
16
+ */
17
+ export declare function compileCss(css: string, name?: string, options?: CompileCssOptions): StyleSheet;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A stylesheet compiled as the build compiles an app's, for `render()`'s `globalStyles`:
3
+ * `render(Badge, { globalStyles: compileCss(':root { --brand: red }') })`.
4
+ *
5
+ * The compiler is `@ng-native/metro`'s, which this package depends on and a test file in a
6
+ * workspace library cannot always resolve for itself. Loaded when called, and through Node's own
7
+ * `require`: it is CommonJS, and nothing but a test needs it.
8
+ */
9
+ export function compileCss(css, name = 'test', options = {}) {
10
+ const { createRequire } = process.getBuiltinModule('node:module');
11
+ const compiler = createRequire(import.meta.url)('@ng-native/metro/css/compile.cjs');
12
+ return compiler.compileCss(css, name, options);
13
+ }
14
+ //# sourceMappingURL=compile-css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-css.js","sourceRoot":"","sources":["../src/compile-css.ts"],"names":[],"mappings":"AAUA;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CACxB,GAAW,EACX,IAAI,GAAG,MAAM,EACb,UAA6B,EAAE;IAE/B,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,kCAAkC,CAEjF,CAAC;IACF,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC","sourcesContent":["import type { StyleSheet } from '@ng-native/fabric';\n\nexport interface CompileCssOptions {\n /**\n * Told of each declaration or rule native cannot express, which is then left out. With none, the\n * first one throws, so a test never passes on a sheet that quietly lost a rule.\n */\n readonly onUnsupported?: (message: string) => void;\n}\n\n/**\n * A stylesheet compiled as the build compiles an app's, for `render()`'s `globalStyles`:\n * `render(Badge, { globalStyles: compileCss(':root { --brand: red }') })`.\n *\n * The compiler is `@ng-native/metro`'s, which this package depends on and a test file in a\n * workspace library cannot always resolve for itself. Loaded when called, and through Node's own\n * `require`: it is CommonJS, and nothing but a test needs it.\n */\nexport function compileCss(\n css: string,\n name = 'test',\n options: CompileCssOptions = {},\n): StyleSheet {\n const { createRequire } = process.getBuiltinModule('node:module');\n const compiler = createRequire(import.meta.url)('@ng-native/metro/css/compile.cjs') as {\n compileCss(css: string, name: string, options: CompileCssOptions): StyleSheet;\n };\n return compiler.compileCss(css, name, options);\n}\n"]}
package/dist/events.d.ts CHANGED
@@ -8,7 +8,16 @@ declare const named: {
8
8
  scroll(node: FakeFabricNode, payload?: EventPayload): Promise<void>;
9
9
  focus(node: FakeFabricNode): Promise<void>;
10
10
  blur(node: FakeFabricNode): Promise<void>;
11
+ /** A `(layout)` event with the frame native would report: `{ x, y, width, height }`. */
12
+ layout(node: FakeFabricNode, frame?: Partial<LayoutFrame>): Promise<void>;
11
13
  };
14
+ /** Where a view is in its parent, and its size, as `(layout)` reports them. */
15
+ export interface LayoutFrame {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ }
12
21
  type FireEvent = ((node: FakeFabricNode, name: string, payload?: unknown) => Promise<void>) & typeof named;
13
22
  /**
14
23
  * `fireEvent(node, 'press')`, `fireEvent(node, 'changeText', 'Ada')`, or any other event by name -
package/dist/events.js CHANGED
@@ -72,7 +72,18 @@ const named = {
72
72
  emit(node, 'topBlur');
73
73
  await settle();
74
74
  },
75
+ /** A `(layout)` event with the frame native would report: `{ x, y, width, height }`. */
76
+ async layout(node, frame = {}) {
77
+ emit(node, 'topLayout', { layout: { x: 0, y: 0, width: 0, height: 0, ...frame } });
78
+ await settle();
79
+ },
75
80
  };
81
+ /** The frame in a layout payload, written bare, as `{ layout }`, or as `{ nativeEvent: { layout } }`. */
82
+ function frameOf(payload) {
83
+ const given = (payload ?? {});
84
+ const event = (given.nativeEvent ?? given);
85
+ return event.layout ?? event;
86
+ }
76
87
  /**
77
88
  * `fireEvent(node, 'press')`, `fireEvent(node, 'changeText', 'Ada')`, or any other event by name -
78
89
  * `fireEvent(node, 'submitEditing', { text })` emits `topSubmitEditing`.
@@ -83,6 +94,9 @@ export const fireEvent = Object.assign(async (node, name, payload) => {
83
94
  if (name === 'press' || name === 'scroll' || name === 'focus' || name === 'blur') {
84
95
  return named[name](node, payload);
85
96
  }
97
+ // A layout by name takes the frame, or the event wrapped as `fireEvent.scroll` takes one.
98
+ if (name === 'layout')
99
+ return named.layout(node, frameOf(payload));
86
100
  emit(node, topLevel(name), nativeOf(payload));
87
101
  await settle();
88
102
  }, named);
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `fireEvent` and `userEvent`, as React Native Testing Library names them, emitting the Fabric\n * events the native side would.\n *\n * Each goes through `fabric.emit`, which is the handler the renderer registered with Fabric, so an\n * event takes the real route: the engine's responder negotiation for a touch, its bubbling for the\n * rest, then the component's listener. Nothing here calls a handler directly, and a disabled\n * pressable ignores a press here for the same reason it does on a phone.\n *\n * Both are async where RNTL's `fireEvent` is not. Change detection is zoneless and commits on a\n * later task, so an event has to be awaited before the tree it changed can be read.\n */\nimport { currentOf, ownerOf, settle } from './render.ts';\nimport { flatten, isTextInput } from './queries.ts';\nimport type { FakeFabricNode } from './test-utils.ts';\n\n/** A payload as a handler would see it (`{ nativeEvent }`), or the native event itself. */\nexport type EventPayload = Record<string, unknown>;\n\nconst nativeOf = (payload: EventPayload | undefined): Record<string, unknown> =>\n ((payload && 'nativeEvent' in payload ? payload['nativeEvent'] : payload) as Record<\n string,\n unknown\n >) ?? {};\n\nfunction emit(node: FakeFabricNode, type: string, nativeEvent: Record<string, unknown> = {}): void {\n ownerOf(node).fabric.emit(node, type, { target: node.reactTag, ...nativeEvent });\n}\n\n/** `press` -> `topPress`: the naming the renderer uses for `(touchEnd)` and every other event. */\nconst topLevel = (name: string): string =>\n name.startsWith('top') ? name : `top${name[0]!.toUpperCase()}${name.slice(1)}`;\n\n/** One finger, as Fabric reports it. The press machine reads `pageX`/`pageY`. */\nfunction touch(node: FakeFabricNode, phase: 'start' | 'end'): Record<string, unknown> {\n const point = {\n identifier: 0,\n target: node.reactTag,\n pageX: 0,\n pageY: 0,\n locationX: 0,\n locationY: 0,\n timestamp: Date.now(),\n };\n return { ...point, touches: phase === 'start' ? [point] : [], changedTouches: [point] };\n}\n\n/** The navigation bar's search field, which reports text as `topChangeText` with no count. */\nconst isSearchBar = (node: FakeFabricNode): boolean => node.viewName === 'RNSSearchBar';\n\n/** The text field at or under a node, so a query for its wrapper still reaches the input. */\nfunction fieldOf(node: FakeFabricNode): FakeFabricNode {\n const current = currentOf(node) ?? node;\n const field = flatten([current]).find((n) => isTextInput(n) || isSearchBar(n));\n if (!field) throw new Error(`No TextInput or search bar at or under this ${node.viewName}.`);\n return field;\n}\n\n/** The count native would send next: one past the last one JS echoed back. */\nconst nextCount = (field: FakeFabricNode): number =>\n Number((currentOf(field) ?? field).props['mostRecentEventCount'] ?? 0) + 1;\n\nfunction changeText(field: FakeFabricNode, text: string): void {\n if (isSearchBar(field)) emit(field, 'topChangeText', { text });\n else emit(field, 'topChange', { text, eventCount: nextCount(field) });\n}\n\nasync function press(node: FakeFabricNode): Promise<void> {\n emit(node, 'topTouchStart', touch(node, 'start'));\n emit(node, 'topTouchEnd', touch(node, 'end'));\n await settle();\n}\n\nconst named = {\n press,\n async changeText(node: FakeFabricNode, text: string): Promise<void> {\n changeText(fieldOf(node), text);\n await settle();\n },\n async scroll(node: FakeFabricNode, payload?: EventPayload): Promise<void> {\n emit(node, 'topScroll', nativeOf(payload));\n await settle();\n },\n async focus(node: FakeFabricNode): Promise<void> {\n emit(node, 'topFocus');\n await settle();\n },\n async blur(node: FakeFabricNode): Promise<void> {\n emit(node, 'topBlur');\n await settle();\n },\n};\n\ntype FireEvent = ((node: FakeFabricNode, name: string, payload?: unknown) => Promise<void>) &\n typeof named;\n\n/**\n * `fireEvent(node, 'press')`, `fireEvent(node, 'changeText', 'Ada')`, or any other event by name -\n * `fireEvent(node, 'submitEditing', { text })` emits `topSubmitEditing`.\n */\nexport const fireEvent: FireEvent = Object.assign(\n async (node: FakeFabricNode, name: string, payload?: unknown): Promise<void> => {\n if (name === 'changeText') return named.changeText(node, String(payload));\n if (name === 'press' || name === 'scroll' || name === 'focus' || name === 'blur') {\n return named[name](node, payload as EventPayload);\n }\n emit(node, topLevel(name), nativeOf(payload as EventPayload));\n await settle();\n },\n named,\n);\n\n/** Options for `userEvent.type`, named as React Native Testing Library names them. */\nexport interface TypeOptions {\n /** Leave the field focused afterwards. By default typing ends with the field blurring. */\n skipBlur?: boolean;\n /** Press return at the end: `topSubmitEditing`, before any blur. */\n submitEditing?: boolean;\n}\n\n/**\n * Interactions as a person performs them: several events, a task apart, in the order the platform\n * sends them. Slower than `fireEvent`, and closer to what a device does.\n */\nexport interface UserEvent {\n /** A finger down and up, a task apart, through the responder. */\n press(node: FakeFabricNode): Promise<void>;\n /** A finger held for `duration` ms (500, the long-press delay, by default). */\n longPress(node: FakeFabricNode, options?: { duration?: number }): Promise<void>;\n /** Focus the field, then one change per character, each with the next `eventCount`. */\n type(node: FakeFabricNode, text: string, options?: TypeOptions): Promise<void>;\n /** Focus the field, empty it, and blur. */\n clear(node: FakeFabricNode): Promise<void>;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nasync function hold(node: FakeFabricNode, duration: number): Promise<void> {\n emit(node, 'topTouchStart', touch(node, 'start'));\n await settle();\n if (duration) await sleep(duration);\n emit(node, 'topTouchEnd', touch(node, 'end'));\n await settle();\n}\n\nasync function endEditing(field: FakeFabricNode, text: string): Promise<void> {\n emit(field, 'topEndEditing', { text });\n emit(field, 'topBlur');\n await settle();\n}\n\nconst user: UserEvent = {\n press: (node) => hold(node, 0),\n longPress: (node, { duration = 500 } = {}) => hold(node, duration),\n async type(node, text, options = {}) {\n const field = fieldOf(node);\n if (field.props['editable'] === false) return;\n emit(field, 'topFocus');\n await settle();\n const maxLength = field.props['maxLength'];\n const limit = typeof maxLength === 'number' ? maxLength : Infinity;\n let value = String((currentOf(field) ?? field).props['text'] ?? '');\n for (const key of text) {\n // `maxLength` is enforced natively: the field never calls back with text past it, so a key\n // pressed once the limit is reached is swallowed the same way it would be on a device.\n if (value.length >= limit) continue;\n value += key;\n emit(field, 'topKeyPress', { key });\n changeText(field, value);\n await settle();\n }\n if (options.submitEditing) emit(field, 'topSubmitEditing', { text: value });\n if (!options.skipBlur) await endEditing(field, value);\n else await settle();\n },\n async clear(node) {\n const field = fieldOf(node);\n if (field.props['editable'] === false) return;\n emit(field, 'topFocus');\n changeText(field, '');\n await settle();\n await endEditing(field, '');\n },\n};\n\n/** RNTL's `userEvent`: `userEvent.setup()` for an instance, or the same calls directly. */\nexport const userEvent: UserEvent & { setup(): UserEvent } = { ...user, setup: () => user };\n"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `fireEvent` and `userEvent`, as React Native Testing Library names them, emitting the Fabric\n * events the native side would.\n *\n * Each goes through `fabric.emit`, which is the handler the renderer registered with Fabric, so an\n * event takes the real route: the engine's responder negotiation for a touch, its bubbling for the\n * rest, then the component's listener. Nothing here calls a handler directly, and a disabled\n * pressable ignores a press here for the same reason it does on a phone.\n *\n * Both are async where RNTL's `fireEvent` is not. Change detection is zoneless and commits on a\n * later task, so an event has to be awaited before the tree it changed can be read.\n */\nimport { currentOf, ownerOf, settle } from './render.ts';\nimport { flatten, isTextInput } from './queries.ts';\nimport type { FakeFabricNode } from './test-utils.ts';\n\n/** A payload as a handler would see it (`{ nativeEvent }`), or the native event itself. */\nexport type EventPayload = Record<string, unknown>;\n\nconst nativeOf = (payload: EventPayload | undefined): Record<string, unknown> =>\n ((payload && 'nativeEvent' in payload ? payload['nativeEvent'] : payload) as Record<\n string,\n unknown\n >) ?? {};\n\nfunction emit(node: FakeFabricNode, type: string, nativeEvent: Record<string, unknown> = {}): void {\n ownerOf(node).fabric.emit(node, type, { target: node.reactTag, ...nativeEvent });\n}\n\n/** `press` -> `topPress`: the naming the renderer uses for `(touchEnd)` and every other event. */\nconst topLevel = (name: string): string =>\n name.startsWith('top') ? name : `top${name[0]!.toUpperCase()}${name.slice(1)}`;\n\n/** One finger, as Fabric reports it. The press machine reads `pageX`/`pageY`. */\nfunction touch(node: FakeFabricNode, phase: 'start' | 'end'): Record<string, unknown> {\n const point = {\n identifier: 0,\n target: node.reactTag,\n pageX: 0,\n pageY: 0,\n locationX: 0,\n locationY: 0,\n timestamp: Date.now(),\n };\n return { ...point, touches: phase === 'start' ? [point] : [], changedTouches: [point] };\n}\n\n/** The navigation bar's search field, which reports text as `topChangeText` with no count. */\nconst isSearchBar = (node: FakeFabricNode): boolean => node.viewName === 'RNSSearchBar';\n\n/** The text field at or under a node, so a query for its wrapper still reaches the input. */\nfunction fieldOf(node: FakeFabricNode): FakeFabricNode {\n const current = currentOf(node) ?? node;\n const field = flatten([current]).find((n) => isTextInput(n) || isSearchBar(n));\n if (!field) throw new Error(`No TextInput or search bar at or under this ${node.viewName}.`);\n return field;\n}\n\n/** The count native would send next: one past the last one JS echoed back. */\nconst nextCount = (field: FakeFabricNode): number =>\n Number((currentOf(field) ?? field).props['mostRecentEventCount'] ?? 0) + 1;\n\nfunction changeText(field: FakeFabricNode, text: string): void {\n if (isSearchBar(field)) emit(field, 'topChangeText', { text });\n else emit(field, 'topChange', { text, eventCount: nextCount(field) });\n}\n\nasync function press(node: FakeFabricNode): Promise<void> {\n emit(node, 'topTouchStart', touch(node, 'start'));\n emit(node, 'topTouchEnd', touch(node, 'end'));\n await settle();\n}\n\nconst named = {\n press,\n async changeText(node: FakeFabricNode, text: string): Promise<void> {\n changeText(fieldOf(node), text);\n await settle();\n },\n async scroll(node: FakeFabricNode, payload?: EventPayload): Promise<void> {\n emit(node, 'topScroll', nativeOf(payload));\n await settle();\n },\n async focus(node: FakeFabricNode): Promise<void> {\n emit(node, 'topFocus');\n await settle();\n },\n async blur(node: FakeFabricNode): Promise<void> {\n emit(node, 'topBlur');\n await settle();\n },\n /** A `(layout)` event with the frame native would report: `{ x, y, width, height }`. */\n async layout(node: FakeFabricNode, frame: Partial<LayoutFrame> = {}): Promise<void> {\n emit(node, 'topLayout', { layout: { x: 0, y: 0, width: 0, height: 0, ...frame } });\n await settle();\n },\n};\n\n/** Where a view is in its parent, and its size, as `(layout)` reports them. */\nexport interface LayoutFrame {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** The frame in a layout payload, written bare, as `{ layout }`, or as `{ nativeEvent: { layout } }`. */\nfunction frameOf(payload: unknown): Partial<LayoutFrame> {\n const given = (payload ?? {}) as { nativeEvent?: unknown; layout?: Partial<LayoutFrame> };\n const event = (given.nativeEvent ?? given) as { layout?: Partial<LayoutFrame> };\n return event.layout ?? (event as Partial<LayoutFrame>);\n}\n\ntype FireEvent = ((node: FakeFabricNode, name: string, payload?: unknown) => Promise<void>) &\n typeof named;\n\n/**\n * `fireEvent(node, 'press')`, `fireEvent(node, 'changeText', 'Ada')`, or any other event by name -\n * `fireEvent(node, 'submitEditing', { text })` emits `topSubmitEditing`.\n */\nexport const fireEvent: FireEvent = Object.assign(\n async (node: FakeFabricNode, name: string, payload?: unknown): Promise<void> => {\n if (name === 'changeText') return named.changeText(node, String(payload));\n if (name === 'press' || name === 'scroll' || name === 'focus' || name === 'blur') {\n return named[name](node, payload as EventPayload);\n }\n // A layout by name takes the frame, or the event wrapped as `fireEvent.scroll` takes one.\n if (name === 'layout') return named.layout(node, frameOf(payload));\n emit(node, topLevel(name), nativeOf(payload as EventPayload));\n await settle();\n },\n named,\n);\n\n/** Options for `userEvent.type`, named as React Native Testing Library names them. */\nexport interface TypeOptions {\n /** Leave the field focused afterwards. By default typing ends with the field blurring. */\n skipBlur?: boolean;\n /** Press return at the end: `topSubmitEditing`, before any blur. */\n submitEditing?: boolean;\n}\n\n/**\n * Interactions as a person performs them: several events, a task apart, in the order the platform\n * sends them. Slower than `fireEvent`, and closer to what a device does.\n */\nexport interface UserEvent {\n /** A finger down and up, a task apart, through the responder. */\n press(node: FakeFabricNode): Promise<void>;\n /** A finger held for `duration` ms (500, the long-press delay, by default). */\n longPress(node: FakeFabricNode, options?: { duration?: number }): Promise<void>;\n /** Focus the field, then one change per character, each with the next `eventCount`. */\n type(node: FakeFabricNode, text: string, options?: TypeOptions): Promise<void>;\n /** Focus the field, empty it, and blur. */\n clear(node: FakeFabricNode): Promise<void>;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nasync function hold(node: FakeFabricNode, duration: number): Promise<void> {\n emit(node, 'topTouchStart', touch(node, 'start'));\n await settle();\n if (duration) await sleep(duration);\n emit(node, 'topTouchEnd', touch(node, 'end'));\n await settle();\n}\n\nasync function endEditing(field: FakeFabricNode, text: string): Promise<void> {\n emit(field, 'topEndEditing', { text });\n emit(field, 'topBlur');\n await settle();\n}\n\nconst user: UserEvent = {\n press: (node) => hold(node, 0),\n longPress: (node, { duration = 500 } = {}) => hold(node, duration),\n async type(node, text, options = {}) {\n const field = fieldOf(node);\n if (field.props['editable'] === false) return;\n emit(field, 'topFocus');\n await settle();\n const maxLength = field.props['maxLength'];\n const limit = typeof maxLength === 'number' ? maxLength : Infinity;\n let value = String((currentOf(field) ?? field).props['text'] ?? '');\n for (const key of text) {\n // `maxLength` is enforced natively: the field never calls back with text past it, so a key\n // pressed once the limit is reached is swallowed the same way it would be on a device.\n if (value.length >= limit) continue;\n value += key;\n emit(field, 'topKeyPress', { key });\n changeText(field, value);\n await settle();\n }\n if (options.submitEditing) emit(field, 'topSubmitEditing', { text: value });\n if (!options.skipBlur) await endEditing(field, value);\n else await settle();\n },\n async clear(node) {\n const field = fieldOf(node);\n if (field.props['editable'] === false) return;\n emit(field, 'topFocus');\n changeText(field, '');\n await settle();\n await endEditing(field, '');\n },\n};\n\n/** RNTL's `userEvent`: `userEvent.setup()` for an instance, or the same calls directly. */\nexport const userEvent: UserEvent & { setup(): UserEvent } = { ...user, setup: () => user };\n"]}
@@ -1,39 +1,3 @@
1
- /**
2
- * What a test gets for `react-native-gesture-handler`, whose source Node cannot load.
3
- *
4
- * `Gesture.Pan()`, `Gesture.Tap()`, `Gesture.Simultaneous(...)` and every other builder returns a
5
- * gesture that takes any configuration call, `.minDistance(10).onUpdate(...)`, and returns itself,
6
- * so a component builds its gestures in a test exactly as it does on a device. The callbacks it was
7
- * given are kept on `callbacks`, by name, for a test that wants to call one. `State` and
8
- * `Directions` are the library's own numbers.
9
- */
10
- /** The configuration and callback calls gestures take, as far as a test types them. */
11
- type GestureMethod = 'onBegin' | 'onStart' | 'onUpdate' | 'onChange' | 'onEnd' | 'onFinalize' | 'onTouchesDown' | 'onTouchesMove' | 'onTouchesUp' | 'onTouchesCancelled' | 'enabled' | 'runOnJS' | 'minDistance' | 'minPointers' | 'maxPointers' | 'numberOfTaps' | 'minDuration' | 'maxDuration' | 'direction' | 'activeOffsetX' | 'activeOffsetY' | 'failOffsetX' | 'failOffsetY' | 'hitSlop' | 'shouldCancelWhenOutside' | 'manualActivation' | 'simultaneousWithExternalGesture' | 'requireExternalGestureToFail' | 'blocksExternalGesture' | 'withRef' | 'withTestId';
12
- /**
13
- * A gesture built in a test: every configuration call returns it, and callbacks are kept. `kind`
14
- * is the builder that made it, `Pan` or `Race`, and `gestures` what a composed one was made of.
15
- */
16
- export type TestGesture = {
17
- readonly kind: string;
18
- readonly gestures: readonly TestGesture[];
19
- readonly callbacks: Record<string, (...args: never[]) => unknown>;
20
- } & {
21
- readonly [M in GestureMethod]: (...args: readonly unknown[]) => TestGesture;
22
- };
23
- type Builder = 'Pan' | 'Tap' | 'LongPress' | 'Pinch' | 'Rotation' | 'Fling' | 'Hover' | 'Manual' | 'Native' | 'ForceTouch' | 'Race' | 'Simultaneous' | 'Exclusive';
24
- export declare const Gesture: Readonly<Record<Builder, (...gestures: readonly unknown[]) => TestGesture>>;
25
- export declare const State: {
26
- readonly UNDETERMINED: 0;
27
- readonly FAILED: 1;
28
- readonly BEGAN: 2;
29
- readonly CANCELLED: 3;
30
- readonly ACTIVE: 4;
31
- readonly END: 5;
32
- };
33
- export declare const Directions: {
34
- readonly RIGHT: 1;
35
- readonly LEFT: 2;
36
- readonly UP: 4;
37
- readonly DOWN: 8;
38
- };
39
- export {};
1
+ /** What a test gets for `react-native-gesture-handler`, whose source Node cannot load. */
2
+ export * from '@ng-native/components/stand-ins/gesture-handler';
3
+ export type { StandInGesture as TestGesture } from '@ng-native/components/stand-ins/gesture-handler';
@@ -1,42 +1,3 @@
1
- /**
2
- * What a test gets for `react-native-gesture-handler`, whose source Node cannot load.
3
- *
4
- * `Gesture.Pan()`, `Gesture.Tap()`, `Gesture.Simultaneous(...)` and every other builder returns a
5
- * gesture that takes any configuration call, `.minDistance(10).onUpdate(...)`, and returns itself,
6
- * so a component builds its gestures in a test exactly as it does on a device. The callbacks it was
7
- * given are kept on `callbacks`, by name, for a test that wants to call one. `State` and
8
- * `Directions` are the library's own numbers.
9
- */
10
- function gesture(kind, gestures) {
11
- const callbacks = {};
12
- const fields = { kind, gestures, callbacks };
13
- const self = new Proxy(fields, {
14
- get(target, key) {
15
- if (typeof key === 'string' && key in fields)
16
- return fields[key];
17
- // Not a thenable, and nothing to say to a symbol such as `Symbol.toPrimitive`.
18
- if (typeof key === 'symbol' || key === 'then')
19
- return undefined;
20
- return (...args) => {
21
- if (key.startsWith('on') && typeof args[0] === 'function') {
22
- callbacks[key] = args[0];
23
- }
24
- return self;
25
- };
26
- },
27
- });
28
- return self;
29
- }
30
- export const Gesture = new Proxy({}, {
31
- get: (_target, kind) => (...gestures) => gesture(String(kind), gestures),
32
- });
33
- export const State = {
34
- UNDETERMINED: 0,
35
- FAILED: 1,
36
- BEGAN: 2,
37
- CANCELLED: 3,
38
- ACTIVE: 4,
39
- END: 5,
40
- };
41
- export const Directions = { RIGHT: 1, LEFT: 2, UP: 4, DOWN: 8 };
1
+ /** What a test gets for `react-native-gesture-handler`, whose source Node cannot load. */
2
+ export * from '@ng-native/components/stand-ins/gesture-handler';
42
3
  //# sourceMappingURL=gesture-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gesture-handler.js","sourceRoot":"","sources":["../src/gesture-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA6DH,SAAS,OAAO,CAAC,IAAY,EAAE,QAAgC;IAC7D,MAAM,SAAS,GAAkD,EAAE,CAAC;IACpE,MAAM,MAAM,GAA4B,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtE,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,MAAqB,EAAE;QAC5C,GAAG,CAAC,MAAM,EAAE,GAAG;YACb,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,+EAA+E;YAC/E,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,MAAM;gBAAE,OAAO,SAAS,CAAC;YAChE,OAAO,CAAC,GAAG,IAAe,EAAE,EAAE;gBAC5B,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU,EAAE,CAAC;oBAC1D,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAkC,CAAC;gBAC5D,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,KAAK,CAC9B,EAAE,EACF;IACE,GAAG,EACD,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAClB,CAAC,GAAG,QAAmB,EAAE,EAAE,CACzB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAyB,CAAC;CACrD,CAC6E,CAAC;AAEjF,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,YAAY,EAAE,CAAC;IACf,MAAM,EAAE,CAAC;IACT,KAAK,EAAE,CAAC;IACR,SAAS,EAAE,CAAC;IACZ,MAAM,EAAE,CAAC;IACT,GAAG,EAAE,CAAC;CACE,CAAC;AAEX,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAW,CAAC","sourcesContent":["/**\n * What a test gets for `react-native-gesture-handler`, whose source Node cannot load.\n *\n * `Gesture.Pan()`, `Gesture.Tap()`, `Gesture.Simultaneous(...)` and every other builder returns a\n * gesture that takes any configuration call, `.minDistance(10).onUpdate(...)`, and returns itself,\n * so a component builds its gestures in a test exactly as it does on a device. The callbacks it was\n * given are kept on `callbacks`, by name, for a test that wants to call one. `State` and\n * `Directions` are the library's own numbers.\n */\n\n/** The configuration and callback calls gestures take, as far as a test types them. */\ntype GestureMethod =\n | 'onBegin'\n | 'onStart'\n | 'onUpdate'\n | 'onChange'\n | 'onEnd'\n | 'onFinalize'\n | 'onTouchesDown'\n | 'onTouchesMove'\n | 'onTouchesUp'\n | 'onTouchesCancelled'\n | 'enabled'\n | 'runOnJS'\n | 'minDistance'\n | 'minPointers'\n | 'maxPointers'\n | 'numberOfTaps'\n | 'minDuration'\n | 'maxDuration'\n | 'direction'\n | 'activeOffsetX'\n | 'activeOffsetY'\n | 'failOffsetX'\n | 'failOffsetY'\n | 'hitSlop'\n | 'shouldCancelWhenOutside'\n | 'manualActivation'\n | 'simultaneousWithExternalGesture'\n | 'requireExternalGestureToFail'\n | 'blocksExternalGesture'\n | 'withRef'\n | 'withTestId';\n\n/**\n * A gesture built in a test: every configuration call returns it, and callbacks are kept. `kind`\n * is the builder that made it, `Pan` or `Race`, and `gestures` what a composed one was made of.\n */\nexport type TestGesture = {\n readonly kind: string;\n readonly gestures: readonly TestGesture[];\n readonly callbacks: Record<string, (...args: never[]) => unknown>;\n} & { readonly [M in GestureMethod]: (...args: readonly unknown[]) => TestGesture };\n\ntype Builder =\n | 'Pan'\n | 'Tap'\n | 'LongPress'\n | 'Pinch'\n | 'Rotation'\n | 'Fling'\n | 'Hover'\n | 'Manual'\n | 'Native'\n | 'ForceTouch'\n | 'Race'\n | 'Simultaneous'\n | 'Exclusive';\n\nfunction gesture(kind: string, gestures: readonly TestGesture[]): TestGesture {\n const callbacks: Record<string, (...args: never[]) => unknown> = {};\n const fields: Record<string, unknown> = { kind, gestures, callbacks };\n const self = new Proxy(fields as TestGesture, {\n get(target, key) {\n if (typeof key === 'string' && key in fields) return fields[key];\n // Not a thenable, and nothing to say to a symbol such as `Symbol.toPrimitive`.\n if (typeof key === 'symbol' || key === 'then') return undefined;\n return (...args: unknown[]) => {\n if (key.startsWith('on') && typeof args[0] === 'function') {\n callbacks[key] = args[0] as (...args: never[]) => unknown;\n }\n return self;\n };\n },\n });\n return self;\n}\n\nexport const Gesture = new Proxy(\n {},\n {\n get:\n (_target, kind) =>\n (...gestures: unknown[]) =>\n gesture(String(kind), gestures as TestGesture[]),\n },\n) as Readonly<Record<Builder, (...gestures: readonly unknown[]) => TestGesture>>;\n\nexport const State = {\n UNDETERMINED: 0,\n FAILED: 1,\n BEGAN: 2,\n CANCELLED: 3,\n ACTIVE: 4,\n END: 5,\n} as const;\n\nexport const Directions = { RIGHT: 1, LEFT: 2, UP: 4, DOWN: 8 } as const;\n"]}
1
+ {"version":3,"file":"gesture-handler.js","sourceRoot":"","sources":["../src/gesture-handler.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,cAAc,iDAAiD,CAAC","sourcesContent":["/** What a test gets for `react-native-gesture-handler`, whose source Node cannot load. */\nexport * from '@ng-native/components/stand-ins/gesture-handler';\nexport type { StandInGesture as TestGesture } from '@ng-native/components/stand-ins/gesture-handler';\n"]}
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  export { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';
10
10
  export { cleanup, render, screen, settle, waitForElementToBeRemoved, within, type RenderOptions, type RenderResult, } from './render.ts';
11
11
  export { injectService, type InjectServiceOptions } from './inject-service.ts';
12
+ export { memoryDatabase, type MemoryDatabase } from './memory-database.ts';
13
+ export { compileCss, type CompileCssOptions } from './compile-css.ts';
12
14
  export type { BoundQueries, ByRoleOptions, Matcher, TextMatchOptions } from './queries.ts';
13
15
  export { waitFor, type WaitForOptions } from './wait-for.ts';
14
16
  export { gestureOf } from './gesture-of.ts';
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@
9
9
  export { createFakeFabric } from "./test-utils.js";
10
10
  export { cleanup, render, screen, settle, waitForElementToBeRemoved, within, } from "./render.js";
11
11
  export { injectService } from "./inject-service.js";
12
+ export { memoryDatabase } from "./memory-database.js";
13
+ export { compileCss } from "./compile-css.js";
12
14
  export { waitFor } from "./wait-for.js";
13
15
  export { gestureOf } from "./gesture-of.js";
14
16
  export { fireEvent, userEvent, } from "./events.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,gBAAgB,EAAwC,MAAM,iBAAiB,CAAC;AACzF,OAAO,EACL,OAAO,EACP,MAAM,EACN,MAAM,EACN,MAAM,EACN,yBAAyB,EACzB,MAAM,GAGP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAC;AAE/E,OAAO,EAAE,OAAO,EAAuB,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EACL,SAAS,EACT,SAAS,GAIV,MAAM,aAAa,CAAC","sourcesContent":["/**\n * The fake `nativeFabricUIManager`, and a Testing Library on top of it. Kept out of the engine so\n * nothing in a shipped bundle can reach it, and out of `test/` so an app's own tests can use it.\n *\n * The runners live beside `src/` rather than in it: `@ng-native/testing/vitest` and\n * `@ng-native/testing/register` are plain JavaScript, because they are loaded by Node before\n * anything can compile TypeScript for it.\n */\nexport { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';\nexport {\n cleanup,\n render,\n screen,\n settle,\n waitForElementToBeRemoved,\n within,\n type RenderOptions,\n type RenderResult,\n} from './render.ts';\nexport { injectService, type InjectServiceOptions } from './inject-service.ts';\nexport type { BoundQueries, ByRoleOptions, Matcher, TextMatchOptions } from './queries.ts';\nexport { waitFor, type WaitForOptions } from './wait-for.ts';\nexport { gestureOf } from './gesture-of.ts';\nexport type { TestGesture } from './gesture-handler.ts';\nexport {\n fireEvent,\n userEvent,\n type EventPayload,\n type TypeOptions,\n type UserEvent,\n} from './events.ts';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,gBAAgB,EAAwC,MAAM,iBAAiB,CAAC;AACzF,OAAO,EACL,OAAO,EACP,MAAM,EACN,MAAM,EACN,MAAM,EACN,yBAAyB,EACzB,MAAM,GAGP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAuB,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAA0B,MAAM,kBAAkB,CAAC;AAEtE,OAAO,EAAE,OAAO,EAAuB,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EACL,SAAS,EACT,SAAS,GAIV,MAAM,aAAa,CAAC","sourcesContent":["/**\n * The fake `nativeFabricUIManager`, and a Testing Library on top of it. Kept out of the engine so\n * nothing in a shipped bundle can reach it, and out of `test/` so an app's own tests can use it.\n *\n * The runners live beside `src/` rather than in it: `@ng-native/testing/vitest` and\n * `@ng-native/testing/register` are plain JavaScript, because they are loaded by Node before\n * anything can compile TypeScript for it.\n */\nexport { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';\nexport {\n cleanup,\n render,\n screen,\n settle,\n waitForElementToBeRemoved,\n within,\n type RenderOptions,\n type RenderResult,\n} from './render.ts';\nexport { injectService, type InjectServiceOptions } from './inject-service.ts';\nexport { memoryDatabase, type MemoryDatabase } from './memory-database.ts';\nexport { compileCss, type CompileCssOptions } from './compile-css.ts';\nexport type { BoundQueries, ByRoleOptions, Matcher, TextMatchOptions } from './queries.ts';\nexport { waitFor, type WaitForOptions } from './wait-for.ts';\nexport { gestureOf } from './gesture-of.ts';\nexport type { TestGesture } from './gesture-handler.ts';\nexport {\n fireEvent,\n userEvent,\n type EventPayload,\n type TypeOptions,\n type UserEvent,\n} from './events.ts';\n"]}
@@ -12,8 +12,17 @@ export interface InjectServiceOptions {
12
12
  providers?: (Provider | EnvironmentProviders)[];
13
13
  }
14
14
  /**
15
- * The instance of `token` from a fresh app's root injector, built with `providers`. The app is
16
- * destroyed by `cleanup()`, which ends the service's `DestroyRef`.
15
+ * The instance of `token` from an app's root injector.
16
+ *
17
+ * The calls in one test share an app, so a service and the services it injects are the ones a
18
+ * later call returns, as they share a root in the app. A call with `providers` starts a new app,
19
+ * which the calls after it use. The app ends with its test: the next test starts another, whether
20
+ * or not `cleanup()` was called, so one test never sees another's services. `cleanup()` destroys
21
+ * every app, which ends each service's `DestroyRef`.
22
+ *
23
+ * An app is shared only while exactly one test is running, since that is the only time the test
24
+ * a call belongs to is known. Every call makes an app of its own outside a test (a `beforeAll`),
25
+ * while tests run at once (`it.concurrent`), and where the runner's setup did not run.
17
26
  */
18
27
  export declare function injectService<T>(token: ProviderToken<T>, options?: InjectServiceOptions): T;
19
28
  /**
@@ -19,22 +19,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
19
19
  args: [{ selector: 'ng-native-service-host', template: '' }]
20
20
  }] });
21
21
  const apps = [];
22
+ const TESTS = Symbol.for('ng-native.testing.tests');
23
+ const tests = () => globalThis[TESTS];
24
+ /** The app the calls in the running test share, and which test that is. */
25
+ let shared = null;
22
26
  /**
23
- * The instance of `token` from a fresh app's root injector, built with `providers`. The app is
24
- * destroyed by `cleanup()`, which ends the service's `DestroyRef`.
27
+ * The instance of `token` from an app's root injector.
28
+ *
29
+ * The calls in one test share an app, so a service and the services it injects are the ones a
30
+ * later call returns, as they share a root in the app. A call with `providers` starts a new app,
31
+ * which the calls after it use. The app ends with its test: the next test starts another, whether
32
+ * or not `cleanup()` was called, so one test never sees another's services. `cleanup()` destroys
33
+ * every app, which ends each service's `DestroyRef`.
34
+ *
35
+ * An app is shared only while exactly one test is running, since that is the only time the test
36
+ * a call belongs to is known. Every call makes an app of its own outside a test (a `beforeAll`),
37
+ * while tests run at once (`it.concurrent`), and where the runner's setup did not run.
25
38
  */
26
39
  export function injectService(token, options = {}) {
27
- const { applicationRef } = mount(1, ServiceHost, createFakeFabric(), {
28
- providers: options.providers,
29
- });
40
+ const state = tests();
41
+ const test = state?.running === 1 ? state.finished : undefined;
42
+ const reusable = options.providers === undefined && test !== undefined && shared?.test === test;
43
+ const app = reusable ? shared.app : appWith(options.providers);
44
+ shared = test === undefined ? null : { app, test };
45
+ return app.injector.get(token);
46
+ }
47
+ function appWith(providers) {
48
+ const { applicationRef } = mount(1, ServiceHost, createFakeFabric(), { providers });
30
49
  apps.push(applicationRef);
31
- return applicationRef.injector.get(token);
50
+ return applicationRef;
32
51
  }
33
52
  /**
34
53
  * Destroy every app `injectService` created. Called by `cleanup()`. A teardown that throws does
35
54
  * not stop the rest: each app is destroyed, then the first error is thrown.
36
55
  */
37
56
  export function destroyServiceApps() {
57
+ shared = null;
38
58
  const errors = [];
39
59
  for (const app of apps.splice(0)) {
40
60
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"inject-service.js","sourceRoot":"","sources":["../src/inject-service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,SAAS,GAKV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;;AAOnD,MACM,WAAW;uGAAX,WAAW;2FAAX,WAAW,kFAD0C,EAAE;;2FACvD,WAAW;kBADhB,SAAS;mBAAC,EAAE,QAAQ,EAAE,wBAAwB,EAAE,QAAQ,EAAE,EAAE,EAAE;;AAG/D,MAAM,IAAI,GAAqB,EAAE,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAI,KAAuB,EAAE,UAAgC,EAAE;IAC1F,MAAM,EAAE,cAAc,EAAE,GAAG,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,EAAE;QACnE,SAAS,EAAE,OAAO,CAAC,SAAS;KAC7B,CAAC,CAAC;IACH,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC1B,OAAO,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,GAAG,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC","sourcesContent":["/**\n * A service on its own, with no component to render.\n *\n * There is no TestBed, and `Injector.create()` is no stand-in for it: an injector made that way\n * has no root scope, so a `@Service()` or `providedIn: 'root'` class is not found in it, nor is\n * anything such a class injects. The app injector `mount()` creates is the root an app has, so\n * the service gets the same providers, `DestroyRef` and platform it gets on a device.\n */\nimport {\n Component,\n type ApplicationRef,\n type EnvironmentProviders,\n type Provider,\n type ProviderToken,\n} from '@angular/core';\nimport { mount } from '@ng-native/platform';\nimport { createFakeFabric } from './test-utils.ts';\n\nexport interface InjectServiceOptions {\n /** Providers for the app the service is created in: a stand-in for what it injects. */\n providers?: (Provider | EnvironmentProviders)[];\n}\n\n@Component({ selector: 'ng-native-service-host', template: '' })\nclass ServiceHost {}\n\nconst apps: ApplicationRef[] = [];\n\n/**\n * The instance of `token` from a fresh app's root injector, built with `providers`. The app is\n * destroyed by `cleanup()`, which ends the service's `DestroyRef`.\n */\nexport function injectService<T>(token: ProviderToken<T>, options: InjectServiceOptions = {}): T {\n const { applicationRef } = mount(1, ServiceHost, createFakeFabric(), {\n providers: options.providers,\n });\n apps.push(applicationRef);\n return applicationRef.injector.get(token);\n}\n\n/**\n * Destroy every app `injectService` created. Called by `cleanup()`. A teardown that throws does\n * not stop the rest: each app is destroyed, then the first error is thrown.\n */\nexport function destroyServiceApps(): void {\n const errors: unknown[] = [];\n for (const app of apps.splice(0)) {\n try {\n app.destroy();\n } catch (error) {\n errors.push(error);\n }\n }\n if (errors.length > 0) throw errors[0];\n}\n"]}
1
+ {"version":3,"file":"inject-service.js","sourceRoot":"","sources":["../src/inject-service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,SAAS,GAKV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;;AAOnD,MACM,WAAW;uGAAX,WAAW;2FAAX,WAAW,kFAD0C,EAAE;;2FACvD,WAAW;kBADhB,SAAS;mBAAC,EAAE,QAAQ,EAAE,wBAAwB,EAAE,QAAQ,EAAE,EAAE,EAAE;;AAG/D,MAAM,IAAI,GAAqB,EAAE,CAAC;AAWlC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;AACpD,MAAM,KAAK,GAAG,GAAsB,EAAE,CAAE,UAAgD,CAAC,KAAK,CAAC,CAAC;AAEhG,2EAA2E;AAC3E,IAAI,MAAM,GAAmE,IAAI,CAAC;AAElF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAI,KAAuB,EAAE,UAAgC,EAAE;IAC1F,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC;IACtB,MAAM,IAAI,GAAG,KAAK,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/D,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,EAAE,IAAI,KAAK,IAAI,CAAC;IAChG,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChE,MAAM,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACnD,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,OAAO,CAAC,SAA4C;IAC3D,MAAM,EAAE,cAAc,EAAE,GAAG,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IACpF,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC1B,OAAO,cAAc,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,GAAG,IAAI,CAAC;IACd,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,GAAG,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC","sourcesContent":["/**\n * A service on its own, with no component to render.\n *\n * There is no TestBed, and `Injector.create()` is no stand-in for it: an injector made that way\n * has no root scope, so a `@Service()` or `providedIn: 'root'` class is not found in it, nor is\n * anything such a class injects. The app injector `mount()` creates is the root an app has, so\n * the service gets the same providers, `DestroyRef` and platform it gets on a device.\n */\nimport {\n Component,\n type ApplicationRef,\n type EnvironmentProviders,\n type Provider,\n type ProviderToken,\n} from '@angular/core';\nimport { mount } from '@ng-native/platform';\nimport { createFakeFabric } from './test-utils.ts';\n\nexport interface InjectServiceOptions {\n /** Providers for the app the service is created in: a stand-in for what it injects. */\n providers?: (Provider | EnvironmentProviders)[];\n}\n\n@Component({ selector: 'ng-native-service-host', template: '' })\nclass ServiceHost {}\n\nconst apps: ApplicationRef[] = [];\n\n/**\n * What the runner's setup says about the tests: how many have finished, which tells one test from\n * the next, and how many are running now. `runner/setup.mjs` keeps it under Vitest and\n * `runner/register.mjs` under `node:test`. Undefined where neither ran.\n */\ninterface Tests {\n finished: number;\n running: number;\n}\nconst TESTS = Symbol.for('ng-native.testing.tests');\nconst tests = (): Tests | undefined => (globalThis as Record<symbol, Tests | undefined>)[TESTS];\n\n/** The app the calls in the running test share, and which test that is. */\nlet shared: { readonly app: ApplicationRef; readonly test: number } | null = null;\n\n/**\n * The instance of `token` from an app's root injector.\n *\n * The calls in one test share an app, so a service and the services it injects are the ones a\n * later call returns, as they share a root in the app. A call with `providers` starts a new app,\n * which the calls after it use. The app ends with its test: the next test starts another, whether\n * or not `cleanup()` was called, so one test never sees another's services. `cleanup()` destroys\n * every app, which ends each service's `DestroyRef`.\n *\n * An app is shared only while exactly one test is running, since that is the only time the test\n * a call belongs to is known. Every call makes an app of its own outside a test (a `beforeAll`),\n * while tests run at once (`it.concurrent`), and where the runner's setup did not run.\n */\nexport function injectService<T>(token: ProviderToken<T>, options: InjectServiceOptions = {}): T {\n const state = tests();\n const test = state?.running === 1 ? state.finished : undefined;\n const reusable = options.providers === undefined && test !== undefined && shared?.test === test;\n const app = reusable ? shared!.app : appWith(options.providers);\n shared = test === undefined ? null : { app, test };\n return app.injector.get(token);\n}\n\nfunction appWith(providers: InjectServiceOptions['providers']): ApplicationRef {\n const { applicationRef } = mount(1, ServiceHost, createFakeFabric(), { providers });\n apps.push(applicationRef);\n return applicationRef;\n}\n\n/**\n * Destroy every app `injectService` created. Called by `cleanup()`. A teardown that throws does\n * not stop the rest: each app is destroyed, then the first error is thrown.\n */\nexport function destroyServiceApps(): void {\n shared = null;\n const errors: unknown[] = [];\n for (const app of apps.splice(0)) {\n try {\n app.destroy();\n } catch (error) {\n errors.push(error);\n }\n }\n if (errors.length > 0) throw errors[0];\n}\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * An in-memory SQLite database with the part of `expo-sqlite`'s interface an app's services use,
3
+ * for a test in Node, where no native module can load. It is Node's own `node:sqlite`, so the SQL
4
+ * a service runs is run for real: a constraint is enforced, a migration creates its tables.
5
+ *
6
+ * const restore = openDatabasesWith(() => memoryDatabase());
7
+ *
8
+ * with `openDatabasesWith` from `@ng-native/expo/database`, which points `database()` at it.
9
+ */
10
+ type Row = Record<string, unknown>;
11
+ export interface MemoryDatabase {
12
+ execAsync(source: string): Promise<void>;
13
+ runAsync(source: string, ...params: unknown[]): Promise<{
14
+ lastInsertRowId: number;
15
+ changes: number;
16
+ }>;
17
+ getFirstAsync<T = Row>(source: string, ...params: unknown[]): Promise<T | null>;
18
+ getAllAsync<T = Row>(source: string, ...params: unknown[]): Promise<T[]>;
19
+ withTransactionAsync(task: () => Promise<void>): Promise<void>;
20
+ closeAsync(): Promise<void>;
21
+ }
22
+ export declare function memoryDatabase(): MemoryDatabase;
23
+ export {};
@@ -0,0 +1,60 @@
1
+ /** A value as SQLite binds it: `expo-sqlite` takes a boolean as 1 or 0, which Node refuses. */
2
+ const value = (param) => (typeof param === 'boolean' ? Number(param) : param);
3
+ /** `expo-sqlite` takes parameters one by one, as one array, or as one object of named ones. */
4
+ function bound(params) {
5
+ const [only] = params;
6
+ if (params.length !== 1)
7
+ return params.map(value);
8
+ if (Array.isArray(only))
9
+ return only.map(value);
10
+ // Bytes are one value, a BLOB, and not an object of named parameters.
11
+ if (only !== null && typeof only === 'object' && !(only instanceof Uint8Array)) {
12
+ const named = Object.entries(only).map(([name, param]) => [name, value(param)]);
13
+ return [Object.fromEntries(named)];
14
+ }
15
+ return [value(only)];
16
+ }
17
+ export function memoryDatabase() {
18
+ const sqlite = process.getBuiltinModule('node:sqlite');
19
+ if (!sqlite) {
20
+ throw new Error('[angular-native] memoryDatabase() needs node:sqlite: Node 22.13 or later.');
21
+ }
22
+ const { DatabaseSync } = sqlite;
23
+ // Foreign keys off until asked for, as a connection `expo-sqlite` opens is: Node turns them on.
24
+ const db = new DatabaseSync(':memory:', { enableForeignKeyConstraints: false });
25
+ return {
26
+ execAsync: async (source) => db.exec(source),
27
+ runAsync: async (source, ...params) => {
28
+ const { lastInsertRowid, changes } = db.prepare(source).run(...bound(params));
29
+ return { lastInsertRowId: Number(lastInsertRowid), changes: Number(changes) };
30
+ },
31
+ getFirstAsync: async (source, ...params) => {
32
+ const row = db.prepare(source).get(...bound(params));
33
+ return row === undefined ? null : { ...row };
34
+ },
35
+ getAllAsync: async (source, ...params) => db
36
+ .prepare(source)
37
+ .all(...bound(params))
38
+ .map((row) => ({ ...row })),
39
+ withTransactionAsync: async (task) => {
40
+ db.exec('BEGIN');
41
+ try {
42
+ await task();
43
+ db.exec('COMMIT');
44
+ }
45
+ catch (error) {
46
+ // SQLite rolls some failures back itself, and a second rollback then fails: the error
47
+ // worth having is the task's.
48
+ try {
49
+ db.exec('ROLLBACK');
50
+ }
51
+ catch {
52
+ // Already rolled back.
53
+ }
54
+ throw error;
55
+ }
56
+ },
57
+ closeAsync: async () => db.close(),
58
+ };
59
+ }
60
+ //# sourceMappingURL=memory-database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-database.js","sourceRoot":"","sources":["../src/memory-database.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * An in-memory SQLite database with the part of `expo-sqlite`'s interface an app's services use,\n * for a test in Node, where no native module can load. It is Node's own `node:sqlite`, so the SQL\n * a service runs is run for real: a constraint is enforced, a migration creates its tables.\n *\n * const restore = openDatabasesWith(() => memoryDatabase());\n *\n * with `openDatabasesWith` from `@ng-native/expo/database`, which points `database()` at it.\n */\ntype Row = Record<string, unknown>;\n\nexport interface MemoryDatabase {\n execAsync(source: string): Promise<void>;\n runAsync(\n source: string,\n ...params: unknown[]\n ): Promise<{ lastInsertRowId: number; changes: number }>;\n getFirstAsync<T = Row>(source: string, ...params: unknown[]): Promise<T | null>;\n getAllAsync<T = Row>(source: string, ...params: unknown[]): Promise<T[]>;\n withTransactionAsync(task: () => Promise<void>): Promise<void>;\n closeAsync(): Promise<void>;\n}\n\n/** A value as SQLite binds it: `expo-sqlite` takes a boolean as 1 or 0, which Node refuses. */\nconst value = (param: unknown): unknown => (typeof param === 'boolean' ? Number(param) : param);\n\n/** `expo-sqlite` takes parameters one by one, as one array, or as one object of named ones. */\nfunction bound(params: unknown[]): never[] {\n const [only] = params;\n if (params.length !== 1) return params.map(value) as never[];\n if (Array.isArray(only)) return only.map(value) as never[];\n // Bytes are one value, a BLOB, and not an object of named parameters.\n if (only !== null && typeof only === 'object' && !(only instanceof Uint8Array)) {\n const named = Object.entries(only).map(([name, param]) => [name, value(param)]);\n return [Object.fromEntries(named)] as never[];\n }\n return [value(only)] as never[];\n}\n\nexport function memoryDatabase(): MemoryDatabase {\n const sqlite = process.getBuiltinModule('node:sqlite') as\n typeof import('node:sqlite') | undefined;\n if (!sqlite) {\n throw new Error('[angular-native] memoryDatabase() needs node:sqlite: Node 22.13 or later.');\n }\n const { DatabaseSync } = sqlite;\n // Foreign keys off until asked for, as a connection `expo-sqlite` opens is: Node turns them on.\n const db = new DatabaseSync(':memory:', { enableForeignKeyConstraints: false });\n return {\n execAsync: async (source) => db.exec(source),\n runAsync: async (source, ...params) => {\n const { lastInsertRowid, changes } = db.prepare(source).run(...bound(params));\n return { lastInsertRowId: Number(lastInsertRowid), changes: Number(changes) };\n },\n getFirstAsync: async <T>(source: string, ...params: unknown[]) => {\n const row = db.prepare(source).get(...bound(params));\n return row === undefined ? null : ({ ...row } as T);\n },\n getAllAsync: async <T>(source: string, ...params: unknown[]) =>\n db\n .prepare(source)\n .all(...bound(params))\n .map((row) => ({ ...row })) as T[],\n withTransactionAsync: async (task) => {\n db.exec('BEGIN');\n try {\n await task();\n db.exec('COMMIT');\n } catch (error) {\n // SQLite rolls some failures back itself, and a second rollback then fails: the error\n // worth having is the task's.\n try {\n db.exec('ROLLBACK');\n } catch {\n // Already rolled back.\n }\n throw error;\n }\n },\n closeAsync: async () => db.close(),\n };\n}\n"]}
package/dist/queries.d.ts CHANGED
@@ -57,6 +57,11 @@ export type BoundQueries = {
57
57
  [K in Name as `findAllBy${K}`]: (...args: FindArgs<K>) => Promise<FakeFabricNode[]>;
58
58
  };
59
59
  export declare const flatten: (nodes: readonly FakeFabricNode[]) => FakeFabricNode[];
60
+ /**
61
+ * `nodes` with each node's `parent` set through the tree beneath, so a test can go up from what a
62
+ * query found: the row a text is in. Not enumerable, so a node compares and prints as it did.
63
+ */
64
+ export declare function withParents(nodes: readonly FakeFabricNode[], parent?: FakeFabricNode): readonly FakeFabricNode[];
60
65
  /** What a screen reader would read out: every `RawText` under the node, in order. */
61
66
  export declare function textContent(node: FakeFabricNode): string;
62
67
  /** A text field under any of the names the platforms register it by. */
package/dist/queries.js CHANGED
@@ -11,6 +11,21 @@ function matches(value, matcher, exact = true) {
11
11
  }
12
12
  const show = (matcher) => matcher instanceof RegExp ? String(matcher) : JSON.stringify(matcher);
13
13
  export const flatten = (nodes) => nodes.flatMap((node) => [node, ...flatten(node.children)]);
14
+ /**
15
+ * `nodes` with each node's `parent` set through the tree beneath, so a test can go up from what a
16
+ * query found: the row a text is in. Not enumerable, so a node compares and prints as it did.
17
+ */
18
+ export function withParents(nodes, parent) {
19
+ for (const node of nodes) {
20
+ // The nodes a walk starts from keep the parent they have: `within(row)` starts from the row.
21
+ if (parent || !('parent' in node)) {
22
+ const value = parent ?? null;
23
+ Object.defineProperty(node, 'parent', { value, configurable: true, writable: true });
24
+ }
25
+ withParents(node.children, node);
26
+ }
27
+ return nodes;
28
+ }
14
29
  /**
15
30
  * A view that takes itself and everything under it out of sight or out of the accessibility tree,
16
31
  * as React Native Testing Library's `isHiddenFromAccessibility` decides it.
@@ -39,7 +54,7 @@ const nameOf = (node) => typeof node.props['accessibilityLabel'] === 'string'
39
54
  const QUERIES = {
40
55
  Role: (role, options = {}) => ({
41
56
  describe: `role ${show(role)}` + (options.name === undefined ? '' : ` and name ${show(options.name)}`),
42
- select: (node) => matches(node.props['accessibilityRole'], role) &&
57
+ select: (node) => (matches(node.props['accessibilityRole'], role) || matches(node.props['role'], role)) &&
43
58
  (options.name === undefined || matches(nameOf(node), options.name, options.exact)),
44
59
  }),
45
60
  // `Paragraph` alone: a nested `<text>` commits as a `Text` span inside one, so the paragraph is
@@ -61,7 +76,9 @@ const QUERIES = {
61
76
  }),
62
77
  PlaceholderText: (placeholder, options = {}) => ({
63
78
  describe: `placeholder ${show(placeholder)}`,
64
- select: (node) => matches(node.props['placeholder'], placeholder, options.exact),
79
+ // A text field alone draws a placeholder. A wrapper with a `placeholder` input of its own has
80
+ // the attribute on its host view too, which nobody sees.
81
+ select: (node) => isTextInput(node) && matches(node.props['placeholder'], placeholder, options.exact),
65
82
  }),
66
83
  // A text field's value is its `text` prop, which is what `<text-input>` binds its model to.
67
84
  DisplayValue: (value, options = {}) => ({
@@ -82,7 +99,7 @@ export function bindQueries(roots, tree, found) {
82
99
  const query = QUERIES[name](...args);
83
100
  const options = args[1];
84
101
  const walk = options?.includeHiddenElements ? flatten : reachable;
85
- const hits = walk(roots()).filter(query.select);
102
+ const hits = walk(withParents(roots())).filter(query.select);
86
103
  hits.forEach(found);
87
104
  return hits;
88
105
  };
@@ -141,6 +158,7 @@ const IDENTIFYING = [
141
158
  'testID',
142
159
  'nativeID',
143
160
  'accessibilityRole',
161
+ 'role',
144
162
  'accessibilityLabel',
145
163
  'placeholder',
146
164
  'text',