@ng-native/testing 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dom-component-page.d.ts +4 -0
- package/dist/dom-component-page.js +11 -0
- package/dist/dom-component-page.js.map +1 -0
- package/dist/events.js +25 -3
- package/dist/events.js.map +1 -1
- package/dist/queries.js +3 -1
- package/dist/queries.js.map +1 -1
- package/package.json +7 -7
- package/runner/compile.mjs +20 -0
- package/runner/loader.mjs +11 -2
- package/runner/vitest.mjs +32 -10
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use dom';
|
|
2
|
+
/**
|
|
3
|
+
* Fixture for the `dom-component` tests: a DOM component's file, with a `mountInWebView` that
|
|
4
|
+
* throws where the real one reads `window`. A test imports a reference to the page, never this.
|
|
5
|
+
*/
|
|
6
|
+
function mountInWebView(component) {
|
|
7
|
+
throw new Error(`The page was loaded in a test: ${String(component)}`);
|
|
8
|
+
}
|
|
9
|
+
export default mountInWebView(class Chart {
|
|
10
|
+
});
|
|
11
|
+
//# sourceMappingURL=dom-component-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-component-page.js","sourceRoot":"","sources":["../src/dom-component-page.ts"],"names":[],"mappings":"AAAA,SAAS,CAAC;AACV;;;GAGG;AACH,SAAS,cAAc,CAAC,SAAkB;IACxC,MAAM,IAAI,KAAK,CAAC,kCAAkC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;AACzE,CAAC;AAED,eAAe,cAAc,CAAC,MAAM,KAAK;CAAG,CAAC,CAAC","sourcesContent":["'use dom';\n/**\n * Fixture for the `dom-component` tests: a DOM component's file, with a `mountInWebView` that\n * throws where the real one reads `window`. A test imports a reference to the page, never this.\n */\nfunction mountInWebView(component: unknown): { domComponent: string } {\n throw new Error(`The page was loaded in a test: ${String(component)}`);\n}\n\nexport default mountInWebView(class Chart {});\n"]}
|
package/dist/events.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* rest, then the component's listener. Nothing here calls a handler directly, and a disabled
|
|
8
8
|
* pressable ignores a press here for the same reason it does on a phone.
|
|
9
9
|
*
|
|
10
|
+
* A touch is only sent where a finger could land, as React Native Testing Library decides it from
|
|
11
|
+
* `pointerEvents`: a press, a type or a clear on a node that takes no touch does nothing at all.
|
|
12
|
+
*
|
|
10
13
|
* Both are async where RNTL's `fireEvent` is not. Change detection is zoneless and commits on a
|
|
11
14
|
* later task, so an event has to be awaited before the tree it changed can be read.
|
|
12
15
|
*/
|
|
13
16
|
import { currentOf, ownerOf, settle } from "./render.js";
|
|
14
|
-
import { flatten, isSearchBar, isTextInput } from "./queries.js";
|
|
17
|
+
import { flatten, isSearchBar, isTextInput, withParents } from "./queries.js";
|
|
15
18
|
const nativeOf = (payload) => (payload && 'nativeEvent' in payload ? payload['nativeEvent'] : payload) ?? {};
|
|
16
19
|
function emit(node, type, nativeEvent = {}) {
|
|
17
20
|
ownerOf(node).fabric.emit(node, type, { target: node.reactTag, ...nativeEvent });
|
|
@@ -31,6 +34,21 @@ function touch(node, phase) {
|
|
|
31
34
|
};
|
|
32
35
|
return { ...point, touches: phase === 'start' ? [point] : [], changedTouches: [point] };
|
|
33
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether a finger could reach the node, by RNTL's reading of `pointerEvents` on the node as last
|
|
39
|
+
* committed: not when it is `none` or `box-none` itself, nor under a `none` or `box-only` view.
|
|
40
|
+
*/
|
|
41
|
+
function touchable(node) {
|
|
42
|
+
withParents(ownerOf(node).fabric.committed);
|
|
43
|
+
let blocking = 'box-none';
|
|
44
|
+
for (let at = currentOf(node) ?? node; at; at = at.parent ?? null) {
|
|
45
|
+
const pointerEvents = at.props['pointerEvents'];
|
|
46
|
+
if (pointerEvents === 'none' || pointerEvents === blocking)
|
|
47
|
+
return false;
|
|
48
|
+
blocking = 'box-only';
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
34
52
|
/** The text field at or under a node, so a query for its wrapper still reaches the input. */
|
|
35
53
|
function fieldOf(node) {
|
|
36
54
|
const current = currentOf(node) ?? node;
|
|
@@ -48,6 +66,8 @@ function changeText(field, text) {
|
|
|
48
66
|
emit(field, 'topChange', { text, eventCount: nextCount(field) });
|
|
49
67
|
}
|
|
50
68
|
async function press(node) {
|
|
69
|
+
if (!touchable(node))
|
|
70
|
+
return;
|
|
51
71
|
emit(node, 'topTouchStart', touch(node, 'start'));
|
|
52
72
|
emit(node, 'topTouchEnd', touch(node, 'end'));
|
|
53
73
|
await settle();
|
|
@@ -100,6 +120,8 @@ export const fireEvent = Object.assign(async (node, name, payload) => {
|
|
|
100
120
|
}, named);
|
|
101
121
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
102
122
|
async function hold(node, duration) {
|
|
123
|
+
if (!touchable(node))
|
|
124
|
+
return;
|
|
103
125
|
emit(node, 'topTouchStart', touch(node, 'start'));
|
|
104
126
|
await settle();
|
|
105
127
|
if (duration)
|
|
@@ -117,7 +139,7 @@ const user = {
|
|
|
117
139
|
longPress: (node, { duration = 500 } = {}) => hold(node, duration),
|
|
118
140
|
async type(node, text, options = {}) {
|
|
119
141
|
const field = fieldOf(node);
|
|
120
|
-
if (field.props['editable'] === false)
|
|
142
|
+
if (field.props['editable'] === false || !touchable(field))
|
|
121
143
|
return;
|
|
122
144
|
emit(field, 'topFocus');
|
|
123
145
|
await settle();
|
|
@@ -143,7 +165,7 @@ const user = {
|
|
|
143
165
|
},
|
|
144
166
|
async clear(node) {
|
|
145
167
|
const field = fieldOf(node);
|
|
146
|
-
if (field.props['editable'] === false)
|
|
168
|
+
if (field.props['editable'] === false || !touchable(field))
|
|
147
169
|
return;
|
|
148
170
|
emit(field, 'topFocus');
|
|
149
171
|
changeText(field, '');
|
package/dist/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `fireEvent` and `userEvent`, as React Native Testing Library names them, emitting the Fabric\n * events the native side would.\n *\n * Each goes through `fabric.emit`, which is the handler the renderer registered with Fabric, so an\n * event takes the real route: the engine's responder negotiation for a touch, its bubbling for the\n * rest, then the component's listener. Nothing here calls a handler directly, and a disabled\n * pressable ignores a press here for the same reason it does on a phone.\n *\n * Both are async where RNTL's `fireEvent` is not. Change detection is zoneless and commits on a\n * later task, so an event has to be awaited before the tree it changed can be read.\n */\nimport { currentOf, ownerOf, settle } from './render.ts';\nimport { flatten, isSearchBar, isTextInput } from './queries.ts';\nimport type { FakeFabricNode } from './test-utils.ts';\n\n/** A payload as a handler would see it (`{ nativeEvent }`), or the native event itself. */\nexport type EventPayload = Record<string, unknown>;\n\nconst nativeOf = (payload: EventPayload | undefined): Record<string, unknown> =>\n ((payload && 'nativeEvent' in payload ? payload['nativeEvent'] : payload) as Record<\n string,\n unknown\n >) ?? {};\n\nfunction emit(node: FakeFabricNode, type: string, nativeEvent: Record<string, unknown> = {}): void {\n ownerOf(node).fabric.emit(node, type, { target: node.reactTag, ...nativeEvent });\n}\n\n/** `press` -> `topPress`: the naming the renderer uses for `(touchEnd)` and every other event. */\nconst topLevel = (name: string): string =>\n name.startsWith('top') ? name : `top${name[0]!.toUpperCase()}${name.slice(1)}`;\n\n/** One finger, as Fabric reports it. The press machine reads `pageX`/`pageY`. */\nfunction touch(node: FakeFabricNode, phase: 'start' | 'end'): Record<string, unknown> {\n const point = {\n identifier: 0,\n target: node.reactTag,\n pageX: 0,\n pageY: 0,\n locationX: 0,\n locationY: 0,\n timestamp: Date.now(),\n };\n return { ...point, touches: phase === 'start' ? [point] : [], changedTouches: [point] };\n}\n\n/** The text field at or under a node, so a query for its wrapper still reaches the input. */\nfunction fieldOf(node: FakeFabricNode): FakeFabricNode {\n const current = currentOf(node) ?? node;\n const field = flatten([current]).find((n) => isTextInput(n) || isSearchBar(n));\n if (!field) throw new Error(`No TextInput or search bar at or under this ${node.viewName}.`);\n return field;\n}\n\n/** The count native would send next: one past the last one JS echoed back. */\nconst nextCount = (field: FakeFabricNode): number =>\n Number((currentOf(field) ?? field).props['mostRecentEventCount'] ?? 0) + 1;\n\nfunction changeText(field: FakeFabricNode, text: string): void {\n if (isSearchBar(field)) emit(field, 'topChangeText', { text });\n else emit(field, 'topChange', { text, eventCount: nextCount(field) });\n}\n\nasync function press(node: FakeFabricNode): Promise<void> {\n emit(node, 'topTouchStart', touch(node, 'start'));\n emit(node, 'topTouchEnd', touch(node, 'end'));\n await settle();\n}\n\nconst named = {\n press,\n async changeText(node: FakeFabricNode, text: string): Promise<void> {\n changeText(fieldOf(node), text);\n await settle();\n },\n async scroll(node: FakeFabricNode, payload?: EventPayload): Promise<void> {\n emit(node, 'topScroll', nativeOf(payload));\n await settle();\n },\n async focus(node: FakeFabricNode): Promise<void> {\n emit(node, 'topFocus');\n await settle();\n },\n async blur(node: FakeFabricNode): Promise<void> {\n emit(node, 'topBlur');\n await settle();\n },\n /** A `(layout)` event with the frame native would report: `{ x, y, width, height }`. */\n async layout(node: FakeFabricNode, frame: Partial<LayoutFrame> = {}): Promise<void> {\n emit(node, 'topLayout', { layout: { x: 0, y: 0, width: 0, height: 0, ...frame } });\n await settle();\n },\n};\n\n/** Where a view is in its parent, and its size, as `(layout)` reports them. */\nexport interface LayoutFrame {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** The frame in a layout payload, written bare, as `{ layout }`, or as `{ nativeEvent: { layout } }`. */\nfunction frameOf(payload: unknown): Partial<LayoutFrame> {\n const given = (payload ?? {}) as { nativeEvent?: unknown; layout?: Partial<LayoutFrame> };\n const event = (given.nativeEvent ?? given) as { layout?: Partial<LayoutFrame> };\n return event.layout ?? (event as Partial<LayoutFrame>);\n}\n\ntype FireEvent = ((node: FakeFabricNode, name: string, payload?: unknown) => Promise<void>) &\n typeof named;\n\n/**\n * `fireEvent(node, 'press')`, `fireEvent(node, 'changeText', 'Ada')`, or any other event by name -\n * `fireEvent(node, 'submitEditing', { text })` emits `topSubmitEditing`.\n */\nexport const fireEvent: FireEvent = Object.assign(\n async (node: FakeFabricNode, name: string, payload?: unknown): Promise<void> => {\n if (name === 'changeText') return named.changeText(node, String(payload));\n if (name === 'press' || name === 'scroll' || name === 'focus' || name === 'blur') {\n return named[name](node, payload as EventPayload);\n }\n // A layout by name takes the frame, or the event wrapped as `fireEvent.scroll` takes one.\n if (name === 'layout') return named.layout(node, frameOf(payload));\n emit(node, topLevel(name), nativeOf(payload as EventPayload));\n await settle();\n },\n named,\n);\n\n/** Options for `userEvent.type`, named as React Native Testing Library names them. */\nexport interface TypeOptions {\n /** Leave the field focused afterwards. By default typing ends with the field blurring. */\n skipBlur?: boolean;\n /** Press return at the end: `topSubmitEditing`, before any blur. */\n submitEditing?: boolean;\n}\n\n/**\n * Interactions as a person performs them: several events, a task apart, in the order the platform\n * sends them. Slower than `fireEvent`, and closer to what a device does.\n */\nexport interface UserEvent {\n /** A finger down and up, a task apart, through the responder. */\n press(node: FakeFabricNode): Promise<void>;\n /** A finger held for `duration` ms (500, the long-press delay, by default). */\n longPress(node: FakeFabricNode, options?: { duration?: number }): Promise<void>;\n /** Focus the field, then one change per character, each with the next `eventCount`. */\n type(node: FakeFabricNode, text: string, options?: TypeOptions): Promise<void>;\n /** Focus the field, empty it, and blur. */\n clear(node: FakeFabricNode): Promise<void>;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nasync function hold(node: FakeFabricNode, duration: number): Promise<void> {\n emit(node, 'topTouchStart', touch(node, 'start'));\n await settle();\n if (duration) await sleep(duration);\n emit(node, 'topTouchEnd', touch(node, 'end'));\n await settle();\n}\n\nasync function endEditing(field: FakeFabricNode, text: string): Promise<void> {\n emit(field, 'topEndEditing', { text });\n emit(field, 'topBlur');\n await settle();\n}\n\nconst user: UserEvent = {\n press: (node) => hold(node, 0),\n longPress: (node, { duration = 500 } = {}) => hold(node, duration),\n async type(node, text, options = {}) {\n const field = fieldOf(node);\n if (field.props['editable'] === false) return;\n emit(field, 'topFocus');\n await settle();\n const maxLength = field.props['maxLength'];\n const limit = typeof maxLength === 'number' ? maxLength : Infinity;\n let value = String((currentOf(field) ?? field).props['text'] ?? '');\n for (const key of text) {\n // `maxLength` is enforced natively: the field never calls back with text past it, so a key\n // pressed once the limit is reached is swallowed the same way it would be on a device.\n if (value.length >= limit) continue;\n value += key;\n emit(field, 'topKeyPress', { key });\n changeText(field, value);\n await settle();\n }\n if (options.submitEditing) emit(field, 'topSubmitEditing', { text: value });\n if (!options.skipBlur) await endEditing(field, value);\n else await settle();\n },\n async clear(node) {\n const field = fieldOf(node);\n if (field.props['editable'] === false) return;\n emit(field, 'topFocus');\n changeText(field, '');\n await settle();\n await endEditing(field, '');\n },\n};\n\n/** RNTL's `userEvent`: `userEvent.setup()` for an instance, or the same calls directly. */\nexport const userEvent: UserEvent & { setup(): UserEvent } = { ...user, setup: () => user };\n"]}
|
|
1
|
+
{"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"]}
|
package/dist/queries.js
CHANGED
|
@@ -4,8 +4,10 @@ function matches(value, matcher, exact = true) {
|
|
|
4
4
|
if (typeof value !== 'string')
|
|
5
5
|
return false;
|
|
6
6
|
const text = normalize(value);
|
|
7
|
-
if (matcher instanceof RegExp)
|
|
7
|
+
if (matcher instanceof RegExp) {
|
|
8
|
+
matcher.lastIndex = 0;
|
|
8
9
|
return matcher.test(text);
|
|
10
|
+
}
|
|
9
11
|
const wanted = normalize(matcher);
|
|
10
12
|
return exact ? text === wanted : text.toLowerCase().includes(wanted.toLowerCase());
|
|
11
13
|
}
|
package/dist/queries.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 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/** `ByRole`'s options: a role alone matches every node with it, `name` narrows it to one. */\nexport interface ByRoleOptions extends TextMatchOptions {\n /** The accessible name: `accessibilityLabel`, or failing that the text inside the node. */\n name?: Matcher;\n}\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) return matcher.test(text);\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)}` + (options.name === undefined ? '' : ` and name ${show(options.name)}`),\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 }),\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 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/** `ByRole`'s options: a role alone matches every node with it, `name` narrows it to one. */\nexport interface ByRoleOptions extends TextMatchOptions {\n /** The accessible name: `accessibilityLabel`, or failing that the text inside the node. */\n name?: Matcher;\n}\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)}` + (options.name === undefined ? '' : ` and name ${show(options.name)}`),\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 }),\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-native/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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.
|
|
38
|
-
"@ng-native/metro": "0.
|
|
39
|
-
"@ng-native/platform": "0.
|
|
37
|
+
"@ng-native/fabric": "0.11.0",
|
|
38
|
+
"@ng-native/metro": "0.11.0",
|
|
39
|
+
"@ng-native/platform": "0.11.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.
|
|
46
|
+
"@ng-native/components": "0.11.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
49
|
"@angular/compiler": {
|
|
@@ -62,8 +62,8 @@
|
|
|
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.
|
|
66
|
-
"@ng-native/router": "0.
|
|
65
|
+
"@ng-native/components": "0.11.0",
|
|
66
|
+
"@ng-native/router": "0.11.0",
|
|
67
67
|
"vite": "^8.3.1",
|
|
68
68
|
"vitest": "^5.0.2"
|
|
69
69
|
},
|
package/runner/compile.mjs
CHANGED
|
@@ -86,6 +86,26 @@ export function compileMarkdown(source, file) {
|
|
|
86
86
|
return markdownModule(source, file);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** @type {typeof import('@ng-native/metro/dom-component.cjs') | undefined} */
|
|
90
|
+
let domComponent;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What native code gets for a DOM component's file, as Metro hands it over: a reference to the
|
|
94
|
+
* page a web view loads, and none of the page. A test is native code. Evaluated instead, the
|
|
95
|
+
* file's `mountInWebView` reads `window` as it is imported, and its web-only imports load too.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} source
|
|
98
|
+
* @param {string} file
|
|
99
|
+
* @returns {string | null} an ES module, or nothing for any other file
|
|
100
|
+
*/
|
|
101
|
+
export function domComponentReference(source, file) {
|
|
102
|
+
// A package's own files are never an app's DOM component.
|
|
103
|
+
if (file.includes('/node_modules/') || !source.includes('use dom')) return null;
|
|
104
|
+
domComponent ??= require('@ng-native/metro/dom-component.cjs');
|
|
105
|
+
if (!domComponent.isAngularDomComponent(source)) return null;
|
|
106
|
+
return domComponent.domComponentReference(file, { dev: true }).code;
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
/**
|
|
90
110
|
* `require('./logo.png')`, which Metro turns into an asset id. An ES module has no `require` under
|
|
91
111
|
* Vitest or under Node, so the module would throw as it is evaluated; it gets `{ testUri }`
|
package/runner/loader.mjs
CHANGED
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
import { readFileSync } from 'node:fs';
|
|
19
19
|
import { stripTypeScriptTypes } from 'node:module';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
compileAngular,
|
|
23
|
+
compileMarkdown,
|
|
24
|
+
domComponentReference,
|
|
25
|
+
needsAngular,
|
|
26
|
+
stubAssets,
|
|
27
|
+
} from './compile.mjs';
|
|
22
28
|
|
|
23
29
|
/** @type {string[]} */
|
|
24
30
|
let skip = [];
|
|
@@ -71,10 +77,13 @@ export async function load(url, context, nextLoad) {
|
|
|
71
77
|
*/
|
|
72
78
|
function loadTypeScript(file, installed) {
|
|
73
79
|
const read = readFileSync(file, 'utf8');
|
|
80
|
+
const platform = web.some((part) => file.includes(part)) ? 'web' : undefined;
|
|
81
|
+
// Native code gets a reference to a DOM component's page; a file compiled for the web is it.
|
|
82
|
+
const reference = platform ? null : domComponentReference(read, file);
|
|
83
|
+
if (reference) return { format: 'module', source: reference, shortCircuit: true };
|
|
74
84
|
const source = installed ? read : stubAssets(read);
|
|
75
85
|
const decorated = needsAngular(source, file);
|
|
76
86
|
if (!decorated && !installed && source === read) return null;
|
|
77
|
-
const platform = web.some((part) => file.includes(part)) ? 'web' : undefined;
|
|
78
87
|
const code = decorated ? compileAngular(source, file, { platform }).code : source;
|
|
79
88
|
return {
|
|
80
89
|
format: 'module',
|
package/runner/vitest.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ngNative()`: the Vite plugin that lets Vitest run Angular Native code in Node, with no DOM.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Seven things, each of which Vitest would otherwise get wrong on its own:
|
|
5
5
|
*
|
|
6
6
|
* - It compiles. Every decorated `.ts` goes through `@ng-native/metro`'s AOT transform and every
|
|
7
7
|
* partial-compiled package through the linker (see `compile.mjs`), before Vite strips types.
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* - It stubs assets. With no `require`, an app's `require('./logo.png')` would throw; it becomes
|
|
14
14
|
* `{ testUri }` instead. And it stands in for the gesture and animation entry points, whose
|
|
15
15
|
* React Native source Node cannot load (`STAND_INS`).
|
|
16
|
+
* - It imports a DOM component's file as native code does under Metro: as a reference to its
|
|
17
|
+
* page (`domComponentReference`), so no test loads the page.
|
|
16
18
|
* - It imports a `.md` file as Metro does, as `{ attributes, content, tokens }`, unless a plugin
|
|
17
19
|
* ahead of it already made the file a module.
|
|
18
20
|
* - It resolves as Metro does. A workspace library's copy of a package at the app's version
|
|
@@ -26,7 +28,13 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
26
28
|
import path from 'node:path';
|
|
27
29
|
import { fileURLToPath } from 'node:url';
|
|
28
30
|
import { defaultClientConditions, defaultServerConditions } from 'vite';
|
|
29
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
compileAngular,
|
|
33
|
+
compileMarkdown,
|
|
34
|
+
domComponentReference,
|
|
35
|
+
needsAngular,
|
|
36
|
+
stubAssets,
|
|
37
|
+
} from './compile.mjs';
|
|
30
38
|
|
|
31
39
|
const SETUP = fileURLToPath(new URL('./setup.mjs', import.meta.url));
|
|
32
40
|
|
|
@@ -218,6 +226,25 @@ function asWritten(source, file) {
|
|
|
218
226
|
}
|
|
219
227
|
}
|
|
220
228
|
|
|
229
|
+
/**
|
|
230
|
+
* A script as a test runs it: assets stubbed, Angular compiled, `require` hidden.
|
|
231
|
+
*
|
|
232
|
+
* @param {import('vite').Rollup.TransformPluginContext} context
|
|
233
|
+
* @param {string} source
|
|
234
|
+
* @param {string} file
|
|
235
|
+
* @param {{ libraryStyles?: string[] | false, projectRoot: string }} options
|
|
236
|
+
*/
|
|
237
|
+
function compiled(context, source, file, options) {
|
|
238
|
+
const code = file.includes('/node_modules/') ? source : stubAssets(source);
|
|
239
|
+
if (!needsAngular(code, file)) {
|
|
240
|
+
const hidden = hideRequire(code, file);
|
|
241
|
+
return hidden === source ? null : { code: hidden, map: null };
|
|
242
|
+
}
|
|
243
|
+
const result = compileAngular(code, file, options);
|
|
244
|
+
for (const dependency of result.dependencies) context.addWatchFile(dependency);
|
|
245
|
+
return { code: hideRequire(result.code, file), map: result.map ?? null };
|
|
246
|
+
}
|
|
247
|
+
|
|
221
248
|
/**
|
|
222
249
|
* @param {{ inline?: (string | RegExp)[], libraryStyles?: string[] | false }} [options]
|
|
223
250
|
* @returns {import('vitest/config').Plugin}
|
|
@@ -255,17 +282,12 @@ export function ngNative(options = {}) {
|
|
|
255
282
|
return asWritten(source, file) ? { code: compileMarkdown(source, file), map: null } : null;
|
|
256
283
|
}
|
|
257
284
|
if (file.startsWith('\0') || !/\.m?[jt]s$/.test(file)) return null;
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
return hidden === source ? null : { code: hidden, map: null };
|
|
262
|
-
}
|
|
263
|
-
const result = compileAngular(code, file, {
|
|
285
|
+
const reference = domComponentReference(source, file);
|
|
286
|
+
if (reference) return { code: reference, map: null };
|
|
287
|
+
return compiled(this, source, file, {
|
|
264
288
|
libraryStyles: options.libraryStyles,
|
|
265
289
|
projectRoot: root,
|
|
266
290
|
});
|
|
267
|
-
for (const dependency of result.dependencies) this.addWatchFile(dependency);
|
|
268
|
-
return { code: hideRequire(result.code, file), map: result.map ?? null };
|
|
269
291
|
},
|
|
270
292
|
};
|
|
271
293
|
}
|