@react-native-harness/ui 1.0.0-alpha.23 → 1.0.0-alpha.25
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/README.md +25 -11
- package/android/src/main/java/com/harnessui/HarnessUIModule.kt +26 -16
- package/android/src/main/java/com/harnessui/UIHelperImpl.kt +136 -41
- package/android/src/main/java/com/harnessui/ViewQueryHelper.kt +36 -15
- package/android/src/main/java/com/harnessui/ViewRegistry.kt +51 -0
- package/dist/HarnessUI.d.ts +17 -0
- package/dist/HarnessUI.d.ts.map +1 -0
- package/dist/HarnessUI.js +2 -0
- package/dist/HarnessUI.web.d.ts +14 -0
- package/dist/HarnessUI.web.d.ts.map +1 -0
- package/dist/HarnessUI.web.js +46 -0
- package/dist/NativeHarness.d.ts +22 -0
- package/dist/NativeHarness.d.ts.map +1 -0
- package/dist/NativeHarness.js +2 -0
- package/dist/NativeHarnessUI.d.ts +10 -37
- package/dist/NativeHarnessUI.d.ts.map +1 -1
- package/dist/WebHarness.d.ts +14 -0
- package/dist/WebHarness.d.ts.map +1 -0
- package/dist/WebHarness.js +53 -0
- package/dist/WebHarnessUI.d.ts +15 -0
- package/dist/WebHarnessUI.d.ts.map +1 -0
- package/dist/WebHarnessUI.js +81 -0
- package/dist/harness/NativeHarness.d.ts +17 -0
- package/dist/harness/NativeHarness.d.ts.map +1 -0
- package/dist/harness/NativeHarness.js +2 -0
- package/dist/harness/WebHarness.d.ts +14 -0
- package/dist/harness/WebHarness.d.ts.map +1 -0
- package/dist/harness/WebHarness.js +53 -0
- package/dist/harness/index.d.ts +4 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +8 -0
- package/dist/harness/native-harness.d.ts +17 -0
- package/dist/harness/native-harness.d.ts.map +1 -0
- package/dist/harness/native-harness.js +2 -0
- package/dist/harness/web-harness.d.ts +14 -0
- package/dist/harness/web-harness.d.ts.map +1 -0
- package/dist/harness/web-harness.js +53 -0
- package/dist/harness.d.ts +4 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/screen.d.ts +9 -7
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +50 -20
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/userEvent.d.ts.map +1 -1
- package/dist/userEvent.js +10 -19
- package/ios/HarnessUI.mm +58 -8
- package/ios/ViewQueryHelper.h +9 -0
- package/ios/ViewQueryHelper.mm +24 -1
- package/ios/ViewRegistry.h +8 -0
- package/ios/ViewRegistry.mm +33 -0
- package/package.json +2 -2
- package/src/NativeHarnessUI.ts +11 -45
- package/src/WebHarnessUI.ts +113 -0
- package/src/harness.ts +12 -0
- package/src/index.ts +1 -1
- package/src/screen.ts +64 -27
- package/src/types.ts +54 -0
- package/src/userEvent.ts +10 -19
package/src/screen.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ViewInfo, type BoundingBox } from './types.js';
|
|
2
2
|
import { waitFor } from '@react-native-harness/runtime';
|
|
3
|
+
import HarnessUI from './harness.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Represents an element found on screen
|
|
6
|
-
* This can be used with userEvent
|
|
6
|
+
* Represents an element found on screen.
|
|
7
|
+
* This is an opaque reference that can be used with userEvent or screenshot.
|
|
7
8
|
*/
|
|
8
|
-
export type ElementReference =
|
|
9
|
+
export type ElementReference = {
|
|
10
|
+
readonly nativeId: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const wrapElement = (viewInfo: ViewInfo): ElementReference => ({
|
|
14
|
+
nativeId: viewInfo.nativeId,
|
|
15
|
+
});
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Screenshot result containing PNG image data.
|
|
@@ -69,54 +76,57 @@ export type Screen = {
|
|
|
69
76
|
queryAllByAccessibilityLabel: (label: string) => ElementReference[];
|
|
70
77
|
|
|
71
78
|
/**
|
|
72
|
-
* Captures a screenshot of the entire app window
|
|
73
|
-
* @param
|
|
79
|
+
* Captures a screenshot of the entire app window, a specific element, or a region.
|
|
80
|
+
* @param target Optional element reference or bounding box to capture. If not provided, captures the entire window.
|
|
74
81
|
* @returns Promise resolving to ScreenshotResult with PNG data, or null if capture fails.
|
|
75
82
|
*/
|
|
76
|
-
screenshot: (
|
|
83
|
+
screenshot: (
|
|
84
|
+
target?: ElementReference | BoundingBox
|
|
85
|
+
) => Promise<ScreenshotResult | null>;
|
|
77
86
|
};
|
|
78
87
|
|
|
79
88
|
const createScreen = (): Screen => {
|
|
80
89
|
return {
|
|
81
90
|
findByTestId: async (testId: string): Promise<ElementReference> => {
|
|
82
91
|
return waitFor(() => {
|
|
83
|
-
const result =
|
|
92
|
+
const result = HarnessUI.queryByTestId(testId);
|
|
84
93
|
if (!result) {
|
|
85
94
|
throw new Error(`Unable to find element with testID: ${testId}`);
|
|
86
95
|
}
|
|
87
|
-
return result;
|
|
96
|
+
return wrapElement(result);
|
|
88
97
|
});
|
|
89
98
|
},
|
|
90
99
|
|
|
91
100
|
findAllByTestId: async (testId: string): Promise<ElementReference[]> => {
|
|
92
101
|
return waitFor(() => {
|
|
93
|
-
const results =
|
|
102
|
+
const results = HarnessUI.queryAllByTestId(testId);
|
|
94
103
|
if (results.length === 0) {
|
|
95
104
|
throw new Error(`Unable to find any elements with testID: ${testId}`);
|
|
96
105
|
}
|
|
97
|
-
return results;
|
|
106
|
+
return results.map(wrapElement);
|
|
98
107
|
});
|
|
99
108
|
},
|
|
100
109
|
|
|
101
110
|
queryByTestId: (testId: string): ElementReference | null => {
|
|
102
|
-
|
|
111
|
+
const result = HarnessUI.queryByTestId(testId);
|
|
112
|
+
return result ? wrapElement(result) : null;
|
|
103
113
|
},
|
|
104
114
|
|
|
105
115
|
queryAllByTestId: (testId: string): ElementReference[] => {
|
|
106
|
-
return
|
|
116
|
+
return HarnessUI.queryAllByTestId(testId).map(wrapElement);
|
|
107
117
|
},
|
|
108
118
|
|
|
109
119
|
findByAccessibilityLabel: async (
|
|
110
120
|
label: string
|
|
111
121
|
): Promise<ElementReference> => {
|
|
112
122
|
return waitFor(() => {
|
|
113
|
-
const result =
|
|
123
|
+
const result = HarnessUI.queryByAccessibilityLabel(label);
|
|
114
124
|
if (!result) {
|
|
115
125
|
throw new Error(
|
|
116
126
|
`Unable to find element with accessibility label: ${label}`
|
|
117
127
|
);
|
|
118
128
|
}
|
|
119
|
-
return result;
|
|
129
|
+
return wrapElement(result);
|
|
120
130
|
});
|
|
121
131
|
},
|
|
122
132
|
|
|
@@ -124,37 +134,59 @@ const createScreen = (): Screen => {
|
|
|
124
134
|
label: string
|
|
125
135
|
): Promise<ElementReference[]> => {
|
|
126
136
|
return waitFor(() => {
|
|
127
|
-
const results =
|
|
137
|
+
const results = HarnessUI.queryAllByAccessibilityLabel(label);
|
|
128
138
|
if (results.length === 0) {
|
|
129
139
|
throw new Error(
|
|
130
140
|
`Unable to find any elements with accessibility label: ${label}`
|
|
131
141
|
);
|
|
132
142
|
}
|
|
133
|
-
return results;
|
|
143
|
+
return results.map(wrapElement);
|
|
134
144
|
});
|
|
135
145
|
},
|
|
136
146
|
|
|
137
147
|
queryByAccessibilityLabel: (label: string): ElementReference | null => {
|
|
138
|
-
|
|
148
|
+
const result = HarnessUI.queryByAccessibilityLabel(label);
|
|
149
|
+
return result ? wrapElement(result) : null;
|
|
139
150
|
},
|
|
140
151
|
|
|
141
152
|
queryAllByAccessibilityLabel: (label: string): ElementReference[] => {
|
|
142
|
-
return
|
|
153
|
+
return HarnessUI.queryAllByAccessibilityLabel(label).map(wrapElement);
|
|
143
154
|
},
|
|
144
155
|
|
|
145
156
|
screenshot: async (
|
|
146
|
-
|
|
157
|
+
target?: ElementReference | BoundingBox
|
|
147
158
|
): Promise<ScreenshotResult | null> => {
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
let captureBounds: ViewInfo | null = null;
|
|
160
|
+
let targetWidth = 0;
|
|
161
|
+
let targetHeight = 0;
|
|
162
|
+
|
|
163
|
+
if (target) {
|
|
164
|
+
if ('nativeId' in target) {
|
|
165
|
+
// ElementReference
|
|
166
|
+
captureBounds = {
|
|
167
|
+
nativeId: target.nativeId,
|
|
168
|
+
x: 0,
|
|
169
|
+
y: 0,
|
|
170
|
+
width: 0,
|
|
171
|
+
height: 0,
|
|
172
|
+
};
|
|
173
|
+
} else {
|
|
174
|
+
// BoundingBox
|
|
175
|
+
captureBounds = {
|
|
176
|
+
nativeId: '',
|
|
177
|
+
...target,
|
|
178
|
+
};
|
|
179
|
+
targetWidth = target.width;
|
|
180
|
+
targetHeight = target.height;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const base64String = await HarnessUI.captureScreenshot(captureBounds);
|
|
150
185
|
|
|
151
186
|
if (!base64String) {
|
|
152
187
|
return null;
|
|
153
188
|
}
|
|
154
189
|
|
|
155
|
-
const width = element?.width ?? 0;
|
|
156
|
-
const height = element?.height ?? 0;
|
|
157
|
-
|
|
158
190
|
// Decode Base64 string to Uint8Array
|
|
159
191
|
const binaryString = atob(base64String);
|
|
160
192
|
const len = binaryString.length;
|
|
@@ -163,10 +195,15 @@ const createScreen = (): Screen => {
|
|
|
163
195
|
bytes[i] = binaryString.charCodeAt(i);
|
|
164
196
|
}
|
|
165
197
|
|
|
198
|
+
// If we captured by nativeId, we might not know the width/height beforehand in JS.
|
|
199
|
+
// But the native side returns the actual captured PNG.
|
|
200
|
+
// Ideally we'd get the size from the native side, but currently the bridge doesn't return it.
|
|
201
|
+
// For now we use the provided target size or 0.
|
|
202
|
+
|
|
166
203
|
return {
|
|
167
204
|
data: bytes,
|
|
168
|
-
width,
|
|
169
|
-
height,
|
|
205
|
+
width: targetWidth,
|
|
206
|
+
height: targetHeight,
|
|
170
207
|
};
|
|
171
208
|
},
|
|
172
209
|
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ViewInfo } from "./NativeHarnessUI.js";
|
|
2
|
+
|
|
3
|
+
export type { ViewInfo, BoundingBox } from "./NativeHarnessUI.js";
|
|
4
|
+
|
|
5
|
+
export interface HarnessUIModule {
|
|
6
|
+
/**
|
|
7
|
+
* Simulates a native press on a view identified by nativeId, or at coordinates if nativeId is empty.
|
|
8
|
+
* Returns a promise that resolves when the press action is complete.
|
|
9
|
+
*/
|
|
10
|
+
simulatePress(nativeId: string, x: number, y: number): Promise<void>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Finds a view by its testID (accessibilityIdentifier on iOS, tag on Android).
|
|
14
|
+
* Returns null if no view is found.
|
|
15
|
+
*/
|
|
16
|
+
queryByTestId(testId: string): ViewInfo | null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Finds all views by testID (accessibilityIdentifier on iOS, tag on Android).
|
|
20
|
+
* Returns an empty array if no views are found.
|
|
21
|
+
*/
|
|
22
|
+
queryAllByTestId(testId: string): ViewInfo[];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Finds a view by its accessibility label.
|
|
26
|
+
* Returns null if no view is found.
|
|
27
|
+
*/
|
|
28
|
+
queryByAccessibilityLabel(label: string): ViewInfo | null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Finds all views by accessibility label.
|
|
32
|
+
* Returns an empty array if no views are found.
|
|
33
|
+
*/
|
|
34
|
+
queryAllByAccessibilityLabel(label: string): ViewInfo[];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Captures a screenshot of the app UI.
|
|
38
|
+
* @param bounds Optional bounds to capture a specific region. Pass null to capture the entire window.
|
|
39
|
+
* @returns Promise resolving to Base64 encoded string containing PNG data, or null on failure.
|
|
40
|
+
*/
|
|
41
|
+
captureScreenshot(bounds: ViewInfo | null): Promise<string | null>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Types a single character into the currently focused text input.
|
|
45
|
+
* If no text input is focused, this is a no-op.
|
|
46
|
+
*/
|
|
47
|
+
typeChar(character: string): Promise<void>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Blurs (resigns first responder from) the currently focused element.
|
|
51
|
+
* Optionally triggers submitEditing event before blur.
|
|
52
|
+
*/
|
|
53
|
+
blur(options: { submitEditing?: boolean }): Promise<void>;
|
|
54
|
+
}
|
package/src/userEvent.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import HarnessUI from './harness.js';
|
|
2
2
|
import type { ElementReference } from './screen.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -63,16 +63,15 @@ export type UserEvent = {
|
|
|
63
63
|
const createUserEvent = (): UserEvent => {
|
|
64
64
|
return {
|
|
65
65
|
press: async (element: ElementReference): Promise<void> => {
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await NativeHarnessUI.simulatePress(centerX, centerY);
|
|
66
|
+
// We pass 0, 0 as coordinates when nativeId is provided.
|
|
67
|
+
// Native side will resolve the view and calculate the center.
|
|
68
|
+
await HarnessUI.simulatePress(element.nativeId, 0, 0);
|
|
70
69
|
// Flush pending events to ensure onPress and other callbacks are processed
|
|
71
70
|
await flushEvents();
|
|
72
71
|
},
|
|
73
72
|
|
|
74
73
|
pressAt: async (x: number, y: number): Promise<void> => {
|
|
75
|
-
await
|
|
74
|
+
await HarnessUI.simulatePress('', x, y);
|
|
76
75
|
// Flush pending events to ensure onPress and other callbacks are processed
|
|
77
76
|
await flushEvents();
|
|
78
77
|
},
|
|
@@ -87,32 +86,24 @@ const createUserEvent = (): UserEvent => {
|
|
|
87
86
|
}
|
|
88
87
|
): Promise<void> => {
|
|
89
88
|
// Press to focus the element (triggers pressIn/pressOut unless skipPress is true)
|
|
90
|
-
// Note: Currently we always press to focus, the skipPress option would need
|
|
91
|
-
// additional implementation in simulatePress to avoid firing press events
|
|
92
89
|
if (!options?.skipPress) {
|
|
93
|
-
|
|
94
|
-
const centerX = element.x + element.width / 2;
|
|
95
|
-
const centerY = element.y + element.height / 2;
|
|
96
|
-
await NativeHarnessUI.simulatePress(centerX, centerY);
|
|
90
|
+
await HarnessUI.simulatePress(element.nativeId, 0, 0);
|
|
97
91
|
await flushEvents();
|
|
98
92
|
} else {
|
|
99
|
-
// Still need to press to focus
|
|
100
|
-
|
|
101
|
-
const centerX = element.x + element.width / 2;
|
|
102
|
-
const centerY = element.y + element.height / 2;
|
|
103
|
-
await NativeHarnessUI.simulatePress(centerX, centerY);
|
|
93
|
+
// Still need to press to focus
|
|
94
|
+
await HarnessUI.simulatePress(element.nativeId, 0, 0);
|
|
104
95
|
await flushEvents();
|
|
105
96
|
}
|
|
106
97
|
|
|
107
98
|
// Type each character one by one
|
|
108
99
|
for (const char of text) {
|
|
109
|
-
await
|
|
100
|
+
await HarnessUI.typeChar(char);
|
|
110
101
|
await flushEvents(); // Let onChangeText fire
|
|
111
102
|
}
|
|
112
103
|
|
|
113
104
|
// Blur (triggers endEditing and blur unless skipBlur)
|
|
114
105
|
if (!options?.skipBlur) {
|
|
115
|
-
await
|
|
106
|
+
await HarnessUI.blur({
|
|
116
107
|
submitEditing: options?.submitEditing ?? false,
|
|
117
108
|
});
|
|
118
109
|
await flushEvents();
|