@react-native-harness/ui 1.0.0-alpha.23 → 1.0.0-alpha.24

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 (53) hide show
  1. package/dist/HarnessUI.d.ts +17 -0
  2. package/dist/HarnessUI.d.ts.map +1 -0
  3. package/dist/HarnessUI.js +2 -0
  4. package/dist/HarnessUI.web.d.ts +14 -0
  5. package/dist/HarnessUI.web.d.ts.map +1 -0
  6. package/dist/HarnessUI.web.js +46 -0
  7. package/dist/NativeHarness.d.ts +22 -0
  8. package/dist/NativeHarness.d.ts.map +1 -0
  9. package/dist/NativeHarness.js +2 -0
  10. package/dist/NativeHarnessUI.d.ts +0 -37
  11. package/dist/NativeHarnessUI.d.ts.map +1 -1
  12. package/dist/WebHarness.d.ts +14 -0
  13. package/dist/WebHarness.d.ts.map +1 -0
  14. package/dist/WebHarness.js +53 -0
  15. package/dist/WebHarnessUI.d.ts +14 -0
  16. package/dist/WebHarnessUI.d.ts.map +1 -0
  17. package/dist/WebHarnessUI.js +53 -0
  18. package/dist/harness/NativeHarness.d.ts +17 -0
  19. package/dist/harness/NativeHarness.d.ts.map +1 -0
  20. package/dist/harness/NativeHarness.js +2 -0
  21. package/dist/harness/WebHarness.d.ts +14 -0
  22. package/dist/harness/WebHarness.d.ts.map +1 -0
  23. package/dist/harness/WebHarness.js +53 -0
  24. package/dist/harness/index.d.ts +4 -0
  25. package/dist/harness/index.d.ts.map +1 -0
  26. package/dist/harness/index.js +8 -0
  27. package/dist/harness/native-harness.d.ts +17 -0
  28. package/dist/harness/native-harness.d.ts.map +1 -0
  29. package/dist/harness/native-harness.js +2 -0
  30. package/dist/harness/web-harness.d.ts +14 -0
  31. package/dist/harness/web-harness.d.ts.map +1 -0
  32. package/dist/harness/web-harness.js +53 -0
  33. package/dist/harness.d.ts +4 -0
  34. package/dist/harness.d.ts.map +1 -0
  35. package/dist/harness.js +8 -0
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/screen.d.ts +1 -1
  39. package/dist/screen.d.ts.map +1 -1
  40. package/dist/screen.js +10 -10
  41. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  42. package/dist/types.d.ts +55 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +1 -0
  45. package/dist/userEvent.js +7 -7
  46. package/package.json +2 -2
  47. package/src/NativeHarnessUI.ts +1 -45
  48. package/src/WebHarnessUI.ts +78 -0
  49. package/src/harness.ts +12 -0
  50. package/src/index.ts +1 -1
  51. package/src/screen.ts +11 -10
  52. package/src/types.ts +60 -0
  53. package/src/userEvent.ts +7 -7
@@ -1,8 +1,6 @@
1
1
  import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
2
 
3
- /**
4
- * Represents the position and dimensions of a view in screen coordinates (points/dp).
5
- */
3
+ // This interface needs to be there for Codegen to work.
6
4
  export interface ViewInfo {
7
5
  x: number;
8
6
  y: number;
@@ -11,56 +9,14 @@ export interface ViewInfo {
11
9
  }
12
10
 
13
11
  interface Spec extends TurboModule {
14
- /**
15
- * Simulates a native press at the specified screen coordinates.
16
- * Returns a promise that resolves when the press action is complete.
17
- */
18
12
  simulatePress(x: number, y: number): Promise<void>;
19
-
20
- /**
21
- * Finds a view by its testID (accessibilityIdentifier on iOS, tag on Android).
22
- * Returns null if no view is found.
23
- */
24
13
  queryByTestId(testId: string): ViewInfo | null;
25
-
26
- /**
27
- * Finds all views by testID (accessibilityIdentifier on iOS, tag on Android).
28
- * Returns an empty array if no views are found.
29
- */
30
14
  queryAllByTestId(testId: string): ViewInfo[];
31
-
32
- /**
33
- * Finds a view by its accessibility label.
34
- * Returns null if no view is found.
35
- */
36
15
  queryByAccessibilityLabel(label: string): ViewInfo | null;
37
-
38
- /**
39
- * Finds all views by accessibility label.
40
- * Returns an empty array if no views are found.
41
- */
42
16
  queryAllByAccessibilityLabel(label: string): ViewInfo[];
43
-
44
- /**
45
- * Captures a screenshot of the app UI.
46
- * @param bounds Optional bounds to capture a specific region. Pass null to capture the entire window.
47
- * @returns Promise resolving to Base64 encoded string containing PNG data, or null on failure.
48
- */
49
17
  captureScreenshot(bounds: ViewInfo | null): Promise<string | null>;
50
-
51
- /**
52
- * Types a single character into the currently focused text input.
53
- * If no text input is focused, this is a no-op.
54
- */
55
18
  typeChar(character: string): Promise<void>;
56
-
57
- /**
58
- * Blurs (resigns first responder from) the currently focused element.
59
- * Optionally triggers submitEditing event before blur.
60
- */
61
19
  blur(options: { submitEditing?: boolean }): Promise<void>;
62
20
  }
63
21
 
64
- export type HarnessUIModule = Spec;
65
-
66
22
  export default TurboModuleRegistry.getEnforcing<Spec>('HarnessUI');
@@ -0,0 +1,78 @@
1
+ import { type HarnessUIModule, type ViewInfo } from './types.js';
2
+
3
+ declare global {
4
+ interface Window {
5
+ __RN_HARNESS_CAPTURE_SCREENSHOT__: (
6
+ bounds: ViewInfo | null
7
+ ) => Promise<string | null>;
8
+ __RN_HARNESS_SIMULATE_PRESS__: (x: number, y: number) => Promise<void>;
9
+ __RN_HARNESS_TYPE_CHAR__: (character: string) => Promise<void>;
10
+ __RN_HARNESS_BLUR__: (options: {
11
+ submitEditing?: boolean;
12
+ }) => Promise<void>;
13
+ }
14
+ }
15
+
16
+ const getElementViewInfo = (element: Element): ViewInfo => {
17
+ const rect = element.getBoundingClientRect();
18
+ return {
19
+ x: rect.left,
20
+ y: rect.top,
21
+ width: rect.width,
22
+ height: rect.height,
23
+ };
24
+ };
25
+
26
+ const WebHarnessUI: HarnessUIModule = {
27
+ simulatePress: async (x, y) => {
28
+ await window.__RN_HARNESS_SIMULATE_PRESS__(x, y);
29
+ },
30
+
31
+ queryByTestId: (testId) => {
32
+ const element = document.querySelector(`[data-testid="${testId}"]`);
33
+ return element ? getElementViewInfo(element) : null;
34
+ },
35
+
36
+ queryAllByTestId: (testId) => {
37
+ const elements = document.querySelectorAll(`[data-testid="${testId}"]`);
38
+ return Array.from(elements).map(getElementViewInfo);
39
+ },
40
+
41
+ queryByAccessibilityLabel: (label) => {
42
+ const element = document.querySelector(`[aria-label="${label}"]`);
43
+ return element ? getElementViewInfo(element) : null;
44
+ },
45
+
46
+ queryAllByAccessibilityLabel: (label) => {
47
+ const elements = document.querySelectorAll(`[aria-label="${label}"]`);
48
+ return Array.from(elements).map(getElementViewInfo);
49
+ },
50
+
51
+ captureScreenshot: async (bounds) => {
52
+ return await window.__RN_HARNESS_CAPTURE_SCREENSHOT__(bounds);
53
+ },
54
+
55
+ typeChar: async (character) => {
56
+ await window.__RN_HARNESS_TYPE_CHAR__(character);
57
+ },
58
+
59
+ blur: async (options) => {
60
+ if (options.submitEditing) {
61
+ // If we want to submit, we must NOT blur before pressing Enter.
62
+ // We let the runner-side bridge handle both Enter and the subsequent blur.
63
+ await window.__RN_HARNESS_BLUR__(options);
64
+ } else {
65
+ // If there is a focused element, blur it directly in the DOM first
66
+ // to trigger local events, then call the runner-side bridge.
67
+ if (
68
+ document.activeElement instanceof HTMLElement ||
69
+ document.activeElement instanceof SVGElement
70
+ ) {
71
+ document.activeElement.blur();
72
+ }
73
+ await window.__RN_HARNESS_BLUR__(options);
74
+ }
75
+ },
76
+ };
77
+
78
+ export default WebHarnessUI;
package/src/harness.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { Platform } from "react-native";
2
+ import { HarnessUIModule } from "./types.js";
3
+
4
+ const getHarnessUI = (): HarnessUIModule => {
5
+ if (Platform.OS === 'web') {
6
+ return require('./WebHarnessUI.js').default;
7
+ }
8
+
9
+ return require('./NativeHarnessUI.js').default;
10
+ }
11
+
12
+ export default getHarnessUI();
package/src/index.ts CHANGED
@@ -15,4 +15,4 @@ export {
15
15
  type ScreenshotResult,
16
16
  } from './screen.js';
17
17
  export { userEvent, type UserEvent } from './userEvent.js';
18
- export type { ViewInfo } from './NativeHarnessUI.js';
18
+ export type { ViewInfo } from './types.js';
package/src/screen.ts CHANGED
@@ -1,5 +1,6 @@
1
- import NativeHarnessUI, { type ViewInfo } from './NativeHarnessUI.js';
1
+ import { type ViewInfo } from './types.js';
2
2
  import { waitFor } from '@react-native-harness/runtime';
3
+ import HarnessUI from './harness.js';
3
4
 
4
5
  /**
5
6
  * Represents an element found on screen with its position and dimensions.
@@ -80,7 +81,7 @@ const createScreen = (): Screen => {
80
81
  return {
81
82
  findByTestId: async (testId: string): Promise<ElementReference> => {
82
83
  return waitFor(() => {
83
- const result = NativeHarnessUI.queryByTestId(testId);
84
+ const result = HarnessUI.queryByTestId(testId);
84
85
  if (!result) {
85
86
  throw new Error(`Unable to find element with testID: ${testId}`);
86
87
  }
@@ -90,7 +91,7 @@ const createScreen = (): Screen => {
90
91
 
91
92
  findAllByTestId: async (testId: string): Promise<ElementReference[]> => {
92
93
  return waitFor(() => {
93
- const results = NativeHarnessUI.queryAllByTestId(testId);
94
+ const results = HarnessUI.queryAllByTestId(testId);
94
95
  if (results.length === 0) {
95
96
  throw new Error(`Unable to find any elements with testID: ${testId}`);
96
97
  }
@@ -99,18 +100,18 @@ const createScreen = (): Screen => {
99
100
  },
100
101
 
101
102
  queryByTestId: (testId: string): ElementReference | null => {
102
- return NativeHarnessUI.queryByTestId(testId);
103
+ return HarnessUI.queryByTestId(testId);
103
104
  },
104
105
 
105
106
  queryAllByTestId: (testId: string): ElementReference[] => {
106
- return NativeHarnessUI.queryAllByTestId(testId);
107
+ return HarnessUI.queryAllByTestId(testId);
107
108
  },
108
109
 
109
110
  findByAccessibilityLabel: async (
110
111
  label: string
111
112
  ): Promise<ElementReference> => {
112
113
  return waitFor(() => {
113
- const result = NativeHarnessUI.queryByAccessibilityLabel(label);
114
+ const result = HarnessUI.queryByAccessibilityLabel(label);
114
115
  if (!result) {
115
116
  throw new Error(
116
117
  `Unable to find element with accessibility label: ${label}`
@@ -124,7 +125,7 @@ const createScreen = (): Screen => {
124
125
  label: string
125
126
  ): Promise<ElementReference[]> => {
126
127
  return waitFor(() => {
127
- const results = NativeHarnessUI.queryAllByAccessibilityLabel(label);
128
+ const results = HarnessUI.queryAllByAccessibilityLabel(label);
128
129
  if (results.length === 0) {
129
130
  throw new Error(
130
131
  `Unable to find any elements with accessibility label: ${label}`
@@ -135,18 +136,18 @@ const createScreen = (): Screen => {
135
136
  },
136
137
 
137
138
  queryByAccessibilityLabel: (label: string): ElementReference | null => {
138
- return NativeHarnessUI.queryByAccessibilityLabel(label);
139
+ return HarnessUI.queryByAccessibilityLabel(label);
139
140
  },
140
141
 
141
142
  queryAllByAccessibilityLabel: (label: string): ElementReference[] => {
142
- return NativeHarnessUI.queryAllByAccessibilityLabel(label);
143
+ return HarnessUI.queryAllByAccessibilityLabel(label);
143
144
  },
144
145
 
145
146
  screenshot: async (
146
147
  element?: ElementReference
147
148
  ): Promise<ScreenshotResult | null> => {
148
149
  const bounds = element ?? null;
149
- const base64String = await NativeHarnessUI.captureScreenshot(bounds);
150
+ const base64String = await HarnessUI.captureScreenshot(bounds);
150
151
 
151
152
  if (!base64String) {
152
153
  return null;
package/src/types.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Represents the position and dimensions of a view in screen coordinates (points/dp).
3
+ */
4
+ export interface ViewInfo {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ export interface HarnessUIModule {
12
+ /**
13
+ * Simulates a native press at the specified screen coordinates.
14
+ * Returns a promise that resolves when the press action is complete.
15
+ */
16
+ simulatePress(x: number, y: number): Promise<void>;
17
+
18
+ /**
19
+ * Finds a view by its testID (accessibilityIdentifier on iOS, tag on Android).
20
+ * Returns null if no view is found.
21
+ */
22
+ queryByTestId(testId: string): ViewInfo | null;
23
+
24
+ /**
25
+ * Finds all views by testID (accessibilityIdentifier on iOS, tag on Android).
26
+ * Returns an empty array if no views are found.
27
+ */
28
+ queryAllByTestId(testId: string): ViewInfo[];
29
+
30
+ /**
31
+ * Finds a view by its accessibility label.
32
+ * Returns null if no view is found.
33
+ */
34
+ queryByAccessibilityLabel(label: string): ViewInfo | null;
35
+
36
+ /**
37
+ * Finds all views by accessibility label.
38
+ * Returns an empty array if no views are found.
39
+ */
40
+ queryAllByAccessibilityLabel(label: string): ViewInfo[];
41
+
42
+ /**
43
+ * Captures a screenshot of the app UI.
44
+ * @param bounds Optional bounds to capture a specific region. Pass null to capture the entire window.
45
+ * @returns Promise resolving to Base64 encoded string containing PNG data, or null on failure.
46
+ */
47
+ captureScreenshot(bounds: ViewInfo | null): Promise<string | null>;
48
+
49
+ /**
50
+ * Types a single character into the currently focused text input.
51
+ * If no text input is focused, this is a no-op.
52
+ */
53
+ typeChar(character: string): Promise<void>;
54
+
55
+ /**
56
+ * Blurs (resigns first responder from) the currently focused element.
57
+ * Optionally triggers submitEditing event before blur.
58
+ */
59
+ blur(options: { submitEditing?: boolean }): Promise<void>;
60
+ }
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
  /**
@@ -66,13 +66,13 @@ const createUserEvent = (): UserEvent => {
66
66
  // Calculate center point of the element
67
67
  const centerX = element.x + element.width / 2;
68
68
  const centerY = element.y + element.height / 2;
69
- await NativeHarnessUI.simulatePress(centerX, centerY);
69
+ await HarnessUI.simulatePress(centerX, centerY);
70
70
  // Flush pending events to ensure onPress and other callbacks are processed
71
71
  await flushEvents();
72
72
  },
73
73
 
74
74
  pressAt: async (x: number, y: number): Promise<void> => {
75
- await NativeHarnessUI.simulatePress(x, y);
75
+ await HarnessUI.simulatePress(x, y);
76
76
  // Flush pending events to ensure onPress and other callbacks are processed
77
77
  await flushEvents();
78
78
  },
@@ -93,26 +93,26 @@ const createUserEvent = (): UserEvent => {
93
93
  // Calculate center point of the element
94
94
  const centerX = element.x + element.width / 2;
95
95
  const centerY = element.y + element.height / 2;
96
- await NativeHarnessUI.simulatePress(centerX, centerY);
96
+ await HarnessUI.simulatePress(centerX, centerY);
97
97
  await flushEvents();
98
98
  } else {
99
99
  // Still need to press to focus, but ideally without press events
100
100
  // For now, we press anyway - future enhancement could add a focusOnly method
101
101
  const centerX = element.x + element.width / 2;
102
102
  const centerY = element.y + element.height / 2;
103
- await NativeHarnessUI.simulatePress(centerX, centerY);
103
+ await HarnessUI.simulatePress(centerX, centerY);
104
104
  await flushEvents();
105
105
  }
106
106
 
107
107
  // Type each character one by one
108
108
  for (const char of text) {
109
- await NativeHarnessUI.typeChar(char);
109
+ await HarnessUI.typeChar(char);
110
110
  await flushEvents(); // Let onChangeText fire
111
111
  }
112
112
 
113
113
  // Blur (triggers endEditing and blur unless skipBlur)
114
114
  if (!options?.skipBlur) {
115
- await NativeHarnessUI.blur({
115
+ await HarnessUI.blur({
116
116
  submitEditing: options?.submitEditing ?? false,
117
117
  });
118
118
  await flushEvents();