@nyby/detox-component-testing 1.5.0 → 1.6.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/README.md +5 -50
- package/dist/ComponentHarness.d.ts +1 -1
- package/dist/ComponentHarness.d.ts.map +1 -1
- package/dist/ComponentHarness.js +10 -12
- package/dist/ComponentHarness.js.map +1 -1
- package/dist/ComponentRegistry.d.ts +1 -1
- package/dist/ComponentRegistry.d.ts.map +1 -1
- package/dist/ComponentRegistry.js +2 -2
- package/dist/ComponentRegistry.js.map +1 -1
- package/dist/configureHarness.d.ts +1 -4
- package/dist/configureHarness.d.ts.map +1 -1
- package/dist/configureHarness.js +0 -12
- package/dist/configureHarness.js.map +1 -1
- package/dist/debug.d.ts +8 -0
- package/dist/debug.d.ts.map +1 -1
- package/dist/debug.js +26 -32
- package/dist/debug.js.map +1 -1
- package/dist/environment.js +7 -37
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/mount.d.ts +0 -1
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +9 -10
- package/dist/mount.js.map +1 -1
- package/dist/test.d.ts +2 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js.map +1 -1
- package/package.json +3 -3
- package/src/ComponentHarness.tsx +46 -37
- package/src/ComponentRegistry.ts +7 -4
- package/src/configureHarness.ts +2 -14
- package/src/debug.ts +34 -34
- package/src/detox-env.d.ts +1 -1
- package/src/environment.js +7 -37
- package/src/index.ts +7 -4
- package/src/mount.ts +26 -15
- package/src/test.ts +2 -2
- package/dist/DebugTree.d.ts +0 -20
- package/dist/DebugTree.d.ts.map +0 -1
- package/dist/DebugTree.js +0 -239
- package/dist/DebugTree.js.map +0 -1
- package/src/DebugTree.tsx +0 -280
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nyby/detox-component-testing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Component testing support for Detox and React Native",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"react-native-launch-arguments": "*"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.5.0",
|
|
45
46
|
"@types/react": "^19.2.14",
|
|
46
47
|
"@types/react-native": "^0.72.8",
|
|
47
48
|
"prettier": "^3.8.1",
|
|
48
49
|
"typescript": "^5.9.3"
|
|
49
|
-
}
|
|
50
|
-
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
50
|
+
}
|
|
51
51
|
}
|
package/src/ComponentHarness.tsx
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
Component as ReactComponent,
|
|
3
|
+
useState,
|
|
4
|
+
useRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { View, Text, TextInput, ScrollView } from "react-native";
|
|
9
|
+
import { LaunchArguments } from "react-native-launch-arguments";
|
|
10
|
+
import { getComponent } from "./ComponentRegistry";
|
|
11
|
+
import { getWrapper } from "./configureHarness";
|
|
7
12
|
|
|
8
13
|
interface ErrorBoundaryState {
|
|
9
14
|
error: Error | null;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
class RenderErrorBoundary extends ReactComponent<
|
|
13
|
-
|
|
17
|
+
class RenderErrorBoundary extends ReactComponent<
|
|
18
|
+
{ children: ReactNode },
|
|
19
|
+
ErrorBoundaryState
|
|
20
|
+
> {
|
|
21
|
+
state: ErrorBoundaryState = { error: null };
|
|
14
22
|
|
|
15
23
|
static getDerivedStateFromError(error: Error) {
|
|
16
|
-
return {error};
|
|
24
|
+
return { error };
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
render() {
|
|
20
28
|
if (this.state.error) {
|
|
21
29
|
return (
|
|
22
30
|
<ScrollView testID="detox-render-error">
|
|
23
|
-
<Text testID="detox-render-error-message">
|
|
31
|
+
<Text testID="detox-render-error-message">
|
|
32
|
+
{this.state.error.message}
|
|
33
|
+
</Text>
|
|
24
34
|
</ScrollView>
|
|
25
35
|
);
|
|
26
36
|
}
|
|
@@ -28,8 +38,8 @@ class RenderErrorBoundary extends ReactComponent<{children: ReactNode}, ErrorBou
|
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
const PROP_PREFIX =
|
|
32
|
-
const SPY_PREFIX =
|
|
41
|
+
const PROP_PREFIX = "detoxProp_";
|
|
42
|
+
const SPY_PREFIX = "detoxSpy_";
|
|
33
43
|
|
|
34
44
|
interface MountPayload {
|
|
35
45
|
id: string;
|
|
@@ -51,14 +61,14 @@ function parseLaunchArgs(args: Record<string, any>): {
|
|
|
51
61
|
spies.push(key.slice(SPY_PREFIX.length));
|
|
52
62
|
}
|
|
53
63
|
});
|
|
54
|
-
return {props, spies};
|
|
64
|
+
return { props, spies };
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
export function ComponentHarness() {
|
|
58
68
|
const launchArgs = LaunchArguments.value() as Record<string, any>;
|
|
59
69
|
const [mountPayload, setMountPayload] = useState<MountPayload | null>(null);
|
|
60
70
|
|
|
61
|
-
const handleControl = useCallback((e: {nativeEvent: {text: string}}) => {
|
|
71
|
+
const handleControl = useCallback((e: { nativeEvent: { text: string } }) => {
|
|
62
72
|
try {
|
|
63
73
|
setMountPayload(JSON.parse(e.nativeEvent.text));
|
|
64
74
|
} catch (_e) {}
|
|
@@ -68,9 +78,9 @@ export function ComponentHarness() {
|
|
|
68
78
|
if (mountPayload) {
|
|
69
79
|
activeMount = mountPayload;
|
|
70
80
|
} else if (launchArgs.detoxComponentName) {
|
|
71
|
-
const {props, spies} = parseLaunchArgs(launchArgs);
|
|
81
|
+
const { props, spies } = parseLaunchArgs(launchArgs);
|
|
72
82
|
activeMount = {
|
|
73
|
-
id:
|
|
83
|
+
id: "0",
|
|
74
84
|
name: launchArgs.detoxComponentName as string,
|
|
75
85
|
props,
|
|
76
86
|
spies,
|
|
@@ -78,12 +88,12 @@ export function ComponentHarness() {
|
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
return (
|
|
81
|
-
<View style={{flex: 1}}>
|
|
91
|
+
<View style={{ flex: 1 }}>
|
|
82
92
|
<TextInput
|
|
83
93
|
testID="detox-harness-control"
|
|
84
94
|
onEndEditing={handleControl}
|
|
85
95
|
style={{
|
|
86
|
-
position:
|
|
96
|
+
position: "absolute",
|
|
87
97
|
bottom: 0,
|
|
88
98
|
left: 0,
|
|
89
99
|
right: 0,
|
|
@@ -94,7 +104,7 @@ export function ComponentHarness() {
|
|
|
94
104
|
/>
|
|
95
105
|
{activeMount && (
|
|
96
106
|
<>
|
|
97
|
-
<Text testID="detox-mount-id" style={{height: 1}}>
|
|
107
|
+
<Text testID="detox-mount-id" style={{ height: 1 }}>
|
|
98
108
|
{activeMount.id}
|
|
99
109
|
</Text>
|
|
100
110
|
<RenderErrorBoundary>
|
|
@@ -111,13 +121,13 @@ interface SpyData {
|
|
|
111
121
|
lastArgs: any[];
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
function ComponentRenderer({mount}: {mount: MountPayload}) {
|
|
115
|
-
const {Component, defaultProps} = getComponent(mount.name);
|
|
124
|
+
function ComponentRenderer({ mount }: { mount: MountPayload }) {
|
|
125
|
+
const { Component, defaultProps } = getComponent(mount.name);
|
|
116
126
|
const spyNames = mount.spies || [];
|
|
117
127
|
|
|
118
128
|
const initialData: Record<string, SpyData> = {};
|
|
119
129
|
spyNames.forEach((name) => {
|
|
120
|
-
initialData[name] = {count: 0, lastArgs: []};
|
|
130
|
+
initialData[name] = { count: 0, lastArgs: [] };
|
|
121
131
|
});
|
|
122
132
|
|
|
123
133
|
const [spyData, setSpyData] = useState(initialData);
|
|
@@ -139,25 +149,24 @@ function ComponentRenderer({mount}: {mount: MountPayload}) {
|
|
|
139
149
|
spyProps[name] = spyFnsRef.current[name];
|
|
140
150
|
});
|
|
141
151
|
|
|
142
|
-
const props = {...defaultProps, ...(mount.props || {}), ...spyProps};
|
|
152
|
+
const props = { ...defaultProps, ...(mount.props || {}), ...spyProps };
|
|
143
153
|
const Wrapper = getWrapper();
|
|
144
|
-
const debugTreeConfig = getDebugTreeConfig();
|
|
145
|
-
|
|
146
|
-
const content = (
|
|
147
|
-
<View testID="component-harness-root" style={{flex: 1}}>
|
|
148
|
-
<Component {...props} />
|
|
149
|
-
{spyNames.map((name) => (
|
|
150
|
-
<View key={name}>
|
|
151
|
-
<Text testID={`spy-${name}-count`}>{String(spyData[name].count)}</Text>
|
|
152
|
-
<Text testID={`spy-${name}-lastArgs`}>{JSON.stringify(spyData[name].lastArgs)}</Text>
|
|
153
|
-
</View>
|
|
154
|
-
))}
|
|
155
|
-
</View>
|
|
156
|
-
);
|
|
157
154
|
|
|
158
155
|
return (
|
|
159
156
|
<Wrapper launchArgs={mount.props || {}}>
|
|
160
|
-
|
|
157
|
+
<View testID="component-harness-root" style={{ flex: 1 }}>
|
|
158
|
+
<Component {...props} />
|
|
159
|
+
{spyNames.map((name) => (
|
|
160
|
+
<View key={name}>
|
|
161
|
+
<Text testID={`spy-${name}-count`}>
|
|
162
|
+
{String(spyData[name].count)}
|
|
163
|
+
</Text>
|
|
164
|
+
<Text testID={`spy-${name}-lastArgs`}>
|
|
165
|
+
{JSON.stringify(spyData[name].lastArgs)}
|
|
166
|
+
</Text>
|
|
167
|
+
</View>
|
|
168
|
+
))}
|
|
169
|
+
</View>
|
|
161
170
|
</Wrapper>
|
|
162
171
|
);
|
|
163
172
|
}
|
package/src/ComponentRegistry.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ComponentType} from
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
2
|
|
|
3
3
|
export interface ComponentEntry<P = any> {
|
|
4
4
|
Component: ComponentType<P>;
|
|
@@ -12,13 +12,16 @@ export function registerComponent<P>(
|
|
|
12
12
|
Component: ComponentType<P>,
|
|
13
13
|
defaultProps?: Partial<P>,
|
|
14
14
|
): void;
|
|
15
|
-
export function registerComponent<P>(
|
|
15
|
+
export function registerComponent<P>(
|
|
16
|
+
Component: ComponentType<P>,
|
|
17
|
+
defaultProps?: Partial<P>,
|
|
18
|
+
): void;
|
|
16
19
|
export function registerComponent<P>(
|
|
17
20
|
nameOrComponent: string | ComponentType<P>,
|
|
18
21
|
componentOrProps?: ComponentType<P> | Partial<P>,
|
|
19
22
|
defaultProps?: Partial<P>,
|
|
20
23
|
): void {
|
|
21
|
-
if (typeof nameOrComponent ===
|
|
24
|
+
if (typeof nameOrComponent === "string") {
|
|
22
25
|
registry.set(nameOrComponent, {
|
|
23
26
|
Component: componentOrProps as ComponentType<P>,
|
|
24
27
|
defaultProps: (defaultProps || {}) as Partial<P>,
|
|
@@ -28,7 +31,7 @@ export function registerComponent<P>(
|
|
|
28
31
|
const name = Component.displayName || Component.name;
|
|
29
32
|
if (!name) {
|
|
30
33
|
throw new Error(
|
|
31
|
-
|
|
34
|
+
"[detox-component-testing] Component must have a name or displayName to register without an explicit name.",
|
|
32
35
|
);
|
|
33
36
|
}
|
|
34
37
|
registry.set(name, {
|
package/src/configureHarness.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {ComponentType, ReactNode} from
|
|
2
|
-
import {DebugTreeProps} from './DebugTree';
|
|
1
|
+
import { ComponentType, ReactNode } from "react";
|
|
3
2
|
|
|
4
3
|
export interface WrapperProps {
|
|
5
4
|
children: ReactNode;
|
|
@@ -8,29 +7,18 @@ export interface WrapperProps {
|
|
|
8
7
|
|
|
9
8
|
export interface HarnessConfig {
|
|
10
9
|
wrapper?: ComponentType<WrapperProps>;
|
|
11
|
-
debugTree?: boolean | Omit<DebugTreeProps, 'children'>;
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
const DefaultWrapper = ({children}: WrapperProps) => children;
|
|
12
|
+
const DefaultWrapper = ({ children }: WrapperProps) => children;
|
|
15
13
|
|
|
16
14
|
let globalWrapper: ComponentType<WrapperProps> | null = null;
|
|
17
|
-
let globalDebugTree: boolean | Omit<DebugTreeProps, 'children'> | null = null;
|
|
18
15
|
|
|
19
16
|
export function configureHarness(config: HarnessConfig): void {
|
|
20
17
|
if (config.wrapper) {
|
|
21
18
|
globalWrapper = config.wrapper;
|
|
22
19
|
}
|
|
23
|
-
if (config.debugTree !== undefined) {
|
|
24
|
-
globalDebugTree = config.debugTree;
|
|
25
|
-
}
|
|
26
20
|
}
|
|
27
21
|
|
|
28
22
|
export function getWrapper(): ComponentType<WrapperProps> {
|
|
29
23
|
return globalWrapper || DefaultWrapper;
|
|
30
24
|
}
|
|
31
|
-
|
|
32
|
-
export function getDebugTreeConfig(): Omit<DebugTreeProps, 'children'> | null {
|
|
33
|
-
if (!globalDebugTree) return null;
|
|
34
|
-
if (globalDebugTree === true) return {};
|
|
35
|
-
return globalDebugTree;
|
|
36
|
-
}
|
package/src/debug.ts
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import {mkdirSync, renameSync, writeFileSync} from
|
|
2
|
-
import {join} from
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Capture a screenshot
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* import { debug } from '@nyby/detox-component-testing/test';
|
|
10
|
-
* await debug(); // artifacts/debug-1.png, debug-1-tree.json, debug-1-view.xml
|
|
11
|
-
* await debug('after-tap'); // artifacts/debug-after-tap.png, etc.
|
|
5
|
+
* Capture a screenshot and native view hierarchy to the given directory.
|
|
6
|
+
* Used by both the `debug()` helper and the custom test environment.
|
|
12
7
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const viewPath = join(dir, `debug-${name}-view.xml`);
|
|
8
|
+
export async function captureArtifacts(
|
|
9
|
+
name: string,
|
|
10
|
+
outputDir: string,
|
|
11
|
+
deviceRef: {
|
|
12
|
+
takeScreenshot: (n: string) => Promise<string>;
|
|
13
|
+
generateViewHierarchyXml: () => Promise<string>;
|
|
14
|
+
},
|
|
15
|
+
) {
|
|
16
|
+
mkdirSync(outputDir, { recursive: true });
|
|
23
17
|
|
|
24
18
|
// Screenshot via Detox, then move to our artifacts dir
|
|
25
19
|
try {
|
|
26
|
-
const tempPath = await
|
|
20
|
+
const tempPath = await deviceRef.takeScreenshot(`debug-${name}`);
|
|
27
21
|
if (tempPath) {
|
|
28
|
-
renameSync(tempPath,
|
|
22
|
+
renameSync(tempPath, join(outputDir, `debug-${name}.png`));
|
|
29
23
|
}
|
|
30
24
|
} catch {}
|
|
31
25
|
|
|
32
|
-
// React component tree via DebugTree harness
|
|
33
|
-
try {
|
|
34
|
-
await element(by.id('debug-tree-control')).replaceText('dump');
|
|
35
|
-
await waitFor(element(by.id('debug-tree-output')))
|
|
36
|
-
.toExist()
|
|
37
|
-
.withTimeout(3000);
|
|
38
|
-
const attrs = await element(by.id('debug-tree-output')).getAttributes();
|
|
39
|
-
const tree = (attrs as any).text || (attrs as any).label || '[]';
|
|
40
|
-
writeFileSync(treePath, tree, 'utf8');
|
|
41
|
-
} catch {}
|
|
42
|
-
|
|
43
26
|
// Native view hierarchy
|
|
44
27
|
try {
|
|
45
|
-
const xml = await
|
|
46
|
-
writeFileSync(
|
|
28
|
+
const xml = await deviceRef.generateViewHierarchyXml();
|
|
29
|
+
writeFileSync(join(outputDir, `debug-${name}-view.xml`), xml, "utf8");
|
|
47
30
|
} catch {}
|
|
48
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Capture a screenshot and native view hierarchy.
|
|
35
|
+
* Drop this anywhere in a test to inspect the current screen state.
|
|
36
|
+
*
|
|
37
|
+
* Usage:
|
|
38
|
+
* import { debug } from '@nyby/detox-component-testing/test';
|
|
39
|
+
* await debug(); // artifacts/debug-1.png, debug-1-view.xml
|
|
40
|
+
* await debug('after-tap'); // artifacts/debug-after-tap.png, etc.
|
|
41
|
+
*/
|
|
42
|
+
let counter = 0;
|
|
43
|
+
|
|
44
|
+
export async function debug(label?: string, outputDir?: string) {
|
|
45
|
+
const name = label || String(++counter);
|
|
46
|
+
const dir = outputDir || join(process.cwd(), "artifacts");
|
|
47
|
+
await captureArtifacts(name, dir, device);
|
|
48
|
+
}
|
package/src/detox-env.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ declare const device: {
|
|
|
5
5
|
generateViewHierarchyXml(): Promise<string>;
|
|
6
6
|
};
|
|
7
7
|
declare function element(matcher: any): any;
|
|
8
|
-
declare const by: {id(id: string): any};
|
|
8
|
+
declare const by: { id(id: string): any };
|
|
9
9
|
declare function waitFor(e: any): any;
|
|
10
10
|
declare function expect(e: any): any;
|
package/src/environment.js
CHANGED
|
@@ -1,47 +1,17 @@
|
|
|
1
|
-
const DetoxCircusEnvironment = require(
|
|
2
|
-
const {
|
|
3
|
-
const {
|
|
1
|
+
const DetoxCircusEnvironment = require("detox/runners/jest/testEnvironment");
|
|
2
|
+
const { join } = require("path");
|
|
3
|
+
const { captureArtifacts } = require("./debug");
|
|
4
4
|
|
|
5
5
|
class CustomDetoxEnvironment extends DetoxCircusEnvironment {
|
|
6
6
|
async handleTestEvent(event, state) {
|
|
7
7
|
await super.handleTestEvent(event, state);
|
|
8
8
|
|
|
9
|
-
if (event.name ===
|
|
10
|
-
|
|
9
|
+
if (event.name === "test_done" && event.test.errors.length > 0) {
|
|
10
|
+
const safeName = event.test.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
11
|
+
const outputDir = join(process.cwd(), "artifacts");
|
|
12
|
+
await captureArtifacts(safeName, outputDir, this.global.device);
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
|
-
|
|
14
|
-
async _dumpDebugInfo(test) {
|
|
15
|
-
const outputDir = join(process.cwd(), 'artifacts');
|
|
16
|
-
const safeName = test.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
17
|
-
|
|
18
|
-
mkdirSync(outputDir, {recursive: true});
|
|
19
|
-
|
|
20
|
-
// Screenshot (saved by Detox into its own artifacts folder)
|
|
21
|
-
try {
|
|
22
|
-
await this.global.device.takeScreenshot(`debug-${safeName}`);
|
|
23
|
-
} catch (_e) {}
|
|
24
|
-
|
|
25
|
-
// Component tree via DebugTree
|
|
26
|
-
try {
|
|
27
|
-
const {element, by, waitFor} = this.global;
|
|
28
|
-
|
|
29
|
-
await element(by.id('debug-tree-control')).replaceText('dump');
|
|
30
|
-
await waitFor(element(by.id('debug-tree-output')))
|
|
31
|
-
.toExist()
|
|
32
|
-
.withTimeout(3000);
|
|
33
|
-
|
|
34
|
-
const attrs = await element(by.id('debug-tree-output')).getAttributes();
|
|
35
|
-
const tree = attrs.text || attrs.label || '[]';
|
|
36
|
-
writeFileSync(join(outputDir, `componenttree-${safeName}.json`), tree, 'utf8');
|
|
37
|
-
} catch (_e) {}
|
|
38
|
-
|
|
39
|
-
// Native view hierarchy
|
|
40
|
-
try {
|
|
41
|
-
const xml = await this.global.device.generateViewHierarchyXml();
|
|
42
|
-
writeFileSync(join(outputDir, `viewhierarchy-${safeName}.xml`), xml, 'utf8');
|
|
43
|
-
} catch (_e) {}
|
|
44
|
-
}
|
|
45
15
|
}
|
|
46
16
|
|
|
47
17
|
module.exports = CustomDetoxEnvironment;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export {registerComponent} from
|
|
2
|
-
export {ComponentHarness} from
|
|
3
|
-
export {
|
|
4
|
-
|
|
1
|
+
export { registerComponent } from "./ComponentRegistry";
|
|
2
|
+
export { ComponentHarness } from "./ComponentHarness";
|
|
3
|
+
export {
|
|
4
|
+
configureHarness,
|
|
5
|
+
WrapperProps,
|
|
6
|
+
HarnessConfig,
|
|
7
|
+
} from "./configureHarness";
|
package/src/mount.ts
CHANGED
|
@@ -9,32 +9,37 @@ export interface SpyExpectation {
|
|
|
9
9
|
lastCalledWith(...args: any[]): Promise<void>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const SPY_MARKER =
|
|
12
|
+
const SPY_MARKER = "__detoxSpy__" as const;
|
|
13
13
|
|
|
14
14
|
let mountCounter = 0;
|
|
15
15
|
let appLaunched = false;
|
|
16
16
|
|
|
17
17
|
export function spy(name: string): SpyMarker {
|
|
18
|
-
return {[SPY_MARKER]: true, name};
|
|
18
|
+
return { [SPY_MARKER]: true, name };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
type MountProps = Record<string, string | number | boolean | SpyMarker>;
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
async function assertNoRenderError(): Promise<void> {
|
|
24
24
|
try {
|
|
25
|
-
await waitFor(element(by.id(
|
|
25
|
+
await waitFor(element(by.id("detox-render-error")))
|
|
26
26
|
.toExist()
|
|
27
27
|
.withTimeout(500);
|
|
28
28
|
} catch {
|
|
29
29
|
return; // Element not found — no render error, all good
|
|
30
30
|
}
|
|
31
31
|
// Element exists — read the error message and throw
|
|
32
|
-
const attrs = (await element(
|
|
33
|
-
|
|
32
|
+
const attrs = (await element(
|
|
33
|
+
by.id("detox-render-error-message"),
|
|
34
|
+
).getAttributes()) as any;
|
|
35
|
+
const message = attrs.text || attrs.label || "Unknown render error";
|
|
34
36
|
throw new Error(`Component render error: ${message}`);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
export async function mount(
|
|
39
|
+
export async function mount(
|
|
40
|
+
componentName: string,
|
|
41
|
+
props?: MountProps,
|
|
42
|
+
): Promise<void> {
|
|
38
43
|
const payload = {
|
|
39
44
|
id: String(++mountCounter),
|
|
40
45
|
name: componentName,
|
|
@@ -44,7 +49,7 @@ export async function mount(componentName: string, props?: MountProps): Promise<
|
|
|
44
49
|
|
|
45
50
|
if (props) {
|
|
46
51
|
Object.entries(props).forEach(([key, value]) => {
|
|
47
|
-
if (value && typeof value ===
|
|
52
|
+
if (value && typeof value === "object" && SPY_MARKER in value) {
|
|
48
53
|
payload.spies.push(key);
|
|
49
54
|
} else {
|
|
50
55
|
payload.props[key] = value;
|
|
@@ -52,19 +57,19 @@ export async function mount(componentName: string, props?: MountProps): Promise<
|
|
|
52
57
|
});
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
const launchArgs: Record<string, any> = {detoxComponentName: componentName};
|
|
60
|
+
const launchArgs: Record<string, any> = { detoxComponentName: componentName };
|
|
56
61
|
Object.entries(payload.props).forEach(([key, value]) => {
|
|
57
62
|
launchArgs[`detoxProp_${key}`] = value;
|
|
58
63
|
});
|
|
59
64
|
payload.spies.forEach((name) => {
|
|
60
65
|
launchArgs[`detoxSpy_${name}`] = true;
|
|
61
66
|
});
|
|
62
|
-
await device.launchApp({newInstance: true, launchArgs});
|
|
67
|
+
await device.launchApp({ newInstance: true, launchArgs });
|
|
63
68
|
appLaunched = true;
|
|
64
69
|
// Harness sets id '0' for the initial launch-args mount
|
|
65
70
|
try {
|
|
66
|
-
await waitFor(element(by.id(
|
|
67
|
-
.toHaveText(
|
|
71
|
+
await waitFor(element(by.id("detox-mount-id")))
|
|
72
|
+
.toHaveText("0")
|
|
68
73
|
.withTimeout(5000);
|
|
69
74
|
} catch (e) {
|
|
70
75
|
await assertNoRenderError(); // Throws with the actual error if one exists
|
|
@@ -79,13 +84,19 @@ export function expectSpy(name: string): SpyExpectation {
|
|
|
79
84
|
const getExpect = () => expect as unknown as (e: any) => any;
|
|
80
85
|
return {
|
|
81
86
|
async toHaveBeenCalled() {
|
|
82
|
-
await getExpect()(element(by.id(`spy-${name}-count`))).not.toHaveText(
|
|
87
|
+
await getExpect()(element(by.id(`spy-${name}-count`))).not.toHaveText(
|
|
88
|
+
"0",
|
|
89
|
+
);
|
|
83
90
|
},
|
|
84
91
|
async toHaveBeenCalledTimes(n: number) {
|
|
85
|
-
await getExpect()(element(by.id(`spy-${name}-count`))).toHaveText(
|
|
92
|
+
await getExpect()(element(by.id(`spy-${name}-count`))).toHaveText(
|
|
93
|
+
String(n),
|
|
94
|
+
);
|
|
86
95
|
},
|
|
87
96
|
async lastCalledWith(...args: any[]) {
|
|
88
|
-
await getExpect()(element(by.id(`spy-${name}-lastArgs`))).toHaveText(
|
|
97
|
+
await getExpect()(element(by.id(`spy-${name}-lastArgs`))).toHaveText(
|
|
98
|
+
JSON.stringify(args),
|
|
99
|
+
);
|
|
89
100
|
},
|
|
90
101
|
};
|
|
91
102
|
}
|
package/src/test.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {mount, spy, expectSpy} from
|
|
2
|
-
export {debug} from
|
|
1
|
+
export { mount, spy, expectSpy } from "./mount";
|
|
2
|
+
export { debug } from "./debug";
|
package/dist/DebugTree.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
export declare function captureTree(ref: any, usefulProps?: Set<string>, skipNames?: Set<string>, nativeDuplicates?: Set<string>): string;
|
|
3
|
-
export interface DebugTreeProps {
|
|
4
|
-
children: React.ReactNode;
|
|
5
|
-
/** Props to include in tree output. Defaults to testID, accessibilityLabel, etc. */
|
|
6
|
-
usefulProps?: string[];
|
|
7
|
-
/** Component names to skip (internal wrappers that add noise). */
|
|
8
|
-
skipNames?: string[];
|
|
9
|
-
/** Native component names that duplicate their parent (e.g. RCTText inside Text). */
|
|
10
|
-
nativeDuplicates?: string[];
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Debug component that captures the React component tree on demand.
|
|
14
|
-
* Wrap your test content with this. Trigger via Detox:
|
|
15
|
-
*
|
|
16
|
-
* await element(by.id('debug-tree-control')).replaceText('dump');
|
|
17
|
-
* const attrs = await element(by.id('debug-tree-output')).getAttributes();
|
|
18
|
-
*/
|
|
19
|
-
export declare function DebugTree({ children, usefulProps, skipNames, nativeDuplicates }: DebugTreeProps): React.JSX.Element;
|
|
20
|
-
//# sourceMappingURL=DebugTree.d.ts.map
|
package/dist/DebugTree.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"DebugTree.d.ts","sourceRoot":"","sources":["../src/DebugTree.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAiN3D,wBAAgB,WAAW,CACzB,GAAG,EAAE,GAAG,EACR,WAAW,GAAE,GAAG,CAAC,MAAM,CAAwB,EAC/C,SAAS,GAAE,GAAG,CAAC,MAAM,CAAsB,EAC3C,gBAAgB,GAAE,GAAG,CAAC,MAAM,CAA6B,GACxD,MAAM,CAQR;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,gBAAgB,EAAC,EAAE,cAAc,qBAsC7F"}
|