@ng-native/testing 0.17.1 → 0.18.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.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * What a test gets for `@ng-native/components/animations`: the graph with no frames between, so
3
+ * an animation has finished by the time `start()` returns. See the components package's
4
+ * `stand-ins/animations` for what it does and does not do.
5
+ */
6
+ export * from '@ng-native/components/stand-ins/animations';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * What a test gets for `@ng-native/components/animations`: the graph with no frames between, so
3
+ * an animation has finished by the time `start()` returns. See the components package's
4
+ * `stand-ins/animations` for what it does and does not do.
5
+ */
6
+ export * from '@ng-native/components/stand-ins/animations';
7
+ //# sourceMappingURL=animations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animations.js","sourceRoot":"","sources":["../src/animations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,4CAA4C,CAAC","sourcesContent":["/**\n * What a test gets for `@ng-native/components/animations`: the graph with no frames between, so\n * an animation has finished by the time `start()` returns. See the components package's\n * `stand-ins/animations` for what it does and does not do.\n */\nexport * from '@ng-native/components/stand-ins/animations';\n"]}
@@ -5,6 +5,12 @@ export interface CompileCssOptions {
5
5
  * first one throws, so a test never passes on a sheet that quietly lost a rule.
6
6
  */
7
7
  readonly onUnsupported?: (message: string) => void;
8
+ /**
9
+ * Read the sheet as the browser it was written for reads it, as the build reads a library's
10
+ * stylesheet: `::before` and `::after` as boxes of their own, `visibility`, a clip to nothing and
11
+ * the rest an app's own sheet is told to write another way.
12
+ */
13
+ readonly generated?: boolean;
8
14
  }
9
15
  /**
10
16
  * A stylesheet compiled as the build compiles an app's, for `render()`'s `globalStyles`:
@@ -1 +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"]}
1
+ {"version":3,"file":"compile-css.js","sourceRoot":"","sources":["../src/compile-css.ts"],"names":[],"mappings":"AAgBA;;;;;;;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 * Read the sheet as the browser it was written for reads it, as the build reads a library's\n * stylesheet: `::before` and `::after` as boxes of their own, `visibility`, a clip to nothing and\n * the rest an app's own sheet is told to write another way.\n */\n readonly generated?: boolean;\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.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * later task, so an event has to be awaited before the tree it changed can be read.
15
15
  */
16
16
  import { currentOf, ownerOf, settle } from "./render.js";
17
+ import { expoUiButtonDisabled, expoUiButtonPress, isExpoUiButton } from "./expo-ui-button.js";
17
18
  import { flatten, isSearchBar, isTextInput, withParents } from "./queries.js";
18
19
  const nativeOf = (payload) => (payload && 'nativeEvent' in payload ? payload['nativeEvent'] : payload) ?? {};
19
20
  function emit(node, type, nativeEvent = {}) {
@@ -37,11 +38,18 @@ function touch(node, phase) {
37
38
  /**
38
39
  * Whether a finger could reach the node, by RNTL's reading of `pointerEvents` on the node as last
39
40
  * committed: not when it is `none` or `box-none` itself, nor under a `none` or `box-only` view.
41
+ *
42
+ * The walk stops at a view committed with `layoutRoot`: `@expo/ui`'s view host for content
43
+ * presented in its own window, a sheet's or a popover's, which takes its content's touches itself,
44
+ * so what is around it, such as the sheet's `pointerEvents: 'none'` host, has no say. So does
45
+ * SwiftUI's sheet, for the toolbar items it presents with that content.
40
46
  */
41
47
  function touchable(node) {
42
48
  withParents(ownerOf(node).fabric.committed);
43
49
  let blocking = 'box-none';
44
50
  for (let at = currentOf(node) ?? node; at; at = at.parent ?? null) {
51
+ if (at.props['layoutRoot'] === true || /ExpoUI_BottomSheetView$/.test(at.viewName))
52
+ return true;
45
53
  const pointerEvents = at.props['pointerEvents'];
46
54
  if (pointerEvents === 'none' || pointerEvents === blocking)
47
55
  return false;
@@ -65,11 +73,21 @@ function changeText(field, text) {
65
73
  else
66
74
  emit(field, 'topChange', { text, eventCount: nextCount(field) });
67
75
  }
76
+ /**
77
+ * The finger lifting: the touch's end, and on `@expo/ui`'s button the press SwiftUI or Compose
78
+ * fires for it, which a disabled one does not.
79
+ */
80
+ function release(node) {
81
+ emit(node, 'topTouchEnd', touch(node, 'end'));
82
+ const current = currentOf(node) ?? node;
83
+ if (isExpoUiButton(current) && !expoUiButtonDisabled(current))
84
+ emit(node, expoUiButtonPress());
85
+ }
68
86
  async function press(node) {
69
87
  if (!touchable(node))
70
88
  return;
71
89
  emit(node, 'topTouchStart', touch(node, 'start'));
72
- emit(node, 'topTouchEnd', touch(node, 'end'));
90
+ release(node);
73
91
  await settle();
74
92
  }
75
93
  const named = {
@@ -126,7 +144,7 @@ async function hold(node, duration) {
126
144
  await settle();
127
145
  if (duration)
128
146
  await sleep(duration);
129
- emit(node, 'topTouchEnd', touch(node, 'end'));
147
+ release(node);
130
148
  await settle();
131
149
  }
132
150
  async function endEditing(field, text) {
@@ -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 * A touch is only sent where a finger could land, as React Native Testing Library decides it from\n * `pointerEvents`: a press, a type or a clear on a node that takes no touch does nothing at all.\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, isSearchBar, isTextInput, withParents } 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/**\n * Whether a finger could reach the node, by RNTL's reading of `pointerEvents` on the node as last\n * committed: not when it is `none` or `box-none` itself, nor under a `none` or `box-only` view.\n */\nfunction touchable(node: FakeFabricNode): boolean {\n withParents(ownerOf(node).fabric.committed);\n let blocking = 'box-none';\n for (let at: FakeFabricNode | null = currentOf(node) ?? node; at; at = at.parent ?? null) {\n const pointerEvents = at.props['pointerEvents'];\n if (pointerEvents === 'none' || pointerEvents === blocking) return false;\n blocking = 'box-only';\n }\n return true;\n}\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 if (!touchable(node)) return;\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 if (!touchable(node)) return;\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 || !touchable(field)) 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 || !touchable(field)) 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 * A touch is only sent where a finger could land, as React Native Testing Library decides it from\n * `pointerEvents`: a press, a type or a clear on a node that takes no touch does nothing at all.\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 { expoUiButtonDisabled, expoUiButtonPress, isExpoUiButton } from './expo-ui-button.ts';\nimport { flatten, isSearchBar, isTextInput, withParents } 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/**\n * Whether a finger could reach the node, by RNTL's reading of `pointerEvents` on the node as last\n * committed: not when it is `none` or `box-none` itself, nor under a `none` or `box-only` view.\n *\n * The walk stops at a view committed with `layoutRoot`: `@expo/ui`'s view host for content\n * presented in its own window, a sheet's or a popover's, which takes its content's touches itself,\n * so what is around it, such as the sheet's `pointerEvents: 'none'` host, has no say. So does\n * SwiftUI's sheet, for the toolbar items it presents with that content.\n */\nfunction touchable(node: FakeFabricNode): boolean {\n withParents(ownerOf(node).fabric.committed);\n let blocking = 'box-none';\n for (let at: FakeFabricNode | null = currentOf(node) ?? node; at; at = at.parent ?? null) {\n if (at.props['layoutRoot'] === true || /ExpoUI_BottomSheetView$/.test(at.viewName)) return true;\n const pointerEvents = at.props['pointerEvents'];\n if (pointerEvents === 'none' || pointerEvents === blocking) return false;\n blocking = 'box-only';\n }\n return true;\n}\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\n/**\n * The finger lifting: the touch's end, and on `@expo/ui`'s button the press SwiftUI or Compose\n * fires for it, which a disabled one does not.\n */\nfunction release(node: FakeFabricNode): void {\n emit(node, 'topTouchEnd', touch(node, 'end'));\n const current = currentOf(node) ?? node;\n if (isExpoUiButton(current) && !expoUiButtonDisabled(current)) emit(node, expoUiButtonPress());\n}\n\nasync function press(node: FakeFabricNode): Promise<void> {\n if (!touchable(node)) return;\n emit(node, 'topTouchStart', touch(node, 'start'));\n release(node);\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 if (!touchable(node)) return;\n emit(node, 'topTouchStart', touch(node, 'start'));\n await settle();\n if (duration) await sleep(duration);\n release(node);\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 || !touchable(field)) 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 || !touchable(field)) 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"]}
@@ -0,0 +1,12 @@
1
+ import type { FakeFabricNode } from './test-utils.ts';
2
+ /** Whether the node is `@expo/ui`'s button, which commits as `Button` in the `ExpoUI` module. */
3
+ export declare const isExpoUiButton: (node: FakeFabricNode) => boolean;
4
+ /** What a screen reader reads out for the button: SwiftUI's `label`, or the text inside it. */
5
+ export declare const expoUiButtonName: (node: FakeFabricNode) => string;
6
+ /**
7
+ * Whether SwiftUI's `disabled` modifier has turned the button off. Compose has no such modifier,
8
+ * and its button draws and takes taps the same with it.
9
+ */
10
+ export declare function expoUiButtonDisabled(node: FakeFabricNode): boolean;
11
+ /** The event a tap on the button fires, by the platform it was rendered for. */
12
+ export declare const expoUiButtonPress: () => string;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `@expo/ui`'s button, SwiftUI's `Button` or Compose's, as the fake sees it.
3
+ *
4
+ * It commits no accessibility props: the native control is the button a screen reader reads out,
5
+ * by the label it draws, and a tap on it fires `buttonPress` on iOS or `buttonPressed` on Android
6
+ * rather than anything the responder hears. The queries and the press events read it from here.
7
+ */
8
+ import { nativePlatform } from '@ng-native/fabric';
9
+ /** Whether the node is `@expo/ui`'s button, which commits as `Button` in the `ExpoUI` module. */
10
+ export const isExpoUiButton = (node) => /ExpoUI_Button$/.test(node.viewName);
11
+ /** The text the button draws: what is written inside it, as SwiftUI's or Compose's text. */
12
+ function drawnText(node) {
13
+ if (node.viewName === 'RawText' || /ExpoUI_TextView$/.test(node.viewName)) {
14
+ return String(node.props['text'] ?? '');
15
+ }
16
+ return node.children.map(drawnText).join('');
17
+ }
18
+ /** What a screen reader reads out for the button: SwiftUI's `label`, or the text inside it. */
19
+ export const expoUiButtonName = (node) => typeof node.props['label'] === 'string' ? node.props['label'] : drawnText(node);
20
+ /**
21
+ * Whether SwiftUI's `disabled` modifier has turned the button off. Compose has no such modifier,
22
+ * and its button draws and takes taps the same with it.
23
+ */
24
+ export function expoUiButtonDisabled(node) {
25
+ const modifiers = node.props['modifiers'];
26
+ return (nativePlatform() !== 'android' &&
27
+ Array.isArray(modifiers) &&
28
+ modifiers.some((modifier) => modifier.$type === 'disabled' && modifier.disabled === true));
29
+ }
30
+ /** The event a tap on the button fires, by the platform it was rendered for. */
31
+ export const expoUiButtonPress = () => nativePlatform() === 'android' ? 'topButtonPressed' : 'topButtonPress';
32
+ //# sourceMappingURL=expo-ui-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expo-ui-button.js","sourceRoot":"","sources":["../src/expo-ui-button.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,iGAAiG;AACjG,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAoB,EAAW,EAAE,CAC9D,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAEvC,4FAA4F;AAC5F,SAAS,SAAS,CAAC,IAAoB;IACrC,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1E,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,+FAA+F;AAC/F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAoB,EAAU,EAAE,CAC/D,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAElF;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAoB;IACvD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC1C,OAAO,CACL,cAAc,EAAE,KAAK,SAAS;QAC9B,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;QACxB,SAAS,CAAC,IAAI,CACZ,CAAC,QAAiD,EAAE,EAAE,CACpD,QAAQ,CAAC,KAAK,KAAK,UAAU,IAAI,QAAQ,CAAC,QAAQ,KAAK,IAAI,CAC9D,CACF,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAW,EAAE,CAC5C,cAAc,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,gBAAgB,CAAC","sourcesContent":["/**\n * `@expo/ui`'s button, SwiftUI's `Button` or Compose's, as the fake sees it.\n *\n * It commits no accessibility props: the native control is the button a screen reader reads out,\n * by the label it draws, and a tap on it fires `buttonPress` on iOS or `buttonPressed` on Android\n * rather than anything the responder hears. The queries and the press events read it from here.\n */\nimport { nativePlatform } from '@ng-native/fabric';\nimport type { FakeFabricNode } from './test-utils.ts';\n\n/** Whether the node is `@expo/ui`'s button, which commits as `Button` in the `ExpoUI` module. */\nexport const isExpoUiButton = (node: FakeFabricNode): boolean =>\n /ExpoUI_Button$/.test(node.viewName);\n\n/** The text the button draws: what is written inside it, as SwiftUI's or Compose's text. */\nfunction drawnText(node: FakeFabricNode): string {\n if (node.viewName === 'RawText' || /ExpoUI_TextView$/.test(node.viewName)) {\n return String(node.props['text'] ?? '');\n }\n return node.children.map(drawnText).join('');\n}\n\n/** What a screen reader reads out for the button: SwiftUI's `label`, or the text inside it. */\nexport const expoUiButtonName = (node: FakeFabricNode): string =>\n typeof node.props['label'] === 'string' ? node.props['label'] : drawnText(node);\n\n/**\n * Whether SwiftUI's `disabled` modifier has turned the button off. Compose has no such modifier,\n * and its button draws and takes taps the same with it.\n */\nexport function expoUiButtonDisabled(node: FakeFabricNode): boolean {\n const modifiers = node.props['modifiers'];\n return (\n nativePlatform() !== 'android' &&\n Array.isArray(modifiers) &&\n modifiers.some(\n (modifier: { $type?: unknown; disabled?: unknown }) =>\n modifier.$type === 'disabled' && modifier.disabled === true,\n )\n );\n}\n\n/** The event a tap on the button fires, by the platform it was rendered for. */\nexport const expoUiButtonPress = (): string =>\n nativePlatform() === 'android' ? 'topButtonPressed' : 'topButtonPress';\n"]}
package/dist/queries.d.ts CHANGED
@@ -1,15 +1,3 @@
1
- /**
2
- * Testing Library's queries, over the tree the renderer committed to the fake Fabric.
3
- *
4
- * There is no DOM here, so `@testing-library/dom` has nothing to query: a query reads the props a
5
- * native view would actually receive. That makes the names mean what React Native Testing Library
6
- * means by them rather than what the web does - a role is `accessibilityRole`, a label is
7
- * `accessibilityLabel`, text is what a `Paragraph` holds.
8
- *
9
- * Every query re-reads the most recent commit. The fake builds a fresh node object per commit, so
10
- * a node held across an interaction is a snapshot of the frame it was found in; query again to see
11
- * what changed.
12
- */
13
1
  import type { FakeFabricNode } from './test-utils.ts';
14
2
  import { type WaitForOptions } from './wait-for.ts';
15
3
  /** What a query matches against: a whole string (normalised), or a RegExp tested against it. */
@@ -73,7 +61,7 @@ export declare const flatten: (nodes: readonly FakeFabricNode[]) => FakeFabricNo
73
61
  export declare function withParents(nodes: readonly FakeFabricNode[], parent?: FakeFabricNode): readonly FakeFabricNode[];
74
62
  /** What a screen reader would read out: every `RawText` under the node, in order. */
75
63
  export declare function textContent(node: FakeFabricNode): string;
76
- /** A text field under any of the names the platforms register it by. */
64
+ /** A text field under either of the names the platforms commit it by. */
77
65
  export declare const isTextInput: (node: FakeFabricNode) => boolean;
78
66
  /** The navigation bar's search field, which reports text as `topChangeText` with no count. */
79
67
  export declare const isSearchBar: (node: FakeFabricNode) => boolean;
package/dist/queries.js CHANGED
@@ -1,4 +1,18 @@
1
+ /**
2
+ * Testing Library's queries, over the tree the renderer committed to the fake Fabric.
3
+ *
4
+ * There is no DOM here, so `@testing-library/dom` has nothing to query: a query reads the props a
5
+ * native view would actually receive. That makes the names mean what React Native Testing Library
6
+ * means by them rather than what the web does - a role is `accessibilityRole`, a label is
7
+ * `accessibilityLabel`, text is what a `Paragraph` holds.
8
+ *
9
+ * Every query re-reads the most recent commit. The fake builds a fresh node object per commit, so
10
+ * a node held across an interaction is a snapshot of the frame it was found in; query again to see
11
+ * what changed.
12
+ */
13
+ import { isTextInputView } from '@ng-native/fabric';
1
14
  import { waitFor } from "./wait-for.js";
15
+ import { expoUiButtonDisabled, expoUiButtonName, isExpoUiButton } from "./expo-ui-button.js";
2
16
  /** The states `ByRole` filters on. */
3
17
  const STATES = ['disabled', 'selected', 'checked', 'busy', 'expanded'];
4
18
  /** The roles React Native Testing Library reads `checked` for; any other has none. */
@@ -13,6 +27,13 @@ const UNSET = {
13
27
  };
14
28
  /** Whether a node can have the state at all: `checked` only on a checkbox, radio or switch. */
15
29
  const has = (node, state) => state !== 'checked' || CHECKABLE.has(node.props['accessibilityRole'] ?? node.props['role']);
30
+ /**
31
+ * The roles a query finds the node by: `accessibilityRole` and `role`. `@expo/ui`'s button is a
32
+ * button, and its `role` is SwiftUI's, which says what the button does (`destructive`).
33
+ */
34
+ const rolesOf = (node) => isExpoUiButton(node)
35
+ ? [node.props['accessibilityRole'] ?? 'button']
36
+ : [node.props['accessibilityRole'], node.props['role']];
16
37
  /**
17
38
  * A state as a screen reader has it: what the committed `accessibilityState` says, which the
18
39
  * components and the `aria-*` attributes write. Unset is false for the states that are off until
@@ -23,6 +44,8 @@ const has = (node, state) => state !== 'checked' || CHECKABLE.has(node.props['ac
23
44
  function stateOf(node, state) {
24
45
  if (state === 'disabled' && isTextInput(node) && node.props['editable'] === false)
25
46
  return true;
47
+ if (state === 'disabled' && isExpoUiButton(node) && expoUiButtonDisabled(node))
48
+ return true;
26
49
  if (!has(node, state))
27
50
  return undefined;
28
51
  const value = node.props['accessibilityState']?.[state];
@@ -84,20 +107,22 @@ export function textContent(node) {
84
107
  return String(node.props['text'] ?? '');
85
108
  return node.children.map(textContent).join('');
86
109
  }
87
- /** A text field under any of the names the platforms register it by. */
88
- export const isTextInput = (node) => /TextInput/.test(node.viewName);
110
+ /** A text field under either of the names the platforms commit it by. */
111
+ export const isTextInput = (node) => isTextInputView(node.viewName);
89
112
  /** The navigation bar's search field, which reports text as `topChangeText` with no count. */
90
113
  export const isSearchBar = (node) => node.viewName === 'RNSSearchBar';
91
114
  /** What a screen reader reads out: the label, or failing that the text inside. */
92
- const nameOf = (node) => typeof node.props['accessibilityLabel'] === 'string'
93
- ? node.props['accessibilityLabel']
94
- : textContent(node);
115
+ const nameOf = (node) => {
116
+ if (typeof node.props['accessibilityLabel'] === 'string')
117
+ return node.props['accessibilityLabel'];
118
+ return isExpoUiButton(node) ? expoUiButtonName(node) : textContent(node);
119
+ };
95
120
  const QUERIES = {
96
121
  Role: (role, options = {}) => ({
97
122
  describe: `role ${show(role)}` +
98
123
  (options.name === undefined ? '' : ` and name ${show(options.name)}`) +
99
124
  describeStates(options),
100
- select: (node) => (matches(node.props['accessibilityRole'], role) || matches(node.props['role'], role)) &&
125
+ select: (node) => rolesOf(node).some((value) => matches(value, role)) &&
101
126
  (options.name === undefined || matches(nameOf(node), options.name, options.exact)) &&
102
127
  inStates(node, options),
103
128
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,OAAO,EAAuB,MAAM,eAAe,CAAC;AA+B7D,sCAAsC;AACtC,MAAM,MAAM,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAU,CAAC;AAGhF,sFAAsF;AACtF,MAAM,SAAS,GAAyB,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEjF,iGAAiG;AACjG,MAAM,KAAK,GAA+C;IACxD,QAAQ,EAAE,KAAK;IACf,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,KAAK;IACX,QAAQ,EAAE,SAAS;CACpB,CAAC;AAEF,+FAA+F;AAC/F,MAAM,GAAG,GAAG,CAAC,IAAoB,EAAE,KAAY,EAAW,EAAE,CAC1D,KAAK,KAAK,SAAS,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;AAE9F;;;;;;GAMG;AACH,SAAS,OAAO,CAAC,IAAoB,EAAE,KAAY;IACjD,IAAI,KAAK,KAAK,UAAU,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC/F,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAyC,EAAE,CAAC,KAAK,CAAC,CAAC;IACjG,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACvF,CAAC;AAED,4DAA4D;AAC5D,MAAM,QAAQ,GAAG,CAAC,IAAoB,EAAE,OAAsB,EAAW,EAAE,CACzE,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;AAEnG,4DAA4D;AAC5D,MAAM,cAAc,GAAG,CAAC,OAAsB,EAAU,EAAE,CACxD,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,SAAS,CAAC;KACnD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;KACzD,IAAI,CAAC,EAAE,CAAC,CAAC;AAgCd,MAAM,SAAS,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAE7E,SAAS,OAAO,CAAC,KAAc,EAAE,OAAgB,EAAE,KAAK,GAAG,IAAI;IAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,OAAO,YAAY,MAAM,EAAE,CAAC;QAC9B,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;QACtB,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,OAAgB,EAAU,EAAE,CACxC,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;AAExE,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,KAAgC,EAAoB,EAAE,CAC5E,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAE7D;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,KAAgC,EAChC,MAAuB;IAEvB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,6FAA6F;QAC7F,IAAI,MAAM,IAAI,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,MAAM,IAAI,IAAI,CAAC;YAC7B,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,SAAS,KAAK,CAAC,IAAoB;IACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,OAAO,CACL,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,KAAK,MAAM,CAAC;QAC7D,KAAK,CAAC,6BAA6B,CAAC,KAAK,IAAI;QAC7C,KAAK,CAAC,aAAa,CAAC,KAAK,IAAI;QAC7B,KAAK,CAAC,2BAA2B,CAAC,KAAK,qBAAqB,CAC7D,CAAC;AACJ,CAAC;AAED,2DAA2D;AAC3D,MAAM,SAAS,GAAG,CAAC,KAAgC,EAAoB,EAAE,CACvE,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AAEpF,qFAAqF;AACrF,MAAM,UAAU,WAAW,CAAC,IAAoB;IAC9C,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACzE,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,wEAAwE;AACxE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAoB,EAAW,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAE9F,8FAA8F;AAC9F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAoB,EAAW,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,cAAc,CAAC;AAI/F,kFAAkF;AAClF,MAAM,MAAM,GAAG,CAAC,IAAoB,EAAU,EAAE,CAC9C,OAAO,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,QAAQ;IAClD,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC;IAClC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AAExB,MAAM,OAAO,GAAsD;IACjE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;QAC7B,QAAQ,EACN,QAAQ,IAAI,CAAC,IAAI,CAAC,EAAE;YACpB,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACrE,cAAc,CAAC,OAAO,CAAC;QACzB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACf,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;YACrF,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YAClF,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,gGAAgG;IAChG,2DAA2D;IAC3D,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;QAC7B,QAAQ,EAAE,QAAQ,IAAI,CAAC,IAAI,CAAC,EAAE;QAC9B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACf,IAAI,CAAC,QAAQ,KAAK,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC;KACnF,CAAC;IACF,+FAA+F;IAC/F,oFAAoF;IACpF,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;QACjC,QAAQ,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE;QAClC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACf,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC;YACpD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC;KACzD,CAAC;IACF,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;QACnC,QAAQ,EAAE,sBAAsB,IAAI,CAAC,KAAK,CAAC,EAAE;QAC7C,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC;KAClF,CAAC;IACF,eAAe,EAAE,CAAC,WAAW,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;QAC/C,QAAQ,EAAE,eAAe,IAAI,CAAC,WAAW,CAAC,EAAE;QAC5C,0FAA0F;QAC1F,4FAA4F;QAC5F,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACf,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC;YACxC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC;KACjE,CAAC;IACF,4FAA4F;IAC5F,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;QACtC,QAAQ,EAAE,iBAAiB,IAAI,CAAC,KAAK,CAAC,EAAE;QACxC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC;KACzF,CAAC;CACH,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,KAAsC,EACtC,IAAkB,EAClB,KAAqC;IAErC,MAAM,KAAK,GAA4B,EAAE,CAAC;IAE1C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAW,EAAE,CAAC;QAClD,MAAM,GAAG,GAAG,CAAC,GAAG,IAAe,EAAoB,EAAE;YACnD,MAAM,KAAK,GAAI,OAAO,CAAC,IAAI,CAAgC,CAAC,GAAG,IAAI,CAAC,CAAC;YACrE,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAiC,CAAC;YACxD,MAAM,IAAI,GAAG,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;YAClE,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC7D,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QACF,MAAM,QAAQ,GAAG,CAAC,IAAe,EAAU,EAAE,CAC1C,OAAO,CAAC,IAAI,CAAgC,CAAC,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC;QAClE,MAAM,GAAG,GAAG,CAAC,GAAG,IAAe,EAAkB,EAAE;YACjD,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YAC1B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC,CAAC,CAAE,CAAC;YACvC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM;gBACzB,CAAC,CAAC,SAAS,IAAI,CAAC,MAAM,eAAe,QAAQ,CAAC,IAAI,CAAC,mCAAmC,IAAI,gCAAgC;gBAC1H,CAAC,CAAC,8BAA8B,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;YACpD,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC;QAC7C,CAAC,CAAC;QACF,8FAA8F;QAC9F,0CAA0C;QAC1C,MAAM,KAAK,GAAG,CAAC,IAAe,EAA2C,EAAE,CACzE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAEtF,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;YAChD,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,MAAM;gBACd,MAAM,IAAI,KAAK,CAAC,8BAA8B,QAAQ,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC,CAAC;YAChF,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QACF,KAAK,CAAC,aAAa,IAAI,EAAE,CAAC,GAAG,GAAG,CAAC;QACjC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,GAAG,GAAG,CAAC;QAC5B,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;YAC/C,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YAC1B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YACzC,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QACzB,CAAC,CAAC;QACF,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;YAC9C,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YACrC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QAC/C,CAAC,CAAC;QACF,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;YACjD,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YACrC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAE,KAAK,CAAC,WAAW,IAAI,EAAE,CAAgB,CAAC,GAAG,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QACpF,CAAC,CAAC;IACJ,CAAC;IAED,OAAO,KAAqB,CAAC;AAC/B,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,KAAgC,EAAE,KAAK,GAAG,CAAC;IACtE,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzF,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;aACzF,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;aACrF,IAAI,CAAC,EAAE,CAAC,CAAC;QACZ,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC7E,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5F,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,MAAM,WAAW,GAAG;IAClB,QAAQ;IACR,UAAU;IACV,mBAAmB;IACnB,MAAM;IACN,oBAAoB;IACpB,aAAa;IACb,MAAM;CACP,CAAC","sourcesContent":["/**\n * Testing Library's queries, over the tree the renderer committed to the fake Fabric.\n *\n * There is no DOM here, so `@testing-library/dom` has nothing to query: a query reads the props a\n * native view would actually receive. That makes the names mean what React Native Testing Library\n * means by them rather than what the web does - a role is `accessibilityRole`, a label is\n * `accessibilityLabel`, text is what a `Paragraph` holds.\n *\n * Every query re-reads the most recent commit. The fake builds a fresh node object per commit, so\n * a node held across an interaction is a snapshot of the frame it was found in; query again to see\n * what changed.\n */\nimport type { FakeFabricNode } from './test-utils.ts';\nimport { waitFor, type WaitForOptions } from './wait-for.ts';\n\n/** What a query matches against: a whole string (normalised), or a RegExp tested against it. */\nexport type Matcher = string | RegExp;\n\n/** How a string `Matcher` compares. */\nexport interface TextMatchOptions {\n /** `false` matches a case-insensitive substring instead of the whole, trimmed string. */\n exact?: boolean;\n /**\n * Also find what nobody could see or reach: views under `display: none`, and those hidden from\n * accessibility. React Native Testing Library's option, and its default of `false`.\n */\n includeHiddenElements?: boolean;\n}\n\n/**\n * `ByRole`'s options: a role alone matches every node with it, `name` narrows it to one, and a\n * state narrows it to the nodes in that state, as React Native Testing Library's do.\n */\nexport interface ByRoleOptions extends TextMatchOptions {\n /** The accessible name: `accessibilityLabel`, or failing that the text inside the node. */\n name?: Matcher;\n /** Disabled, from `accessibilityState` or `aria-disabled`; a field that is not editable is. */\n disabled?: boolean;\n selected?: boolean;\n checked?: boolean | 'mixed';\n busy?: boolean;\n expanded?: boolean;\n}\n\n/** The states `ByRole` filters on. */\nconst STATES = ['disabled', 'selected', 'checked', 'busy', 'expanded'] as const;\ntype State = (typeof STATES)[number];\n\n/** The roles React Native Testing Library reads `checked` for; any other has none. */\nconst CHECKABLE: ReadonlySet<unknown> = new Set(['checkbox', 'radio', 'switch']);\n\n/** What a state is when nothing set it: off, or for `checked` and `expanded`, not had at all. */\nconst UNSET: Readonly<Record<State, false | undefined>> = {\n disabled: false,\n selected: false,\n checked: undefined,\n busy: false,\n expanded: undefined,\n};\n\n/** Whether a node can have the state at all: `checked` only on a checkbox, radio or switch. */\nconst has = (node: FakeFabricNode, state: State): boolean =>\n state !== 'checked' || CHECKABLE.has(node.props['accessibilityRole'] ?? node.props['role']);\n\n/**\n * A state as a screen reader has it: what the committed `accessibilityState` says, which the\n * components and the `aria-*` attributes write. Unset is false for the states that are off until\n * something says otherwise, and stays unset for `checked` and `expanded`, which a node only has\n * when it can be checked or expanded; `checked` only counts on a checkbox, radio or switch. React\n * Native Testing Library's rules.\n */\nfunction stateOf(node: FakeFabricNode, state: State): boolean | 'mixed' | undefined {\n if (state === 'disabled' && isTextInput(node) && node.props['editable'] === false) return true;\n if (!has(node, state)) return undefined;\n const value = (node.props['accessibilityState'] as Record<string, unknown> | undefined)?.[state];\n return value === true || value === false || value === 'mixed' ? value : UNSET[state];\n}\n\n/** Whether a node is in every state the options ask for. */\nconst inStates = (node: FakeFabricNode, options: ByRoleOptions): boolean =>\n STATES.every((state) => options[state] === undefined || stateOf(node, state) === options[state]);\n\n/** The states the options ask for, for an error message. */\nconst describeStates = (options: ByRoleOptions): string =>\n STATES.filter((state) => options[state] !== undefined)\n .map((state) => ` and ${state} ${String(options[state])}`)\n .join('');\n\ninterface QueryArgs {\n Role: [role: Matcher, options?: ByRoleOptions];\n Text: [text: Matcher, options?: TextMatchOptions];\n TestId: [testId: Matcher, options?: TextMatchOptions];\n LabelText: [label: Matcher, options?: TextMatchOptions];\n PlaceholderText: [placeholder: Matcher, options?: TextMatchOptions];\n DisplayValue: [value: Matcher, options?: TextMatchOptions];\n}\n\ntype Name = keyof QueryArgs;\ntype FindArgs<K extends Name> = [...QueryArgs[K], waitForOptions?: WaitForOptions];\n\n/**\n * The query matrix: `getBy`, `getAllBy`, `queryBy`, `queryAllBy`, `findBy` and `findAllBy`, for\n * each of `Role`, `Text`, `TestId`, `LabelText`, `PlaceholderText` and `DisplayValue`.\n */\nexport type BoundQueries = {\n [K in Name as `getBy${K}`]: (...args: QueryArgs[K]) => FakeFabricNode;\n} & {\n [K in Name as `getAllBy${K}`]: (...args: QueryArgs[K]) => FakeFabricNode[];\n} & {\n [K in Name as `queryBy${K}`]: (...args: QueryArgs[K]) => FakeFabricNode | null;\n} & {\n [K in Name as `queryAllBy${K}`]: (...args: QueryArgs[K]) => FakeFabricNode[];\n} & {\n [K in Name as `findBy${K}`]: (...args: FindArgs<K>) => Promise<FakeFabricNode>;\n} & {\n [K in Name as `findAllBy${K}`]: (...args: FindArgs<K>) => Promise<FakeFabricNode[]>;\n};\n\nconst normalize = (text: string): string => text.replace(/\\s+/g, ' ').trim();\n\nfunction matches(value: unknown, matcher: Matcher, exact = true): boolean {\n if (typeof value !== 'string') return false;\n const text = normalize(value);\n if (matcher instanceof RegExp) {\n matcher.lastIndex = 0;\n return matcher.test(text);\n }\n const wanted = normalize(matcher);\n return exact ? text === wanted : text.toLowerCase().includes(wanted.toLowerCase());\n}\n\nconst show = (matcher: Matcher): string =>\n matcher instanceof RegExp ? String(matcher) : JSON.stringify(matcher);\n\nexport const flatten = (nodes: readonly FakeFabricNode[]): FakeFabricNode[] =>\n nodes.flatMap((node) => [node, ...flatten(node.children)]);\n\n/**\n * `nodes` with each node's `parent` set through the tree beneath, so a test can go up from what a\n * query found: the row a text is in. Not enumerable, so a node compares and prints as it did.\n */\nexport function withParents(\n nodes: readonly FakeFabricNode[],\n parent?: FakeFabricNode,\n): readonly FakeFabricNode[] {\n for (const node of nodes) {\n // The nodes a walk starts from keep the parent they have: `within(row)` starts from the row.\n if (parent || !('parent' in node)) {\n const value = parent ?? null;\n Object.defineProperty(node, 'parent', { value, configurable: true, writable: true });\n }\n withParents(node.children, node);\n }\n return nodes;\n}\n\n/**\n * A view that takes itself and everything under it out of sight or out of the accessibility tree,\n * as React Native Testing Library's `isHiddenFromAccessibility` decides it. And one with no\n * opacity that takes no touch, which nobody can see or reach: a row a list keeps to use again.\n * An element that is `display: none` has no view to ask about.\n */\nfunction hides(node: FakeFabricNode): boolean {\n const props = node.props;\n return (\n (props['opacity'] === 0 && props['pointerEvents'] === 'none') ||\n props['accessibilityElementsHidden'] === true ||\n props['aria-hidden'] === true ||\n props['importantForAccessibility'] === 'no-hide-descendants'\n );\n}\n\n/** Every node someone using the app could see or reach. */\nconst reachable = (nodes: readonly FakeFabricNode[]): FakeFabricNode[] =>\n nodes.flatMap((node) => (hides(node) ? [] : [node, ...reachable(node.children)]));\n\n/** What a screen reader would read out: every `RawText` under the node, in order. */\nexport function textContent(node: FakeFabricNode): string {\n if (node.viewName === 'RawText') return String(node.props['text'] ?? '');\n return node.children.map(textContent).join('');\n}\n\n/** A text field under any of the names the platforms register it by. */\nexport const isTextInput = (node: FakeFabricNode): boolean => /TextInput/.test(node.viewName);\n\n/** The navigation bar's search field, which reports text as `topChangeText` with no count. */\nexport const isSearchBar = (node: FakeFabricNode): boolean => node.viewName === 'RNSSearchBar';\n\ntype Query = { describe: string; select: (node: FakeFabricNode) => boolean };\n\n/** What a screen reader reads out: the label, or failing that the text inside. */\nconst nameOf = (node: FakeFabricNode): string =>\n typeof node.props['accessibilityLabel'] === 'string'\n ? node.props['accessibilityLabel']\n : textContent(node);\n\nconst QUERIES: { [K in Name]: (...args: QueryArgs[K]) => Query } = {\n Role: (role, options = {}) => ({\n describe:\n `role ${show(role)}` +\n (options.name === undefined ? '' : ` and name ${show(options.name)}`) +\n describeStates(options),\n select: (node) =>\n (matches(node.props['accessibilityRole'], role) || matches(node.props['role'], role)) &&\n (options.name === undefined || matches(nameOf(node), options.name, options.exact)) &&\n inStates(node, options),\n }),\n // `Paragraph` alone: a nested `<text>` commits as a `Text` span inside one, so the paragraph is\n // the whole run of text and the span is only a part of it.\n Text: (text, options = {}) => ({\n describe: `text ${show(text)}`,\n select: (node) =>\n node.viewName === 'Paragraph' && matches(textContent(node), text, options.exact),\n }),\n // Both, because the components commit both: `testID` from `testID`, `nativeID` from `nativeID`\n // or its web spelling `id`, which is what most templates in this project reach for.\n TestId: (testId, options = {}) => ({\n describe: `testID ${show(testId)}`,\n select: (node) =>\n matches(node.props['testID'], testId, options.exact) ||\n matches(node.props['nativeID'], testId, options.exact),\n }),\n LabelText: (label, options = {}) => ({\n describe: `accessibilityLabel ${show(label)}`,\n select: (node) => matches(node.props['accessibilityLabel'], label, options.exact),\n }),\n PlaceholderText: (placeholder, options = {}) => ({\n describe: `placeholder ${show(placeholder)}`,\n // A text field and the navigation bar's search field draw a placeholder. A wrapper with a\n // `placeholder` input of its own has the attribute on its host view too, which nobody sees.\n select: (node) =>\n (isTextInput(node) || isSearchBar(node)) &&\n matches(node.props['placeholder'], placeholder, options.exact),\n }),\n // A text field's value is its `text` prop, which is what `<text-input>` binds its model to.\n DisplayValue: (value, options = {}) => ({\n describe: `display value ${show(value)}`,\n select: (node) => isTextInput(node) && matches(node.props['text'], value, options.exact),\n }),\n};\n\n/**\n * Every query, bound to a root that is looked up again on each call.\n *\n * `found` sees every node a query hands back, so an event fired on one later knows which render\n * it came from.\n */\nexport function bindQueries(\n roots: () => readonly FakeFabricNode[],\n tree: () => string,\n found: (node: FakeFabricNode) => void,\n): BoundQueries {\n const bound: Record<string, unknown> = {};\n\n for (const name of Object.keys(QUERIES) as Name[]) {\n const all = (...args: unknown[]): FakeFabricNode[] => {\n const query = (QUERIES[name] as (...a: unknown[]) => Query)(...args);\n const options = args[1] as TextMatchOptions | undefined;\n const walk = options?.includeHiddenElements ? flatten : reachable;\n const hits = walk(withParents(roots())).filter(query.select);\n hits.forEach(found);\n return hits;\n };\n const describe = (args: unknown[]): string =>\n (QUERIES[name] as (...a: unknown[]) => Query)(...args).describe;\n const one = (...args: unknown[]): FakeFabricNode => {\n const hits = all(...args);\n if (hits.length === 1) return hits[0]!;\n const problem = hits.length\n ? `Found ${hits.length} nodes with ${describe(args)}, and expected one. Use getAllBy${name} if more than one is expected.`\n : `Unable to find a node with ${describe(args)}.`;\n throw new Error(`${problem}\\n\\n${tree()}`);\n };\n // The last argument of a `findBy` is the `waitFor` options, and it is only ever that when the\n // query itself was given its options too.\n const split = (args: unknown[]): [unknown[], WaitForOptions | undefined] =>\n args.length > 2 ? [args.slice(0, 2), args[2] as WaitForOptions] : [args, undefined];\n\n bound[`getAllBy${name}`] = (...args: unknown[]) => {\n const hits = all(...args);\n if (!hits.length)\n throw new Error(`Unable to find a node with ${describe(args)}.\\n\\n${tree()}`);\n return hits;\n };\n bound[`queryAllBy${name}`] = all;\n bound[`getBy${name}`] = one;\n bound[`queryBy${name}`] = (...args: unknown[]) => {\n const hits = all(...args);\n if (hits.length > 1) return one(...args);\n return hits[0] ?? null;\n };\n bound[`findBy${name}`] = (...args: unknown[]) => {\n const [query, options] = split(args);\n return waitFor(() => one(...query), options);\n };\n bound[`findAllBy${name}`] = (...args: unknown[]) => {\n const [query, options] = split(args);\n return waitFor(() => (bound[`getAllBy${name}`] as typeof all)(...query), options);\n };\n }\n\n return bound as BoundQueries;\n}\n\n/** The debug tree: view names, text, and the props a query can find a node by. */\nexport function describeTree(nodes: readonly FakeFabricNode[], depth = 0): string {\n return nodes\n .map((node) => {\n const text = node.viewName === 'RawText' ? ` ${JSON.stringify(node.props['text'])}` : '';\n const props = IDENTIFYING.filter((key) => node.props[key] != null && node.props[key] !== '')\n .map((key) => ` ${key === 'text' ? 'value' : key}=${JSON.stringify(node.props[key])}`)\n .join('');\n const line = ' '.repeat(depth) + node.viewName + text + (text ? '' : props);\n return node.children.length ? `${line}\\n${describeTree(node.children, depth + 1)}` : line;\n })\n .join('\\n');\n}\n\nconst IDENTIFYING = [\n 'testID',\n 'nativeID',\n 'accessibilityRole',\n 'role',\n 'accessibilityLabel',\n 'placeholder',\n 'text',\n];\n"]}
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Testing Library's queries, over the tree the renderer committed to the fake Fabric.\n *\n * There is no DOM here, so `@testing-library/dom` has nothing to query: a query reads the props a\n * native view would actually receive. That makes the names mean what React Native Testing Library\n * means by them rather than what the web does - a role is `accessibilityRole`, a label is\n * `accessibilityLabel`, text is what a `Paragraph` holds.\n *\n * Every query re-reads the most recent commit. The fake builds a fresh node object per commit, so\n * a node held across an interaction is a snapshot of the frame it was found in; query again to see\n * what changed.\n */\nimport { isTextInputView } from '@ng-native/fabric';\nimport type { FakeFabricNode } from './test-utils.ts';\nimport { waitFor, type WaitForOptions } from './wait-for.ts';\nimport { expoUiButtonDisabled, expoUiButtonName, isExpoUiButton } from './expo-ui-button.ts';\n\n/** What a query matches against: a whole string (normalised), or a RegExp tested against it. */\nexport type Matcher = string | RegExp;\n\n/** How a string `Matcher` compares. */\nexport interface TextMatchOptions {\n /** `false` matches a case-insensitive substring instead of the whole, trimmed string. */\n exact?: boolean;\n /**\n * Also find what nobody could see or reach: views under `display: none`, and those hidden from\n * accessibility. React Native Testing Library's option, and its default of `false`.\n */\n includeHiddenElements?: boolean;\n}\n\n/**\n * `ByRole`'s options: a role alone matches every node with it, `name` narrows it to one, and a\n * state narrows it to the nodes in that state, as React Native Testing Library's do.\n */\nexport interface ByRoleOptions extends TextMatchOptions {\n /** The accessible name: `accessibilityLabel`, or failing that the text inside the node. */\n name?: Matcher;\n /** Disabled, from `accessibilityState` or `aria-disabled`; a field that is not editable is. */\n disabled?: boolean;\n selected?: boolean;\n checked?: boolean | 'mixed';\n busy?: boolean;\n expanded?: boolean;\n}\n\n/** The states `ByRole` filters on. */\nconst STATES = ['disabled', 'selected', 'checked', 'busy', 'expanded'] as const;\ntype State = (typeof STATES)[number];\n\n/** The roles React Native Testing Library reads `checked` for; any other has none. */\nconst CHECKABLE: ReadonlySet<unknown> = new Set(['checkbox', 'radio', 'switch']);\n\n/** What a state is when nothing set it: off, or for `checked` and `expanded`, not had at all. */\nconst UNSET: Readonly<Record<State, false | undefined>> = {\n disabled: false,\n selected: false,\n checked: undefined,\n busy: false,\n expanded: undefined,\n};\n\n/** Whether a node can have the state at all: `checked` only on a checkbox, radio or switch. */\nconst has = (node: FakeFabricNode, state: State): boolean =>\n state !== 'checked' || CHECKABLE.has(node.props['accessibilityRole'] ?? node.props['role']);\n\n/**\n * The roles a query finds the node by: `accessibilityRole` and `role`. `@expo/ui`'s button is a\n * button, and its `role` is SwiftUI's, which says what the button does (`destructive`).\n */\nconst rolesOf = (node: FakeFabricNode): unknown[] =>\n isExpoUiButton(node)\n ? [node.props['accessibilityRole'] ?? 'button']\n : [node.props['accessibilityRole'], node.props['role']];\n\n/**\n * A state as a screen reader has it: what the committed `accessibilityState` says, which the\n * components and the `aria-*` attributes write. Unset is false for the states that are off until\n * something says otherwise, and stays unset for `checked` and `expanded`, which a node only has\n * when it can be checked or expanded; `checked` only counts on a checkbox, radio or switch. React\n * Native Testing Library's rules.\n */\nfunction stateOf(node: FakeFabricNode, state: State): boolean | 'mixed' | undefined {\n if (state === 'disabled' && isTextInput(node) && node.props['editable'] === false) return true;\n if (state === 'disabled' && isExpoUiButton(node) && expoUiButtonDisabled(node)) return true;\n if (!has(node, state)) return undefined;\n const value = (node.props['accessibilityState'] as Record<string, unknown> | undefined)?.[state];\n return value === true || value === false || value === 'mixed' ? value : UNSET[state];\n}\n\n/** Whether a node is in every state the options ask for. */\nconst inStates = (node: FakeFabricNode, options: ByRoleOptions): boolean =>\n STATES.every((state) => options[state] === undefined || stateOf(node, state) === options[state]);\n\n/** The states the options ask for, for an error message. */\nconst describeStates = (options: ByRoleOptions): string =>\n STATES.filter((state) => options[state] !== undefined)\n .map((state) => ` and ${state} ${String(options[state])}`)\n .join('');\n\ninterface QueryArgs {\n Role: [role: Matcher, options?: ByRoleOptions];\n Text: [text: Matcher, options?: TextMatchOptions];\n TestId: [testId: Matcher, options?: TextMatchOptions];\n LabelText: [label: Matcher, options?: TextMatchOptions];\n PlaceholderText: [placeholder: Matcher, options?: TextMatchOptions];\n DisplayValue: [value: Matcher, options?: TextMatchOptions];\n}\n\ntype Name = keyof QueryArgs;\ntype FindArgs<K extends Name> = [...QueryArgs[K], waitForOptions?: WaitForOptions];\n\n/**\n * The query matrix: `getBy`, `getAllBy`, `queryBy`, `queryAllBy`, `findBy` and `findAllBy`, for\n * each of `Role`, `Text`, `TestId`, `LabelText`, `PlaceholderText` and `DisplayValue`.\n */\nexport type BoundQueries = {\n [K in Name as `getBy${K}`]: (...args: QueryArgs[K]) => FakeFabricNode;\n} & {\n [K in Name as `getAllBy${K}`]: (...args: QueryArgs[K]) => FakeFabricNode[];\n} & {\n [K in Name as `queryBy${K}`]: (...args: QueryArgs[K]) => FakeFabricNode | null;\n} & {\n [K in Name as `queryAllBy${K}`]: (...args: QueryArgs[K]) => FakeFabricNode[];\n} & {\n [K in Name as `findBy${K}`]: (...args: FindArgs<K>) => Promise<FakeFabricNode>;\n} & {\n [K in Name as `findAllBy${K}`]: (...args: FindArgs<K>) => Promise<FakeFabricNode[]>;\n};\n\nconst normalize = (text: string): string => text.replace(/\\s+/g, ' ').trim();\n\nfunction matches(value: unknown, matcher: Matcher, exact = true): boolean {\n if (typeof value !== 'string') return false;\n const text = normalize(value);\n if (matcher instanceof RegExp) {\n matcher.lastIndex = 0;\n return matcher.test(text);\n }\n const wanted = normalize(matcher);\n return exact ? text === wanted : text.toLowerCase().includes(wanted.toLowerCase());\n}\n\nconst show = (matcher: Matcher): string =>\n matcher instanceof RegExp ? String(matcher) : JSON.stringify(matcher);\n\nexport const flatten = (nodes: readonly FakeFabricNode[]): FakeFabricNode[] =>\n nodes.flatMap((node) => [node, ...flatten(node.children)]);\n\n/**\n * `nodes` with each node's `parent` set through the tree beneath, so a test can go up from what a\n * query found: the row a text is in. Not enumerable, so a node compares and prints as it did.\n */\nexport function withParents(\n nodes: readonly FakeFabricNode[],\n parent?: FakeFabricNode,\n): readonly FakeFabricNode[] {\n for (const node of nodes) {\n // The nodes a walk starts from keep the parent they have: `within(row)` starts from the row.\n if (parent || !('parent' in node)) {\n const value = parent ?? null;\n Object.defineProperty(node, 'parent', { value, configurable: true, writable: true });\n }\n withParents(node.children, node);\n }\n return nodes;\n}\n\n/**\n * A view that takes itself and everything under it out of sight or out of the accessibility tree,\n * as React Native Testing Library's `isHiddenFromAccessibility` decides it. And one with no\n * opacity that takes no touch, which nobody can see or reach: a row a list keeps to use again.\n * An element that is `display: none` has no view to ask about.\n */\nfunction hides(node: FakeFabricNode): boolean {\n const props = node.props;\n return (\n (props['opacity'] === 0 && props['pointerEvents'] === 'none') ||\n props['accessibilityElementsHidden'] === true ||\n props['aria-hidden'] === true ||\n props['importantForAccessibility'] === 'no-hide-descendants'\n );\n}\n\n/** Every node someone using the app could see or reach. */\nconst reachable = (nodes: readonly FakeFabricNode[]): FakeFabricNode[] =>\n nodes.flatMap((node) => (hides(node) ? [] : [node, ...reachable(node.children)]));\n\n/** What a screen reader would read out: every `RawText` under the node, in order. */\nexport function textContent(node: FakeFabricNode): string {\n if (node.viewName === 'RawText') return String(node.props['text'] ?? '');\n return node.children.map(textContent).join('');\n}\n\n/** A text field under either of the names the platforms commit it by. */\nexport const isTextInput = (node: FakeFabricNode): boolean => isTextInputView(node.viewName);\n\n/** The navigation bar's search field, which reports text as `topChangeText` with no count. */\nexport const isSearchBar = (node: FakeFabricNode): boolean => node.viewName === 'RNSSearchBar';\n\ntype Query = { describe: string; select: (node: FakeFabricNode) => boolean };\n\n/** What a screen reader reads out: the label, or failing that the text inside. */\nconst nameOf = (node: FakeFabricNode): string => {\n if (typeof node.props['accessibilityLabel'] === 'string') return node.props['accessibilityLabel'];\n return isExpoUiButton(node) ? expoUiButtonName(node) : textContent(node);\n};\n\nconst QUERIES: { [K in Name]: (...args: QueryArgs[K]) => Query } = {\n Role: (role, options = {}) => ({\n describe:\n `role ${show(role)}` +\n (options.name === undefined ? '' : ` and name ${show(options.name)}`) +\n describeStates(options),\n select: (node) =>\n rolesOf(node).some((value) => matches(value, role)) &&\n (options.name === undefined || matches(nameOf(node), options.name, options.exact)) &&\n inStates(node, options),\n }),\n // `Paragraph` alone: a nested `<text>` commits as a `Text` span inside one, so the paragraph is\n // the whole run of text and the span is only a part of it.\n Text: (text, options = {}) => ({\n describe: `text ${show(text)}`,\n select: (node) =>\n node.viewName === 'Paragraph' && matches(textContent(node), text, options.exact),\n }),\n // Both, because the components commit both: `testID` from `testID`, `nativeID` from `nativeID`\n // or its web spelling `id`, which is what most templates in this project reach for.\n TestId: (testId, options = {}) => ({\n describe: `testID ${show(testId)}`,\n select: (node) =>\n matches(node.props['testID'], testId, options.exact) ||\n matches(node.props['nativeID'], testId, options.exact),\n }),\n LabelText: (label, options = {}) => ({\n describe: `accessibilityLabel ${show(label)}`,\n select: (node) => matches(node.props['accessibilityLabel'], label, options.exact),\n }),\n PlaceholderText: (placeholder, options = {}) => ({\n describe: `placeholder ${show(placeholder)}`,\n // A text field and the navigation bar's search field draw a placeholder. A wrapper with a\n // `placeholder` input of its own has the attribute on its host view too, which nobody sees.\n select: (node) =>\n (isTextInput(node) || isSearchBar(node)) &&\n matches(node.props['placeholder'], placeholder, options.exact),\n }),\n // A text field's value is its `text` prop, which is what `<text-input>` binds its model to.\n DisplayValue: (value, options = {}) => ({\n describe: `display value ${show(value)}`,\n select: (node) => isTextInput(node) && matches(node.props['text'], value, options.exact),\n }),\n};\n\n/**\n * Every query, bound to a root that is looked up again on each call.\n *\n * `found` sees every node a query hands back, so an event fired on one later knows which render\n * it came from.\n */\nexport function bindQueries(\n roots: () => readonly FakeFabricNode[],\n tree: () => string,\n found: (node: FakeFabricNode) => void,\n): BoundQueries {\n const bound: Record<string, unknown> = {};\n\n for (const name of Object.keys(QUERIES) as Name[]) {\n const all = (...args: unknown[]): FakeFabricNode[] => {\n const query = (QUERIES[name] as (...a: unknown[]) => Query)(...args);\n const options = args[1] as TextMatchOptions | undefined;\n const walk = options?.includeHiddenElements ? flatten : reachable;\n const hits = walk(withParents(roots())).filter(query.select);\n hits.forEach(found);\n return hits;\n };\n const describe = (args: unknown[]): string =>\n (QUERIES[name] as (...a: unknown[]) => Query)(...args).describe;\n const one = (...args: unknown[]): FakeFabricNode => {\n const hits = all(...args);\n if (hits.length === 1) return hits[0]!;\n const problem = hits.length\n ? `Found ${hits.length} nodes with ${describe(args)}, and expected one. Use getAllBy${name} if more than one is expected.`\n : `Unable to find a node with ${describe(args)}.`;\n throw new Error(`${problem}\\n\\n${tree()}`);\n };\n // The last argument of a `findBy` is the `waitFor` options, and it is only ever that when the\n // query itself was given its options too.\n const split = (args: unknown[]): [unknown[], WaitForOptions | undefined] =>\n args.length > 2 ? [args.slice(0, 2), args[2] as WaitForOptions] : [args, undefined];\n\n bound[`getAllBy${name}`] = (...args: unknown[]) => {\n const hits = all(...args);\n if (!hits.length)\n throw new Error(`Unable to find a node with ${describe(args)}.\\n\\n${tree()}`);\n return hits;\n };\n bound[`queryAllBy${name}`] = all;\n bound[`getBy${name}`] = one;\n bound[`queryBy${name}`] = (...args: unknown[]) => {\n const hits = all(...args);\n if (hits.length > 1) return one(...args);\n return hits[0] ?? null;\n };\n bound[`findBy${name}`] = (...args: unknown[]) => {\n const [query, options] = split(args);\n return waitFor(() => one(...query), options);\n };\n bound[`findAllBy${name}`] = (...args: unknown[]) => {\n const [query, options] = split(args);\n return waitFor(() => (bound[`getAllBy${name}`] as typeof all)(...query), options);\n };\n }\n\n return bound as BoundQueries;\n}\n\n/** The debug tree: view names, text, and the props a query can find a node by. */\nexport function describeTree(nodes: readonly FakeFabricNode[], depth = 0): string {\n return nodes\n .map((node) => {\n const text = node.viewName === 'RawText' ? ` ${JSON.stringify(node.props['text'])}` : '';\n const props = IDENTIFYING.filter((key) => node.props[key] != null && node.props[key] !== '')\n .map((key) => ` ${key === 'text' ? 'value' : key}=${JSON.stringify(node.props[key])}`)\n .join('');\n const line = ' '.repeat(depth) + node.viewName + text + (text ? '' : props);\n return node.children.length ? `${line}\\n${describeTree(node.children, depth + 1)}` : line;\n })\n .join('\\n');\n}\n\nconst IDENTIFYING = [\n 'testID',\n 'nativeID',\n 'accessibilityRole',\n 'role',\n 'accessibilityLabel',\n 'placeholder',\n 'text',\n];\n"]}
package/dist/render.js CHANGED
@@ -24,6 +24,28 @@ import { bindQueries, describeTree, flatten } from "./queries.js";
24
24
  import { createFakeFabric } from "./test-utils.js";
25
25
  import { waitFor } from "./wait-for.js";
26
26
  const mounted = [];
27
+ /** Expo's runtime global, as far as a render answers for it. */
28
+ const scope = globalThis;
29
+ /** The `expo` global a render put in place, removed once nothing is rendered. */
30
+ let expoStandIn;
31
+ /**
32
+ * The fake commits any view, so it stands for a build that has every one, and answers Expo's
33
+ * `getViewConfig` for each as that build would: a component that asks the build whether it has a
34
+ * view, as `ui-bottom-sheet` does for SwiftUI's toolbar, draws what a development build draws.
35
+ * Only where there is no `expo` global already, so a test's own stays as the test set it.
36
+ */
37
+ function standInForExpo() {
38
+ if (scope.expo)
39
+ return;
40
+ scope.expo = expoStandIn = { getViewConfig: () => ({}) };
41
+ }
42
+ /** Removes the stand-in once nothing is rendered, unless something has replaced it since. */
43
+ function releaseExpo() {
44
+ if (mounted.length === 0 && expoStandIn && scope.expo === expoStandIn) {
45
+ delete scope.expo;
46
+ expoStandIn = undefined;
47
+ }
48
+ }
27
49
  /** Which render a node a query returned belongs to, so an event on it reaches the right fake. */
28
50
  const owners = new WeakMap();
29
51
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -136,14 +158,23 @@ export async function render(component, options = {}) {
136
158
  const fabric = createFakeFabric();
137
159
  if (!isTemplate)
138
160
  refuseUnknownInputs(type, inputs);
139
- const app = mount(1, type, fabric, {
140
- ...engine,
141
- providers,
142
- inputs: isTemplate ? {} : inputs,
143
- // As a template would bind them, so an output a host directive forwards is heard, by the name
144
- // it is forwarded under, and a name that is no output is refused by Angular, by name.
145
- bindings: Object.entries(on ?? {}).map(([name, handler]) => outputBinding(name, handler)),
146
- });
161
+ standInForExpo();
162
+ let app;
163
+ try {
164
+ app = mount(1, type, fabric, {
165
+ ...engine,
166
+ providers,
167
+ inputs: isTemplate ? {} : inputs,
168
+ // As a template would bind them, so an output a host directive forwards is heard, by the
169
+ // name it is forwarded under, and a name that is no output is refused by Angular, by name.
170
+ bindings: Object.entries(on ?? {}).map(([name, handler]) => outputBinding(name, handler)),
171
+ });
172
+ }
173
+ catch (error) {
174
+ // Never mounted, so no unmount will take the stand-in away.
175
+ releaseExpo();
176
+ throw error;
177
+ }
147
178
  const self = {
148
179
  fabric,
149
180
  engine: app.engine,
@@ -189,7 +220,12 @@ function unmount(target) {
189
220
  if (at === -1)
190
221
  return;
191
222
  mounted.splice(at, 1);
192
- target.applicationRef.destroy();
223
+ try {
224
+ target.applicationRef.destroy();
225
+ }
226
+ finally {
227
+ releaseExpo();
228
+ }
193
229
  }
194
230
  /**
195
231
  * Unmount everything rendered so far, and destroy every app `injectService` created. Registered
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `render()`, `screen` and `within()`, shaped like Angular Testing Library's and running on the\n * fake Fabric instead of TestBed and a DOM.\n *\n * A render is a real `mount()` - the same bootstrap an app runs, with the same renderer and engine\n * - onto a `createFakeFabric()`, so what a query reads is what native would have been handed.\n */\nimport {\n ChangeDetectorRef,\n Component,\n // The rule is for templates an app ships, which are checked when they are compiled. The one use\n // here is the host a test's template string is compiled into: see `hostFor`.\n // eslint-disable-next-line no-restricted-syntax\n NO_ERRORS_SCHEMA,\n outputBinding,\n type ApplicationRef,\n type ComponentRef,\n type EnvironmentProviders,\n type Provider,\n type Type,\n} from '@angular/core';\nimport type { Engine, EngineOptions } from '@ng-native/fabric';\nimport { mount } from '@ng-native/platform';\nimport { destroyServiceApps } from './inject-service.ts';\nimport { bindQueries, describeTree, flatten, type BoundQueries } from './queries.ts';\nimport { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';\nimport { waitFor, type WaitForOptions } from './wait-for.ts';\n\n/**\n * What `render()` takes: Angular Testing Library's options, plus the engine's own.\n *\n * The engine options (`globalStyles`, `conditions`, `tokens` and the rest) are what `mount()`\n * takes on a device, so a test styles a component with the same compiled sheet an app would.\n */\nexport interface RenderOptions<T> extends EngineOptions {\n /** The component's inputs, set before its first change detection, so `required` ones work. */\n inputs?: Record<string, unknown>;\n /** Output handlers, by output name. */\n on?: Record<string, (value: never) => void>;\n /** Environment providers for the app the component is mounted in. */\n providers?: (Provider | EnvironmentProviders)[];\n /** The template form only: what the template may use. */\n imports?: unknown[];\n /** Fields set on the instance: the template form's host, or the component itself. */\n componentProperties?: Partial<T> & Record<string, unknown>;\n}\n\n/**\n * What `render()` resolves to: every query, bound to this render, and the handles to drive it.\n *\n * The queries here stay on this render even after a later one; `screen` moves to the latest.\n */\nexport interface RenderResult<T> extends BoundQueries {\n fabric: FakeFabric;\n componentRef: ComponentRef<T>;\n instance: T;\n /** `ComponentFixture`'s shape, for tests written against it. */\n fixture: {\n componentRef: ComponentRef<T>;\n componentInstance: T;\n detectChanges(): Promise<void>;\n };\n /** Set inputs and properties again, then let the result commit. */\n rerender(changes?: {\n inputs?: Record<string, unknown>;\n componentProperties?: Record<string, unknown>;\n }): Promise<void>;\n /** Run change detection and wait for the commit. */\n detectChanges(): Promise<void>;\n /** Print the committed tree, or one node's subtree. */\n debug(node?: FakeFabricNode): void;\n unmount(): void;\n}\n\ninterface Mounted {\n fabric: FakeFabric;\n engine: Engine;\n applicationRef: ApplicationRef;\n componentRef: ComponentRef<unknown>;\n}\n\nconst mounted: Mounted[] = [];\n/** Which render a node a query returned belongs to, so an event on it reaches the right fake. */\nconst owners = new WeakMap<FakeFabricNode, Mounted>();\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Let zoneless change detection run and commit.\n *\n * One macrotask is enough for a change Angular sees: it schedules its pass on `setTimeout(0)`\n * (racing an animation frame), so a timeout queued after the change runs after the pass. A change\n * the engine sees outside change detection, such as a modal's reported dismissal with no\n * `(dismiss)` listener, commits on the platform's next frame instead, 16ms away in Node, so this\n * waits for that frame while a commit is owed.\n */\nexport async function settle(): Promise<void> {\n await sleep(0);\n for (let frames = 0; frames < 3 && mounted.some((m) => m.engine.pending); frames++) {\n await sleep(16);\n }\n}\n\nfunction latest(): Mounted {\n const current = mounted.at(-1);\n if (!current) throw new Error('Nothing is rendered: call render() first.');\n return current;\n}\n\nexport function ownerOf(node: FakeFabricNode): Mounted {\n return owners.get(node) ?? latest();\n}\n\n/** The node as it is in the most recent commit, found by its tag, or null once it is gone. */\nexport function currentOf(node: FakeFabricNode): FakeFabricNode | null {\n const hit = flatten(ownerOf(node).fabric.committed).find((n) => n.reactTag === node.reactTag);\n if (hit) owners.set(hit, ownerOf(node));\n return hit ?? null;\n}\n\nconst tree = (nodes: readonly FakeFabricNode[]): string => describeTree(nodes) || '(nothing)';\n\nfunction queriesFor(roots: () => readonly FakeFabricNode[], owner: () => Mounted): BoundQueries {\n return bindQueries(\n roots,\n () => tree(roots()),\n (node) => owners.set(node, owner()),\n );\n}\n\n/** The part of a component or directive definition that says what can be bound on its host. */\ninterface Bindable {\n readonly inputs?: Readonly<Record<string, unknown>>;\n readonly hostDirectives?: readonly (HostDirective | (() => HostDirective))[] | null;\n}\ninterface HostDirective {\n readonly inputs?: Readonly<Record<string, string>>;\n}\n\n/**\n * The names a template can bind as inputs on `type`: its own, by alias where one has one, and\n * those its host directives forward, by the name each is forwarded under. Null where the\n * definition cannot be read, and nothing is checked.\n */\nfunction inputNames(type: Type<unknown>): string[] | null {\n const def = (type as { ɵcmp?: Bindable }).ɵcmp;\n if (!def?.inputs) return null;\n const forwarded = (def.hostDirectives ?? []).flatMap((entry) =>\n Object.values((typeof entry === 'function' ? entry() : entry).inputs ?? {}),\n );\n return [...Object.keys(def.inputs), ...forwarded];\n}\n\n/**\n * Throws for a name in `inputs` that is no input of `type`. Angular only logs one, and renders the\n * component with its defaults. Checked before any input is set, so a refused batch changes nothing\n * and a required input left unset by a misspelling is named for what it is.\n */\nfunction refuseUnknownInputs(type: Type<unknown>, inputs: Record<string, unknown> = {}): void {\n const known = inputNames(type);\n if (!known) return;\n const unknown = Object.keys(inputs).filter((name) => !known.includes(name));\n if (!unknown.length) return;\n throw new Error(\n `[angular-native] ${type.name} has no input named ` +\n `${unknown.map((name) => `'${name}'`).join(', ')}. ` +\n (known.length ? `Its inputs are ${known.join(', ')}.` : 'It has no inputs.'),\n );\n}\n\n/**\n * The template form, compiled just in time.\n *\n * ponytail: this needs `@angular/compiler` at runtime, the one place anything here does - the\n * class form is AOT like the app. An AOT compile of the string would mean evaluating generated\n * code that imports the caller's own classes by name; do that if JIT ever disagrees with AOT.\n */\n/** Wrappers built so far: each takes the next as a host attribute, so no two share an ID. */\nlet wrappers = 0;\n\nasync function hostFor<T>(template: string, options: RenderOptions<T>): Promise<Type<unknown>> {\n await import('@angular/compiler' as string);\n const properties = options.componentProperties ?? {};\n class WrapperComponent {\n constructor() {\n Object.assign(this, properties);\n }\n }\n return Component({\n selector: 'ng-native-wrapper',\n // Angular derives a component's ID from its metadata, not its template's text, so wrappers\n // alike in shape collided (NG0912) without it.\n host: { 'data-wrapper': String(++wrappers) },\n template,\n imports: (options.imports ?? []) as Type<unknown>[],\n // A component's own host bindings are checked against the schemas of the template it is used\n // in, and only where that template was compiled just in time: the app's, compiled ahead, has\n // no such check. Without this every native prop a component binds on its host logs NG0303.\n // eslint-disable-next-line no-restricted-syntax -- as above\n schemas: [NO_ERRORS_SCHEMA],\n })(WrapperComponent);\n}\n\n/**\n * Mount a component, or a template string, onto a fresh fake Fabric and wait for the first commit.\n *\n * The same `mount()` an app boots with, so what the queries read is exactly what native would\n * have been sent. A string is compiled as a host component's template, with `imports` and\n * `componentProperties` for what it uses; a class is rendered as-is, with `inputs` set before its\n * first change detection so `input.required()` works.\n */\nexport async function render<T>(\n component: Type<T> | string,\n options: RenderOptions<T> = {},\n): Promise<RenderResult<T>> {\n const { inputs, on, providers, imports: _, componentProperties, ...engine } = options;\n const isTemplate = typeof component === 'string';\n const type = isTemplate ? await hostFor(component, options) : component;\n const fabric = createFakeFabric();\n if (!isTemplate) refuseUnknownInputs(type, inputs);\n const app = mount(1, type, fabric, {\n ...engine,\n providers,\n inputs: isTemplate ? {} : inputs,\n // As a template would bind them, so an output a host directive forwards is heard, by the name\n // it is forwarded under, and a name that is no output is refused by Angular, by name.\n bindings: Object.entries(on ?? {}).map(([name, handler]) =>\n outputBinding(name, handler as (value: unknown) => void),\n ),\n });\n const self: Mounted = {\n fabric,\n engine: app.engine,\n applicationRef: app.applicationRef,\n componentRef: app.componentRef,\n };\n mounted.push(self);\n\n const componentRef = app.componentRef as ComponentRef<T>;\n const instance = componentRef.instance as T & Record<string, unknown>;\n\n const detectChanges = async (): Promise<void> => {\n // The component's own view, not the host's: `componentRef.changeDetectorRef` marks the host\n // and upwards, which leaves an OnPush component with a changed field exactly as it was.\n componentRef.injector.get(ChangeDetectorRef).markForCheck();\n app.applicationRef.tick();\n await settle();\n };\n const rerender: RenderResult<T>['rerender'] = async (changes = {}) => {\n refuseUnknownInputs(type, changes.inputs);\n for (const [name, value] of Object.entries(changes.inputs ?? {})) {\n componentRef.setInput(name, value);\n }\n Object.assign(instance, changes.componentProperties);\n await detectChanges();\n };\n\n if (!isTemplate && componentProperties) await rerender({ componentProperties });\n else await settle();\n\n return {\n ...queriesFor(\n () => fabric.committed,\n () => self,\n ),\n fabric,\n componentRef,\n instance,\n fixture: { componentRef, componentInstance: instance, detectChanges },\n rerender,\n detectChanges,\n debug: (node) => console.log(tree(node ? [currentOf(node) ?? node] : fabric.committed)),\n unmount: () => unmount(self),\n };\n}\n\nfunction unmount(target: Mounted): void {\n const at = mounted.indexOf(target);\n if (at === -1) return;\n mounted.splice(at, 1);\n target.applicationRef.destroy();\n}\n\n/**\n * Unmount everything rendered so far, and destroy every app `injectService` created. Registered\n * as an `afterEach` where one is global.\n */\nexport function cleanup(): void {\n for (const target of [...mounted]) unmount(target);\n destroyServiceApps();\n}\n\n/** Queries over whatever was rendered most recently, as Testing Library's `screen` is. */\nexport const screen: BoundQueries & { debug(node?: FakeFabricNode): void } = {\n ...queriesFor(() => latest().fabric.committed, latest),\n debug: (node) => console.log(tree(node ? [currentOf(node) ?? node] : latest().fabric.committed)),\n};\n\n/** Queries scoped to one node and what is under it, in whichever commit is current. */\nexport function within(node: FakeFabricNode): BoundQueries {\n const owner = ownerOf(node);\n return queriesFor(\n () => {\n const current = currentOf(node);\n return current ? [current] : [];\n },\n () => owner,\n );\n}\n\ntype Present = FakeFabricNode | readonly FakeFabricNode[] | null | undefined;\n\n/** Wait for a node, or whatever a callback finds, to leave the committed tree. */\nexport async function waitForElementToBeRemoved(\n target: FakeFabricNode | readonly FakeFabricNode[] | (() => Present),\n options?: WaitForOptions,\n): Promise<void> {\n const present = (): boolean => {\n let found: Present;\n try {\n found = typeof target === 'function' ? target() : target;\n } catch {\n return false;\n }\n const nodes = found ? ([] as FakeFabricNode[]).concat(found) : [];\n return nodes.some((node) => currentOf(node) !== null);\n };\n if (!present()) {\n throw new Error(\n 'waitForElementToBeRemoved needs the node to be there when it starts; it already is not.',\n );\n }\n await waitFor(() => {\n if (present())\n throw new Error(`Still rendered after the timeout.\\n\\n${tree(latest().fabric.committed)}`);\n }, options);\n}\n\n// Testing Library's own convention: clean up after every test when the runner exposes `afterEach`\n// globally (Vitest with `globals: true`, Jest). Otherwise each `render()` stands alone.\nconst globalAfterEach = (globalThis as { afterEach?: (fn: () => void) => void }).afterEach;\nif (typeof globalAfterEach === 'function') globalAfterEach(cleanup);\n"]}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `render()`, `screen` and `within()`, shaped like Angular Testing Library's and running on the\n * fake Fabric instead of TestBed and a DOM.\n *\n * A render is a real `mount()` - the same bootstrap an app runs, with the same renderer and engine\n * - onto a `createFakeFabric()`, so what a query reads is what native would have been handed.\n */\nimport {\n ChangeDetectorRef,\n Component,\n // The rule is for templates an app ships, which are checked when they are compiled. The one use\n // here is the host a test's template string is compiled into: see `hostFor`.\n // eslint-disable-next-line no-restricted-syntax\n NO_ERRORS_SCHEMA,\n outputBinding,\n type ApplicationRef,\n type ComponentRef,\n type EnvironmentProviders,\n type Provider,\n type Type,\n} from '@angular/core';\nimport type { Engine, EngineOptions } from '@ng-native/fabric';\nimport { mount } from '@ng-native/platform';\nimport { destroyServiceApps } from './inject-service.ts';\nimport { bindQueries, describeTree, flatten, type BoundQueries } from './queries.ts';\nimport { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';\nimport { waitFor, type WaitForOptions } from './wait-for.ts';\n\n/**\n * What `render()` takes: Angular Testing Library's options, plus the engine's own.\n *\n * The engine options (`globalStyles`, `conditions`, `tokens` and the rest) are what `mount()`\n * takes on a device, so a test styles a component with the same compiled sheet an app would.\n */\nexport interface RenderOptions<T> extends EngineOptions {\n /** The component's inputs, set before its first change detection, so `required` ones work. */\n inputs?: Record<string, unknown>;\n /** Output handlers, by output name. */\n on?: Record<string, (value: never) => void>;\n /** Environment providers for the app the component is mounted in. */\n providers?: (Provider | EnvironmentProviders)[];\n /** The template form only: what the template may use. */\n imports?: unknown[];\n /** Fields set on the instance: the template form's host, or the component itself. */\n componentProperties?: Partial<T> & Record<string, unknown>;\n}\n\n/**\n * What `render()` resolves to: every query, bound to this render, and the handles to drive it.\n *\n * The queries here stay on this render even after a later one; `screen` moves to the latest.\n */\nexport interface RenderResult<T> extends BoundQueries {\n fabric: FakeFabric;\n componentRef: ComponentRef<T>;\n instance: T;\n /** `ComponentFixture`'s shape, for tests written against it. */\n fixture: {\n componentRef: ComponentRef<T>;\n componentInstance: T;\n detectChanges(): Promise<void>;\n };\n /** Set inputs and properties again, then let the result commit. */\n rerender(changes?: {\n inputs?: Record<string, unknown>;\n componentProperties?: Record<string, unknown>;\n }): Promise<void>;\n /** Run change detection and wait for the commit. */\n detectChanges(): Promise<void>;\n /** Print the committed tree, or one node's subtree. */\n debug(node?: FakeFabricNode): void;\n unmount(): void;\n}\n\ninterface Mounted {\n fabric: FakeFabric;\n engine: Engine;\n applicationRef: ApplicationRef;\n componentRef: ComponentRef<unknown>;\n}\n\nconst mounted: Mounted[] = [];\n\n/** Expo's runtime global, as far as a render answers for it. */\nconst scope = globalThis as { expo?: object };\n/** The `expo` global a render put in place, removed once nothing is rendered. */\nlet expoStandIn: object | undefined;\n\n/**\n * The fake commits any view, so it stands for a build that has every one, and answers Expo's\n * `getViewConfig` for each as that build would: a component that asks the build whether it has a\n * view, as `ui-bottom-sheet` does for SwiftUI's toolbar, draws what a development build draws.\n * Only where there is no `expo` global already, so a test's own stays as the test set it.\n */\nfunction standInForExpo(): void {\n if (scope.expo) return;\n scope.expo = expoStandIn = { getViewConfig: () => ({}) };\n}\n\n/** Removes the stand-in once nothing is rendered, unless something has replaced it since. */\nfunction releaseExpo(): void {\n if (mounted.length === 0 && expoStandIn && scope.expo === expoStandIn) {\n delete scope.expo;\n expoStandIn = undefined;\n }\n}\n/** Which render a node a query returned belongs to, so an event on it reaches the right fake. */\nconst owners = new WeakMap<FakeFabricNode, Mounted>();\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Let zoneless change detection run and commit.\n *\n * One macrotask is enough for a change Angular sees: it schedules its pass on `setTimeout(0)`\n * (racing an animation frame), so a timeout queued after the change runs after the pass. A change\n * the engine sees outside change detection, such as a modal's reported dismissal with no\n * `(dismiss)` listener, commits on the platform's next frame instead, 16ms away in Node, so this\n * waits for that frame while a commit is owed.\n */\nexport async function settle(): Promise<void> {\n await sleep(0);\n for (let frames = 0; frames < 3 && mounted.some((m) => m.engine.pending); frames++) {\n await sleep(16);\n }\n}\n\nfunction latest(): Mounted {\n const current = mounted.at(-1);\n if (!current) throw new Error('Nothing is rendered: call render() first.');\n return current;\n}\n\nexport function ownerOf(node: FakeFabricNode): Mounted {\n return owners.get(node) ?? latest();\n}\n\n/** The node as it is in the most recent commit, found by its tag, or null once it is gone. */\nexport function currentOf(node: FakeFabricNode): FakeFabricNode | null {\n const hit = flatten(ownerOf(node).fabric.committed).find((n) => n.reactTag === node.reactTag);\n if (hit) owners.set(hit, ownerOf(node));\n return hit ?? null;\n}\n\nconst tree = (nodes: readonly FakeFabricNode[]): string => describeTree(nodes) || '(nothing)';\n\nfunction queriesFor(roots: () => readonly FakeFabricNode[], owner: () => Mounted): BoundQueries {\n return bindQueries(\n roots,\n () => tree(roots()),\n (node) => owners.set(node, owner()),\n );\n}\n\n/** The part of a component or directive definition that says what can be bound on its host. */\ninterface Bindable {\n readonly inputs?: Readonly<Record<string, unknown>>;\n readonly hostDirectives?: readonly (HostDirective | (() => HostDirective))[] | null;\n}\ninterface HostDirective {\n readonly inputs?: Readonly<Record<string, string>>;\n}\n\n/**\n * The names a template can bind as inputs on `type`: its own, by alias where one has one, and\n * those its host directives forward, by the name each is forwarded under. Null where the\n * definition cannot be read, and nothing is checked.\n */\nfunction inputNames(type: Type<unknown>): string[] | null {\n const def = (type as { ɵcmp?: Bindable }).ɵcmp;\n if (!def?.inputs) return null;\n const forwarded = (def.hostDirectives ?? []).flatMap((entry) =>\n Object.values((typeof entry === 'function' ? entry() : entry).inputs ?? {}),\n );\n return [...Object.keys(def.inputs), ...forwarded];\n}\n\n/**\n * Throws for a name in `inputs` that is no input of `type`. Angular only logs one, and renders the\n * component with its defaults. Checked before any input is set, so a refused batch changes nothing\n * and a required input left unset by a misspelling is named for what it is.\n */\nfunction refuseUnknownInputs(type: Type<unknown>, inputs: Record<string, unknown> = {}): void {\n const known = inputNames(type);\n if (!known) return;\n const unknown = Object.keys(inputs).filter((name) => !known.includes(name));\n if (!unknown.length) return;\n throw new Error(\n `[angular-native] ${type.name} has no input named ` +\n `${unknown.map((name) => `'${name}'`).join(', ')}. ` +\n (known.length ? `Its inputs are ${known.join(', ')}.` : 'It has no inputs.'),\n );\n}\n\n/**\n * The template form, compiled just in time.\n *\n * ponytail: this needs `@angular/compiler` at runtime, the one place anything here does - the\n * class form is AOT like the app. An AOT compile of the string would mean evaluating generated\n * code that imports the caller's own classes by name; do that if JIT ever disagrees with AOT.\n */\n/** Wrappers built so far: each takes the next as a host attribute, so no two share an ID. */\nlet wrappers = 0;\n\nasync function hostFor<T>(template: string, options: RenderOptions<T>): Promise<Type<unknown>> {\n await import('@angular/compiler' as string);\n const properties = options.componentProperties ?? {};\n class WrapperComponent {\n constructor() {\n Object.assign(this, properties);\n }\n }\n return Component({\n selector: 'ng-native-wrapper',\n // Angular derives a component's ID from its metadata, not its template's text, so wrappers\n // alike in shape collided (NG0912) without it.\n host: { 'data-wrapper': String(++wrappers) },\n template,\n imports: (options.imports ?? []) as Type<unknown>[],\n // A component's own host bindings are checked against the schemas of the template it is used\n // in, and only where that template was compiled just in time: the app's, compiled ahead, has\n // no such check. Without this every native prop a component binds on its host logs NG0303.\n // eslint-disable-next-line no-restricted-syntax -- as above\n schemas: [NO_ERRORS_SCHEMA],\n })(WrapperComponent);\n}\n\n/**\n * Mount a component, or a template string, onto a fresh fake Fabric and wait for the first commit.\n *\n * The same `mount()` an app boots with, so what the queries read is exactly what native would\n * have been sent. A string is compiled as a host component's template, with `imports` and\n * `componentProperties` for what it uses; a class is rendered as-is, with `inputs` set before its\n * first change detection so `input.required()` works.\n */\nexport async function render<T>(\n component: Type<T> | string,\n options: RenderOptions<T> = {},\n): Promise<RenderResult<T>> {\n const { inputs, on, providers, imports: _, componentProperties, ...engine } = options;\n const isTemplate = typeof component === 'string';\n const type = isTemplate ? await hostFor(component, options) : component;\n const fabric = createFakeFabric();\n if (!isTemplate) refuseUnknownInputs(type, inputs);\n standInForExpo();\n let app: ReturnType<typeof mount>;\n try {\n app = mount(1, type, fabric, {\n ...engine,\n providers,\n inputs: isTemplate ? {} : inputs,\n // As a template would bind them, so an output a host directive forwards is heard, by the\n // name it is forwarded under, and a name that is no output is refused by Angular, by name.\n bindings: Object.entries(on ?? {}).map(([name, handler]) =>\n outputBinding(name, handler as (value: unknown) => void),\n ),\n });\n } catch (error) {\n // Never mounted, so no unmount will take the stand-in away.\n releaseExpo();\n throw error;\n }\n const self: Mounted = {\n fabric,\n engine: app.engine,\n applicationRef: app.applicationRef,\n componentRef: app.componentRef,\n };\n mounted.push(self);\n\n const componentRef = app.componentRef as ComponentRef<T>;\n const instance = componentRef.instance as T & Record<string, unknown>;\n\n const detectChanges = async (): Promise<void> => {\n // The component's own view, not the host's: `componentRef.changeDetectorRef` marks the host\n // and upwards, which leaves an OnPush component with a changed field exactly as it was.\n componentRef.injector.get(ChangeDetectorRef).markForCheck();\n app.applicationRef.tick();\n await settle();\n };\n const rerender: RenderResult<T>['rerender'] = async (changes = {}) => {\n refuseUnknownInputs(type, changes.inputs);\n for (const [name, value] of Object.entries(changes.inputs ?? {})) {\n componentRef.setInput(name, value);\n }\n Object.assign(instance, changes.componentProperties);\n await detectChanges();\n };\n\n if (!isTemplate && componentProperties) await rerender({ componentProperties });\n else await settle();\n\n return {\n ...queriesFor(\n () => fabric.committed,\n () => self,\n ),\n fabric,\n componentRef,\n instance,\n fixture: { componentRef, componentInstance: instance, detectChanges },\n rerender,\n detectChanges,\n debug: (node) => console.log(tree(node ? [currentOf(node) ?? node] : fabric.committed)),\n unmount: () => unmount(self),\n };\n}\n\nfunction unmount(target: Mounted): void {\n const at = mounted.indexOf(target);\n if (at === -1) return;\n mounted.splice(at, 1);\n try {\n target.applicationRef.destroy();\n } finally {\n releaseExpo();\n }\n}\n\n/**\n * Unmount everything rendered so far, and destroy every app `injectService` created. Registered\n * as an `afterEach` where one is global.\n */\nexport function cleanup(): void {\n for (const target of [...mounted]) unmount(target);\n destroyServiceApps();\n}\n\n/** Queries over whatever was rendered most recently, as Testing Library's `screen` is. */\nexport const screen: BoundQueries & { debug(node?: FakeFabricNode): void } = {\n ...queriesFor(() => latest().fabric.committed, latest),\n debug: (node) => console.log(tree(node ? [currentOf(node) ?? node] : latest().fabric.committed)),\n};\n\n/** Queries scoped to one node and what is under it, in whichever commit is current. */\nexport function within(node: FakeFabricNode): BoundQueries {\n const owner = ownerOf(node);\n return queriesFor(\n () => {\n const current = currentOf(node);\n return current ? [current] : [];\n },\n () => owner,\n );\n}\n\ntype Present = FakeFabricNode | readonly FakeFabricNode[] | null | undefined;\n\n/** Wait for a node, or whatever a callback finds, to leave the committed tree. */\nexport async function waitForElementToBeRemoved(\n target: FakeFabricNode | readonly FakeFabricNode[] | (() => Present),\n options?: WaitForOptions,\n): Promise<void> {\n const present = (): boolean => {\n let found: Present;\n try {\n found = typeof target === 'function' ? target() : target;\n } catch {\n return false;\n }\n const nodes = found ? ([] as FakeFabricNode[]).concat(found) : [];\n return nodes.some((node) => currentOf(node) !== null);\n };\n if (!present()) {\n throw new Error(\n 'waitForElementToBeRemoved needs the node to be there when it starts; it already is not.',\n );\n }\n await waitFor(() => {\n if (present())\n throw new Error(`Still rendered after the timeout.\\n\\n${tree(latest().fabric.committed)}`);\n }, options);\n}\n\n// Testing Library's own convention: clean up after every test when the runner exposes `afterEach`\n// globally (Vitest with `globals: true`, Jest). Otherwise each `render()` stands alone.\nconst globalAfterEach = (globalThis as { afterEach?: (fn: () => void) => void }).afterEach;\nif (typeof globalAfterEach === 'function') globalAfterEach(cleanup);\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-native/testing",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Test Angular Native components in Node with no simulator: a fake Fabric, Testing Library queries and events, and Vitest and node:test integration.",
5
5
  "keywords": [
6
6
  "angular",
@@ -34,16 +34,16 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@ng-native/fabric": "0.17.1",
38
- "@ng-native/metro": "0.17.1",
39
- "@ng-native/platform": "0.17.1"
37
+ "@ng-native/fabric": "0.18.0",
38
+ "@ng-native/metro": "0.18.0",
39
+ "@ng-native/platform": "0.18.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@angular/compiler": "^22.0.0",
43
43
  "@angular/core": "^22.0.0",
44
44
  "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
45
45
  "vitest": "^4.0.8 || ^5.0.0",
46
- "@ng-native/components": "0.17.1"
46
+ "@ng-native/components": "0.18.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@angular/compiler": {
@@ -62,8 +62,9 @@
62
62
  "@angular/core": "22.2.0",
63
63
  "@angular/forms": "22.2.0",
64
64
  "@angular/router": "22.2.0",
65
- "@ng-native/components": "0.17.1",
66
- "@ng-native/router": "0.17.1",
65
+ "@ng-native/components": "0.18.0",
66
+ "@ng-native/device": "0.18.0",
67
+ "@ng-native/router": "0.18.0",
67
68
  "vite": "^8.3.1",
68
69
  "vitest": "^5.0.2"
69
70
  },
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Takes from `Intl` the constructors Hermes, the engine an app runs on, does not have. Hermes has
3
+ * `Intl.Collator`, `Intl.DateTimeFormat` and `Intl.NumberFormat`; Node has the ones below as well,
4
+ * so code that builds one of them passes its tests and then throws `TypeError: undefined cannot be
5
+ * used as a constructor` on a device. Without them here, it throws a `TypeError` in the test too.
6
+ *
7
+ * `ngNative()` runs this ahead of the app's own setup files unless it is given `intl: 'node'`, and
8
+ * `register.mjs` imports it. A polyfill installed after it sees `Intl` as Hermes has it. Only
9
+ * Node's own constructors are removed, so a polyfill installed before it, such as one given to
10
+ * `node --import` ahead of `register.mjs`, stays.
11
+ */
12
+ const LACKING = [
13
+ 'DisplayNames',
14
+ 'DurationFormat',
15
+ 'ListFormat',
16
+ 'Locale',
17
+ 'PluralRules',
18
+ 'RelativeTimeFormat',
19
+ 'Segmenter',
20
+ ];
21
+
22
+ const native = (value) =>
23
+ typeof value === 'function' && Function.prototype.toString.call(value).includes('[native code]');
24
+
25
+ for (const name of LACKING) if (native(Intl[name])) delete Intl[name];
@@ -9,6 +9,8 @@
9
9
  * `__DEV__` is true for the same reason.
10
10
  */
11
11
  import { createRequire, register } from 'node:module';
12
+ // Only the `Intl` constructors Hermes has. See `hermes-intl.mjs`.
13
+ import './hermes-intl.mjs';
12
14
 
13
15
  createRequire(import.meta.url)('@ng-native/metro/polyfills/animation-globals.js');
14
16
  // Metro's prelude defines this in every bundle, and app code reads it bare.
@@ -11,6 +11,12 @@ export interface NgNativeOptions {
11
11
  * list the Metro preset takes as `libraryStyles`, so a test draws what the app draws.
12
12
  */
13
13
  libraryStyles?: string[] | false;
14
+ /**
15
+ * Which `Intl` a test runs with. `'hermes'`, the default, has only the constructors Hermes has
16
+ * (`Collator`, `DateTimeFormat` and `NumberFormat`), so code that builds another throws in a test
17
+ * as it does on a device. `'node'` keeps all of Node's, for an app whose tests rely on them.
18
+ */
19
+ intl?: 'hermes' | 'node';
14
20
  }
15
21
 
16
22
  /** Compile Angular for Vitest: AOT for decorated sources, the linker for partial-compiled packages. */
package/runner/vitest.mjs CHANGED
@@ -8,8 +8,9 @@
8
8
  * - It inlines. Vitest hands `node_modules` straight to Node by default, which would mean an
9
9
  * `@ng-native/*` package's `.ts` source reaching a runtime that refuses to strip types under
10
10
  * `node_modules`, and `@angular/*` reaching it unlinked. Inlined, both come through the plugin.
11
- * - It adds a setup file with the globals Angular reads as it is first evaluated, and runs ES
12
- * modules without the CommonJS variables Vitest otherwise injects into them.
11
+ * - It adds a setup file with the globals Angular reads as it is first evaluated, and one that
12
+ * leaves `Intl` with only the constructors Hermes has (`hermes-intl.mjs`, unless `intl: 'node'`),
13
+ * and runs ES modules without the CommonJS variables Vitest otherwise injects into them.
13
14
  * - It stubs assets. With no `require`, an app's `require('./logo.png')` would throw; it becomes
14
15
  * `{ testUri }` instead. And it stands in for the gesture and animation entry points, whose
15
16
  * React Native source Node cannot load (`STAND_INS`).
@@ -40,10 +41,11 @@ import {
40
41
  } from './compile.mjs';
41
42
 
42
43
  const SETUP = fileURLToPath(new URL('./setup.mjs', import.meta.url));
44
+ const HERMES_INTL = fileURLToPath(new URL('./hermes-intl.mjs', import.meta.url));
43
45
 
44
46
  /**
45
47
  * Entry points a test gets a stand-in for, because what they import is React Native source that
46
- * Node cannot load: the gestures and reanimated entry points of `@ng-native/components`, the
48
+ * Node cannot load: the gestures, animations and reanimated entry points of `@ng-native/components`, the
47
49
  * gesture library itself, which an app imports for `Gesture`, and Reanimated and worklets, which
48
50
  * an app imports for `withTiming` and `scheduleOnRN`. See each stand-in for what it does and does
49
51
  * not do.
@@ -52,6 +54,7 @@ const STAND_INS = Object.fromEntries(
52
54
  Object.entries({
53
55
  '@ng-native/components/gestures': 'gestures',
54
56
  'react-native-gesture-handler': 'gesture-handler',
57
+ '@ng-native/components/animations': 'animations',
55
58
  '@ng-native/components/reanimated': 'reanimated',
56
59
  'react-native-reanimated': 'reanimated-library',
57
60
  'react-native-worklets': 'worklets-library',
@@ -260,7 +263,8 @@ function compiled(context, source, file, options) {
260
263
  */
261
264
  function removeModuleCopies(vitest) {
262
265
  const dir = vitest._tmpDir;
263
- if (typeof dir !== 'string' || path.dirname(dir) !== path.resolve(tmpdir())) return;
266
+ // Vitest builds `_tmpDir` with `/` even on Windows; resolve it before comparing.
267
+ if (typeof dir !== 'string' || path.dirname(path.resolve(dir)) !== path.resolve(tmpdir())) return;
264
268
  vitest.onClose(() => rmSync(dir, { recursive: true, force: true }));
265
269
  }
266
270
 
@@ -268,7 +272,7 @@ function removeModuleCopies(vitest) {
268
272
  const cleaned = new WeakSet();
269
273
 
270
274
  /**
271
- * @param {{ inline?: (string | RegExp)[], libraryStyles?: string[] | false }} [options]
275
+ * @param {{ inline?: (string | RegExp)[], libraryStyles?: string[] | false, intl?: 'hermes' | 'node' }} [options]
272
276
  * @returns {import('vitest/config').Plugin}
273
277
  */
274
278
  export function ngNative(options = {}) {
@@ -295,7 +299,10 @@ export function ngNative(options = {}) {
295
299
  root = config.root;
296
300
  },
297
301
  // Once for each project; the directory is the Vitest instance's, so it is removed once.
298
- configureVitest({ vitest }) {
302
+ configureVitest({ vitest, project }) {
303
+ // Ahead of the app's own setup files, which `config` can only add after: a polyfill there
304
+ // that installs only what `Intl` lacks then sees it lacking, as it does on a device.
305
+ if (options.intl !== 'node') project.config.setupFiles.unshift(HERMES_INTL);
299
306
  if (cleaned.has(vitest)) return;
300
307
  cleaned.add(vitest);
301
308
  removeModuleCopies(vitest);