@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.
- 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 +0 -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 +14 -0
- package/dist/WebHarnessUI.d.ts.map +1 -0
- package/dist/WebHarnessUI.js +53 -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 +1 -1
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +10 -10
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/userEvent.js +7 -7
- package/package.json +2 -2
- package/src/NativeHarnessUI.ts +1 -45
- package/src/WebHarnessUI.ts +78 -0
- package/src/harness.ts +12 -0
- package/src/index.ts +1 -1
- package/src/screen.ts +11 -10
- package/src/types.ts +60 -0
- package/src/userEvent.ts +7 -7
package/src/NativeHarnessUI.ts
CHANGED
|
@@ -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
package/src/screen.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
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 =
|
|
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 =
|
|
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
|
|
103
|
+
return HarnessUI.queryByTestId(testId);
|
|
103
104
|
},
|
|
104
105
|
|
|
105
106
|
queryAllByTestId: (testId: string): ElementReference[] => {
|
|
106
|
-
return
|
|
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 =
|
|
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 =
|
|
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
|
|
139
|
+
return HarnessUI.queryByAccessibilityLabel(label);
|
|
139
140
|
},
|
|
140
141
|
|
|
141
142
|
queryAllByAccessibilityLabel: (label: string): ElementReference[] => {
|
|
142
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
115
|
+
await HarnessUI.blur({
|
|
116
116
|
submitEditing: options?.submitEditing ?? false,
|
|
117
117
|
});
|
|
118
118
|
await flushEvents();
|