@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.
Files changed (64) hide show
  1. package/README.md +25 -11
  2. package/android/src/main/java/com/harnessui/HarnessUIModule.kt +26 -16
  3. package/android/src/main/java/com/harnessui/UIHelperImpl.kt +136 -41
  4. package/android/src/main/java/com/harnessui/ViewQueryHelper.kt +36 -15
  5. package/android/src/main/java/com/harnessui/ViewRegistry.kt +51 -0
  6. package/dist/HarnessUI.d.ts +17 -0
  7. package/dist/HarnessUI.d.ts.map +1 -0
  8. package/dist/HarnessUI.js +2 -0
  9. package/dist/HarnessUI.web.d.ts +14 -0
  10. package/dist/HarnessUI.web.d.ts.map +1 -0
  11. package/dist/HarnessUI.web.js +46 -0
  12. package/dist/NativeHarness.d.ts +22 -0
  13. package/dist/NativeHarness.d.ts.map +1 -0
  14. package/dist/NativeHarness.js +2 -0
  15. package/dist/NativeHarnessUI.d.ts +10 -37
  16. package/dist/NativeHarnessUI.d.ts.map +1 -1
  17. package/dist/WebHarness.d.ts +14 -0
  18. package/dist/WebHarness.d.ts.map +1 -0
  19. package/dist/WebHarness.js +53 -0
  20. package/dist/WebHarnessUI.d.ts +15 -0
  21. package/dist/WebHarnessUI.d.ts.map +1 -0
  22. package/dist/WebHarnessUI.js +81 -0
  23. package/dist/harness/NativeHarness.d.ts +17 -0
  24. package/dist/harness/NativeHarness.d.ts.map +1 -0
  25. package/dist/harness/NativeHarness.js +2 -0
  26. package/dist/harness/WebHarness.d.ts +14 -0
  27. package/dist/harness/WebHarness.d.ts.map +1 -0
  28. package/dist/harness/WebHarness.js +53 -0
  29. package/dist/harness/index.d.ts +4 -0
  30. package/dist/harness/index.d.ts.map +1 -0
  31. package/dist/harness/index.js +8 -0
  32. package/dist/harness/native-harness.d.ts +17 -0
  33. package/dist/harness/native-harness.d.ts.map +1 -0
  34. package/dist/harness/native-harness.js +2 -0
  35. package/dist/harness/web-harness.d.ts +14 -0
  36. package/dist/harness/web-harness.d.ts.map +1 -0
  37. package/dist/harness/web-harness.js +53 -0
  38. package/dist/harness.d.ts +4 -0
  39. package/dist/harness.d.ts.map +1 -0
  40. package/dist/harness.js +8 -0
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/screen.d.ts +9 -7
  44. package/dist/screen.d.ts.map +1 -1
  45. package/dist/screen.js +50 -20
  46. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  47. package/dist/types.d.ts +48 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +1 -0
  50. package/dist/userEvent.d.ts.map +1 -1
  51. package/dist/userEvent.js +10 -19
  52. package/ios/HarnessUI.mm +58 -8
  53. package/ios/ViewQueryHelper.h +9 -0
  54. package/ios/ViewQueryHelper.mm +24 -1
  55. package/ios/ViewRegistry.h +8 -0
  56. package/ios/ViewRegistry.mm +33 -0
  57. package/package.json +2 -2
  58. package/src/NativeHarnessUI.ts +11 -45
  59. package/src/WebHarnessUI.ts +113 -0
  60. package/src/harness.ts +12 -0
  61. package/src/index.ts +1 -1
  62. package/src/screen.ts +64 -27
  63. package/src/types.ts +54 -0
  64. package/src/userEvent.ts +10 -19
package/src/screen.ts CHANGED
@@ -1,11 +1,18 @@
1
- import NativeHarnessUI, { type ViewInfo } from './NativeHarnessUI.js';
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 with its position and dimensions.
6
- * This can be used with userEvent.press() to interact with the element.
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 = ViewInfo;
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 or a specific element.
73
- * @param element Optional element reference to capture. If not provided, captures the entire window.
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: (element?: ElementReference) => Promise<ScreenshotResult | null>;
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 = NativeHarnessUI.queryByTestId(testId);
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 = NativeHarnessUI.queryAllByTestId(testId);
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
- return NativeHarnessUI.queryByTestId(testId);
111
+ const result = HarnessUI.queryByTestId(testId);
112
+ return result ? wrapElement(result) : null;
103
113
  },
104
114
 
105
115
  queryAllByTestId: (testId: string): ElementReference[] => {
106
- return NativeHarnessUI.queryAllByTestId(testId);
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 = NativeHarnessUI.queryByAccessibilityLabel(label);
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 = NativeHarnessUI.queryAllByAccessibilityLabel(label);
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
- return NativeHarnessUI.queryByAccessibilityLabel(label);
148
+ const result = HarnessUI.queryByAccessibilityLabel(label);
149
+ return result ? wrapElement(result) : null;
139
150
  },
140
151
 
141
152
  queryAllByAccessibilityLabel: (label: string): ElementReference[] => {
142
- return NativeHarnessUI.queryAllByAccessibilityLabel(label);
153
+ return HarnessUI.queryAllByAccessibilityLabel(label).map(wrapElement);
143
154
  },
144
155
 
145
156
  screenshot: async (
146
- element?: ElementReference
157
+ target?: ElementReference | BoundingBox
147
158
  ): Promise<ScreenshotResult | null> => {
148
- const bounds = element ?? null;
149
- const base64String = await NativeHarnessUI.captureScreenshot(bounds);
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 NativeHarnessUI from './NativeHarnessUI.js';
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
- // Calculate center point of the element
67
- const centerX = element.x + element.width / 2;
68
- const centerY = element.y + element.height / 2;
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 NativeHarnessUI.simulatePress(x, y);
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
- // Calculate center point of the element
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, but ideally without press events
100
- // For now, we press anyway - future enhancement could add a focusOnly method
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 NativeHarnessUI.typeChar(char);
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 NativeHarnessUI.blur({
106
+ await HarnessUI.blur({
116
107
  submitEditing: options?.submitEditing ?? false,
117
108
  });
118
109
  await flushEvents();