@react-native-harness/jest 1.4.0-rc.1 → 1.4.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/dist/__tests__/bridge.test.js +6 -2
- package/dist/__tests__/crash-monitor.test.js +64 -0
- package/dist/__tests__/environment-reset.test.d.ts +2 -0
- package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
- package/dist/__tests__/environment-reset.test.js +238 -0
- package/dist/__tests__/execute-run.test.js +250 -4
- package/dist/__tests__/format-harness-error.test.d.ts +2 -0
- package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
- package/dist/__tests__/format-harness-error.test.js +27 -0
- package/dist/__tests__/harness-cache.test.js +62 -41
- package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
- package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session-signal.test.js +298 -0
- package/dist/__tests__/harness-session.test.js +161 -2
- package/dist/__tests__/index.test.js +3 -1
- package/dist/__tests__/metro-port.test.js +2 -0
- package/dist/__tests__/run.test.d.ts +2 -0
- package/dist/__tests__/run.test.d.ts.map +1 -0
- package/dist/__tests__/run.test.js +48 -0
- package/dist/crash-monitor.d.ts +7 -0
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +19 -0
- package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/derivers.test.js +248 -0
- package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
- package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
- package/dist/diagnostics/__tests__/report.test.js +97 -0
- package/dist/diagnostics/derivers.d.ts +24 -0
- package/dist/diagnostics/derivers.d.ts.map +1 -0
- package/dist/diagnostics/derivers.js +146 -0
- package/dist/diagnostics/index.d.ts +3 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +2 -0
- package/dist/diagnostics/report.d.ts +7 -0
- package/dist/diagnostics/report.d.ts.map +1 -0
- package/dist/diagnostics/report.js +84 -0
- package/dist/environment-reset.d.ts +51 -0
- package/dist/environment-reset.d.ts.map +1 -0
- package/dist/environment-reset.js +83 -0
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +121 -69
- package/dist/format-harness-error.d.ts +5 -0
- package/dist/format-harness-error.d.ts.map +1 -0
- package/dist/format-harness-error.js +43 -0
- package/dist/harness-session.d.ts +26 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +260 -87
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +21 -4
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/bridge.test.ts +8 -3
- package/src/__tests__/crash-monitor.test.ts +84 -0
- package/src/__tests__/environment-reset.test.ts +308 -0
- package/src/__tests__/execute-run.test.ts +327 -4
- package/src/__tests__/format-harness-error.test.ts +32 -0
- package/src/__tests__/harness-cache.test.ts +91 -0
- package/src/__tests__/harness-session.test.ts +211 -1
- package/src/__tests__/index.test.ts +3 -1
- package/src/__tests__/metro-port.test.ts +2 -0
- package/src/__tests__/run.test.ts +74 -0
- package/src/crash-monitor.ts +28 -0
- package/src/diagnostics/__tests__/derivers.test.ts +300 -0
- package/src/diagnostics/__tests__/report.test.ts +128 -0
- package/src/diagnostics/derivers.ts +170 -0
- package/src/diagnostics/index.ts +2 -0
- package/src/diagnostics/report.ts +124 -0
- package/src/environment-reset.ts +138 -0
- package/src/execute-run.ts +136 -77
- package/src/format-harness-error.ts +73 -0
- package/src/harness-session.ts +369 -131
- package/src/run.ts +23 -4
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { runHarnessTestFile } from '../run.js';
|
|
3
|
+
vi.mock('jest-message-util', () => ({
|
|
4
|
+
formatResultsErrors: vi.fn(() => ''),
|
|
5
|
+
}));
|
|
6
|
+
const createHarnessResult = () => ({
|
|
7
|
+
name: 'root',
|
|
8
|
+
tests: [],
|
|
9
|
+
suites: [],
|
|
10
|
+
status: 'passed',
|
|
11
|
+
duration: 0,
|
|
12
|
+
});
|
|
13
|
+
const createSession = (testTimeout) => ({
|
|
14
|
+
config: { testTimeout },
|
|
15
|
+
context: {
|
|
16
|
+
platform: {
|
|
17
|
+
runner: './runner.js',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
runTestFile: vi.fn(async () => createHarnessResult()),
|
|
21
|
+
});
|
|
22
|
+
const createGlobalConfig = (overrides = {}) => ({
|
|
23
|
+
rootDir: '/project',
|
|
24
|
+
...overrides,
|
|
25
|
+
});
|
|
26
|
+
const createProjectConfig = (overrides = {}) => overrides;
|
|
27
|
+
describe('runHarnessTestFile', () => {
|
|
28
|
+
it('uses Harness config testTimeout when Jest config does not set one', async () => {
|
|
29
|
+
const session = createSession(15000);
|
|
30
|
+
await runHarnessTestFile({
|
|
31
|
+
testPath: '/project/example.harness.ts',
|
|
32
|
+
session,
|
|
33
|
+
globalConfig: createGlobalConfig(),
|
|
34
|
+
projectConfig: createProjectConfig(),
|
|
35
|
+
});
|
|
36
|
+
expect(session.runTestFile).toHaveBeenCalledWith('example.harness.ts', expect.objectContaining({ testTimeout: 15000 }));
|
|
37
|
+
});
|
|
38
|
+
it('keeps Harness config testTimeout above Jest config testTimeout', async () => {
|
|
39
|
+
const session = createSession(15000);
|
|
40
|
+
await runHarnessTestFile({
|
|
41
|
+
testPath: '/project/example.harness.ts',
|
|
42
|
+
session,
|
|
43
|
+
globalConfig: createGlobalConfig({ testTimeout: 20000 }),
|
|
44
|
+
projectConfig: createProjectConfig({ testTimeout: 30000 }),
|
|
45
|
+
});
|
|
46
|
+
expect(session.runTestFile).toHaveBeenCalledWith('example.harness.ts', expect.objectContaining({ testTimeout: 15000 }));
|
|
47
|
+
});
|
|
48
|
+
});
|
package/dist/crash-monitor.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export type CrashMonitor = {
|
|
|
15
15
|
reset: () => void;
|
|
16
16
|
setAppSession: (session: AppSession | null) => void;
|
|
17
17
|
handleBridgeDisconnect: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Opens a window during which handleBridgeDisconnect() is suppressed,
|
|
20
|
+
* for use around operations (like a runtime reset) that intentionally
|
|
21
|
+
* disconnect the bridge without the app process dying. Returns a closer
|
|
22
|
+
* that ends the window; safe to call multiple times or nest.
|
|
23
|
+
*/
|
|
24
|
+
expectDisconnect: () => () => void;
|
|
18
25
|
dispose: () => Promise<void>;
|
|
19
26
|
};
|
|
20
27
|
export type CrashMonitorOptions = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAKhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAC;AAOrB,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,UAAU,CAAC;IACrE,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IACpD,sBAAsB,EAAE,MAAM,IAAI,CAAC;IACnC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC,CAAC;AA0EF,eAAO,MAAM,kBAAkB,GAAI,qCAEhC,mBAAwB,KAAG,
|
|
1
|
+
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAKhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAC;AAOrB,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,UAAU,CAAC;IACrE,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IACpD,sBAAsB,EAAE,MAAM,IAAI,CAAC;IACnC;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,MAAM,IAAI,CAAC;IACnC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC,CAAC;AA0EF,eAAO,MAAM,kBAAkB,GAAI,qCAEhC,mBAAwB,KAAG,YA8M7B,CAAC"}
|
package/dist/crash-monitor.js
CHANGED
|
@@ -55,6 +55,7 @@ export const createCrashMonitor = ({ appSession: initialAppSession = null, } = {
|
|
|
55
55
|
let pendingTimer = null;
|
|
56
56
|
let currentTestFilePath = '';
|
|
57
57
|
let currentPhase = 'startup';
|
|
58
|
+
let expectedDisconnectDepth = 0;
|
|
58
59
|
const watchers = new Set();
|
|
59
60
|
const notifyFailure = (err) => {
|
|
60
61
|
const pending = [...watchers];
|
|
@@ -154,6 +155,17 @@ export const createCrashMonitor = ({ appSession: initialAppSession = null, } = {
|
|
|
154
155
|
};
|
|
155
156
|
return { promise, cancel };
|
|
156
157
|
};
|
|
158
|
+
const expectDisconnect = () => {
|
|
159
|
+
expectedDisconnectDepth += 1;
|
|
160
|
+
let closed = false;
|
|
161
|
+
return () => {
|
|
162
|
+
if (closed) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
closed = true;
|
|
166
|
+
expectedDisconnectDepth = Math.max(0, expectedDisconnectDepth - 1);
|
|
167
|
+
};
|
|
168
|
+
};
|
|
157
169
|
return {
|
|
158
170
|
watch,
|
|
159
171
|
isAlive: () => alive,
|
|
@@ -170,15 +182,22 @@ export const createCrashMonitor = ({ appSession: initialAppSession = null, } = {
|
|
|
170
182
|
watchers.clear();
|
|
171
183
|
isResolvingCrash = false;
|
|
172
184
|
currentTestFilePath = '';
|
|
185
|
+
expectedDisconnectDepth = 0;
|
|
173
186
|
clearPendingTimer();
|
|
174
187
|
},
|
|
175
188
|
setAppSession,
|
|
176
189
|
handleBridgeDisconnect: () => {
|
|
190
|
+
if (expectedDisconnectDepth > 0) {
|
|
191
|
+
crashLogger.debug('ignoring expected bridge disconnect');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
177
194
|
startCrashResolution('bridge-disconnect');
|
|
178
195
|
},
|
|
196
|
+
expectDisconnect,
|
|
179
197
|
dispose: async () => {
|
|
180
198
|
disposed = true;
|
|
181
199
|
monitoring = false;
|
|
200
|
+
expectedDisconnectDepth = 0;
|
|
182
201
|
watchers.clear();
|
|
183
202
|
isResolvingCrash = false;
|
|
184
203
|
clearPendingTimer();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derivers.test.d.ts","sourceRoot":"","sources":["../../../src/diagnostics/__tests__/derivers.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createDiagnostics, getEmitter } from '@react-native-harness/tools';
|
|
3
|
+
import { observeBridgeEvents, observeMetroEvents } from '../derivers.js';
|
|
4
|
+
const createFakeBridge = () => {
|
|
5
|
+
const listeners = {
|
|
6
|
+
connected: new Set(),
|
|
7
|
+
disconnected: new Set(),
|
|
8
|
+
event: new Set(),
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
on: (event, listener) => {
|
|
12
|
+
listeners[event].add(listener);
|
|
13
|
+
},
|
|
14
|
+
off: (event, listener) => {
|
|
15
|
+
listeners[event].delete(listener);
|
|
16
|
+
},
|
|
17
|
+
emitConnected: () => listeners.connected.forEach((l) => l({})),
|
|
18
|
+
emitDisconnected: () => listeners.disconnected.forEach((l) => l()),
|
|
19
|
+
emitEvent: (event) => listeners.event.forEach((l) => l(event)),
|
|
20
|
+
listenerCounts: () => ({
|
|
21
|
+
connected: listeners.connected.size,
|
|
22
|
+
disconnected: listeners.disconnected.size,
|
|
23
|
+
event: listeners.event.size,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
describe('observeMetroEvents', () => {
|
|
28
|
+
it('is a no-op and does not subscribe when diagnostics is disabled', () => {
|
|
29
|
+
const events = getEmitter();
|
|
30
|
+
const diagnostics = createDiagnostics({ enabled: false });
|
|
31
|
+
const dispose = observeMetroEvents(events, diagnostics);
|
|
32
|
+
events.emit({
|
|
33
|
+
type: 'bundle_build_started',
|
|
34
|
+
buildID: '1',
|
|
35
|
+
bundleDetails: {
|
|
36
|
+
bundleType: 'bundle',
|
|
37
|
+
dev: true,
|
|
38
|
+
entryFile: 'index.js',
|
|
39
|
+
minify: false,
|
|
40
|
+
platform: 'ios',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(diagnostics.flush()).toEqual([]);
|
|
44
|
+
dispose();
|
|
45
|
+
});
|
|
46
|
+
it('correlates bundle_build_started/done via buildID into a span', () => {
|
|
47
|
+
const events = getEmitter();
|
|
48
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
49
|
+
observeMetroEvents(events, diagnostics);
|
|
50
|
+
events.emit({
|
|
51
|
+
type: 'bundle_build_started',
|
|
52
|
+
buildID: 'build-1',
|
|
53
|
+
bundleDetails: {
|
|
54
|
+
bundleType: 'bundle',
|
|
55
|
+
dev: true,
|
|
56
|
+
entryFile: 'index.js',
|
|
57
|
+
minify: false,
|
|
58
|
+
platform: 'ios',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
events.emit({ type: 'bundle_build_done', buildID: 'build-1' });
|
|
62
|
+
const entries = diagnostics.flush();
|
|
63
|
+
expect(entries).toHaveLength(1);
|
|
64
|
+
expect(entries[0]?.label).toBe('metro.bundle.build');
|
|
65
|
+
expect(entries[0]?.status).toBe('ok');
|
|
66
|
+
expect(entries[0]?.attrs).toMatchObject({
|
|
67
|
+
buildID: 'build-1',
|
|
68
|
+
platform: 'ios',
|
|
69
|
+
entryFile: 'index.js',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it('records a failed span for bundle_build_failed', () => {
|
|
73
|
+
const events = getEmitter();
|
|
74
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
75
|
+
observeMetroEvents(events, diagnostics);
|
|
76
|
+
events.emit({
|
|
77
|
+
type: 'bundle_build_started',
|
|
78
|
+
buildID: 'build-2',
|
|
79
|
+
bundleDetails: {
|
|
80
|
+
bundleType: 'bundle',
|
|
81
|
+
dev: true,
|
|
82
|
+
entryFile: 'index.js',
|
|
83
|
+
minify: false,
|
|
84
|
+
platform: 'android',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
events.emit({ type: 'bundle_build_failed', buildID: 'build-2' });
|
|
88
|
+
const entries = diagnostics.flush();
|
|
89
|
+
expect(entries).toHaveLength(1);
|
|
90
|
+
expect(entries[0]?.status).toBe('error');
|
|
91
|
+
});
|
|
92
|
+
it('records a bundle_request_observed mark', () => {
|
|
93
|
+
const events = getEmitter();
|
|
94
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
95
|
+
observeMetroEvents(events, diagnostics);
|
|
96
|
+
events.emit({
|
|
97
|
+
type: 'bundle_request_observed',
|
|
98
|
+
platform: 'ios',
|
|
99
|
+
requestKind: 'app',
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
url: '/index.bundle',
|
|
102
|
+
});
|
|
103
|
+
const entries = diagnostics.flush();
|
|
104
|
+
expect(entries).toHaveLength(1);
|
|
105
|
+
expect(entries[0]?.label).toBe('metro.bundle.request');
|
|
106
|
+
expect(entries[0]?.kind).toBe('mark');
|
|
107
|
+
expect(entries[0]?.attrs).toEqual({ requestKind: 'app', platform: 'ios' });
|
|
108
|
+
});
|
|
109
|
+
it('dispose() unsubscribes the listener', () => {
|
|
110
|
+
const events = getEmitter();
|
|
111
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
112
|
+
const dispose = observeMetroEvents(events, diagnostics);
|
|
113
|
+
dispose();
|
|
114
|
+
events.emit({
|
|
115
|
+
type: 'bundle_request_observed',
|
|
116
|
+
platform: 'ios',
|
|
117
|
+
requestKind: 'app',
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
url: '/index.bundle',
|
|
120
|
+
});
|
|
121
|
+
expect(diagnostics.flush()).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('observeBridgeEvents', () => {
|
|
125
|
+
it('is a no-op and does not subscribe when diagnostics is disabled', () => {
|
|
126
|
+
const bridge = createFakeBridge();
|
|
127
|
+
const diagnostics = createDiagnostics({ enabled: false });
|
|
128
|
+
observeBridgeEvents(bridge, diagnostics);
|
|
129
|
+
expect(bridge.listenerCounts()).toEqual({ connected: 0, disconnected: 0, event: 0 });
|
|
130
|
+
});
|
|
131
|
+
it('marks bridge.connected and bridge.disconnected', () => {
|
|
132
|
+
const bridge = createFakeBridge();
|
|
133
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
134
|
+
observeBridgeEvents(bridge, diagnostics);
|
|
135
|
+
bridge.emitConnected();
|
|
136
|
+
bridge.emitDisconnected();
|
|
137
|
+
const entries = diagnostics.flush();
|
|
138
|
+
expect(entries.map((e) => e.label)).toEqual(['bridge.connected', 'bridge.disconnected']);
|
|
139
|
+
expect(entries.every((e) => e.kind === 'mark')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
it('records client.bundle.module for module bundling events', () => {
|
|
142
|
+
const bridge = createFakeBridge();
|
|
143
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
144
|
+
observeBridgeEvents(bridge, diagnostics);
|
|
145
|
+
bridge.emitEvent({ type: 'module-bundling-finished', file: 'a.ts', duration: 12 });
|
|
146
|
+
bridge.emitEvent({
|
|
147
|
+
type: 'module-bundling-failed',
|
|
148
|
+
file: 'b.ts',
|
|
149
|
+
duration: 5,
|
|
150
|
+
error: 'boom',
|
|
151
|
+
});
|
|
152
|
+
const entries = diagnostics.flush();
|
|
153
|
+
expect(entries).toHaveLength(2);
|
|
154
|
+
expect(entries[0]).toMatchObject({
|
|
155
|
+
label: 'client.bundle.module',
|
|
156
|
+
duration: 12,
|
|
157
|
+
status: 'ok',
|
|
158
|
+
attrs: { file: 'a.ts' },
|
|
159
|
+
});
|
|
160
|
+
expect(entries[1]).toMatchObject({
|
|
161
|
+
label: 'client.bundle.module',
|
|
162
|
+
duration: 5,
|
|
163
|
+
status: 'error',
|
|
164
|
+
attrs: { file: 'b.ts' },
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
it('records client.setup-file for setup file bundling events', () => {
|
|
168
|
+
const bridge = createFakeBridge();
|
|
169
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
170
|
+
observeBridgeEvents(bridge, diagnostics);
|
|
171
|
+
bridge.emitEvent({
|
|
172
|
+
type: 'setup-file-bundling-finished',
|
|
173
|
+
file: 'setup.ts',
|
|
174
|
+
setupType: 'setupFiles',
|
|
175
|
+
duration: 20,
|
|
176
|
+
});
|
|
177
|
+
const entries = diagnostics.flush();
|
|
178
|
+
expect(entries).toHaveLength(1);
|
|
179
|
+
expect(entries[0]).toMatchObject({
|
|
180
|
+
label: 'client.setup-file',
|
|
181
|
+
duration: 20,
|
|
182
|
+
attrs: { file: 'setup.ts', setupType: 'setupFiles' },
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
it('records client.collect for collection-finished', () => {
|
|
186
|
+
const bridge = createFakeBridge();
|
|
187
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
188
|
+
observeBridgeEvents(bridge, diagnostics);
|
|
189
|
+
bridge.emitEvent({
|
|
190
|
+
type: 'collection-finished',
|
|
191
|
+
file: 'a.test.ts',
|
|
192
|
+
duration: 8,
|
|
193
|
+
totalTests: 3,
|
|
194
|
+
});
|
|
195
|
+
const entries = diagnostics.flush();
|
|
196
|
+
expect(entries[0]).toMatchObject({
|
|
197
|
+
label: 'client.collect',
|
|
198
|
+
duration: 8,
|
|
199
|
+
attrs: { file: 'a.test.ts', totalTests: 3 },
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('records client.suite and client.test with status', () => {
|
|
203
|
+
const bridge = createFakeBridge();
|
|
204
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
205
|
+
observeBridgeEvents(bridge, diagnostics);
|
|
206
|
+
bridge.emitEvent({
|
|
207
|
+
type: 'suite-finished',
|
|
208
|
+
file: 'a.test.ts',
|
|
209
|
+
name: 'my suite',
|
|
210
|
+
duration: 100,
|
|
211
|
+
status: 'failed',
|
|
212
|
+
});
|
|
213
|
+
bridge.emitEvent({
|
|
214
|
+
type: 'test-finished',
|
|
215
|
+
file: 'a.test.ts',
|
|
216
|
+
suite: 'my suite',
|
|
217
|
+
name: 'my test',
|
|
218
|
+
ancestorTitles: [],
|
|
219
|
+
fullName: 'my suite my test',
|
|
220
|
+
startedAt: Date.now(),
|
|
221
|
+
duration: 15,
|
|
222
|
+
status: 'passed',
|
|
223
|
+
});
|
|
224
|
+
const entries = diagnostics.flush();
|
|
225
|
+
expect(entries[0]).toMatchObject({ label: 'client.suite', status: 'error' });
|
|
226
|
+
expect(entries[1]).toMatchObject({ label: 'client.test', status: 'ok' });
|
|
227
|
+
});
|
|
228
|
+
it('anchors recorded entries near "now - duration" using the host clock', () => {
|
|
229
|
+
const bridge = createFakeBridge();
|
|
230
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
231
|
+
observeBridgeEvents(bridge, diagnostics);
|
|
232
|
+
const before = Date.now();
|
|
233
|
+
bridge.emitEvent({ type: 'module-bundling-finished', file: 'a.ts', duration: 50 });
|
|
234
|
+
const after = Date.now();
|
|
235
|
+
const [entry] = diagnostics.flush();
|
|
236
|
+
expect(entry?.startTime).toBeGreaterThanOrEqual(before - 50 - 5);
|
|
237
|
+
expect(entry?.startTime).toBeLessThanOrEqual(after - 50 + 5);
|
|
238
|
+
});
|
|
239
|
+
it('dispose() unsubscribes all listeners', () => {
|
|
240
|
+
const bridge = createFakeBridge();
|
|
241
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
242
|
+
const dispose = observeBridgeEvents(bridge, diagnostics);
|
|
243
|
+
dispose();
|
|
244
|
+
expect(bridge.listenerCounts()).toEqual({ connected: 0, disconnected: 0, event: 0 });
|
|
245
|
+
bridge.emitConnected();
|
|
246
|
+
expect(diagnostics.flush()).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.test.d.ts","sourceRoot":"","sources":["../../../src/diagnostics/__tests__/report.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { printSummary, writeTraceFile } from '../report.js';
|
|
6
|
+
const makeSpanEntry = (overrides = {}) => ({
|
|
7
|
+
label: 'metro.bundle.build',
|
|
8
|
+
startTime: 1_000,
|
|
9
|
+
duration: 250,
|
|
10
|
+
status: 'ok',
|
|
11
|
+
kind: 'span',
|
|
12
|
+
...overrides,
|
|
13
|
+
});
|
|
14
|
+
describe('writeTraceFile', () => {
|
|
15
|
+
let projectRoot;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'react-native-harness-report-test-'));
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(projectRoot, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
it('writes a Chrome Trace Event JSON file under .harness/diagnostics', async () => {
|
|
23
|
+
const entries = [
|
|
24
|
+
makeSpanEntry({ attrs: { file: 'a.test.ts' } }),
|
|
25
|
+
makeSpanEntry({
|
|
26
|
+
label: 'bridge.connected',
|
|
27
|
+
duration: 0,
|
|
28
|
+
kind: 'mark',
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
const filePath = await writeTraceFile(entries, {
|
|
32
|
+
projectRoot,
|
|
33
|
+
runId: 'run-123',
|
|
34
|
+
});
|
|
35
|
+
expect(filePath).toBe(path.join(projectRoot, '.harness', 'diagnostics', 'run-123.json'));
|
|
36
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
expect(parsed.traceEvents).toHaveLength(2);
|
|
39
|
+
expect(parsed.traceEvents[0]).toMatchObject({
|
|
40
|
+
name: 'metro.bundle.build',
|
|
41
|
+
ph: 'X',
|
|
42
|
+
ts: 1_000 * 1000,
|
|
43
|
+
dur: 250 * 1000,
|
|
44
|
+
pid: 1,
|
|
45
|
+
tid: 1,
|
|
46
|
+
args: { file: 'a.test.ts' },
|
|
47
|
+
});
|
|
48
|
+
expect(parsed.traceEvents[1]).toMatchObject({
|
|
49
|
+
name: 'bridge.connected',
|
|
50
|
+
ph: 'i',
|
|
51
|
+
s: 'g',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it('creates the diagnostics directory recursively when missing', async () => {
|
|
55
|
+
const filePath = await writeTraceFile([makeSpanEntry()], {
|
|
56
|
+
projectRoot,
|
|
57
|
+
runId: 'run-456',
|
|
58
|
+
});
|
|
59
|
+
const stat = await fs.stat(filePath);
|
|
60
|
+
expect(stat.isFile()).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('printSummary', () => {
|
|
64
|
+
it('does nothing when there are no entries', () => {
|
|
65
|
+
const lines = [];
|
|
66
|
+
printSummary([], (line) => lines.push(line));
|
|
67
|
+
expect(lines).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
it('groups by top-level namespace and sorts labels by total duration desc', () => {
|
|
70
|
+
const lines = [];
|
|
71
|
+
printSummary([
|
|
72
|
+
makeSpanEntry({ label: 'metro.bundle.build', duration: 100 }),
|
|
73
|
+
makeSpanEntry({ label: 'metro.bundle.build', duration: 200 }),
|
|
74
|
+
makeSpanEntry({ label: 'metro.init', duration: 50 }),
|
|
75
|
+
makeSpanEntry({ label: 'client.test', duration: 5000 }),
|
|
76
|
+
], (line) => lines.push(line));
|
|
77
|
+
const metroHeaderIndex = lines.indexOf(' metro');
|
|
78
|
+
const clientHeaderIndex = lines.indexOf(' client');
|
|
79
|
+
expect(metroHeaderIndex).toBeGreaterThanOrEqual(0);
|
|
80
|
+
expect(clientHeaderIndex).toBeGreaterThanOrEqual(0);
|
|
81
|
+
const bundleLine = lines.find((l) => l.includes('metro.bundle.build'));
|
|
82
|
+
expect(bundleLine).toContain('count=2');
|
|
83
|
+
expect(bundleLine).toContain('total=300ms');
|
|
84
|
+
expect(bundleLine).toContain('max=200ms');
|
|
85
|
+
const clientLine = lines.find((l) => l.includes('client.test'));
|
|
86
|
+
expect(clientLine).toContain('total=5.0s');
|
|
87
|
+
});
|
|
88
|
+
it('formats sub-second durations in ms and second+ durations in s', () => {
|
|
89
|
+
const lines = [];
|
|
90
|
+
printSummary([
|
|
91
|
+
makeSpanEntry({ label: 'a.b', duration: 999 }),
|
|
92
|
+
makeSpanEntry({ label: 'a.c', duration: 1500 }),
|
|
93
|
+
], (line) => lines.push(line));
|
|
94
|
+
expect(lines.find((l) => l.includes('a.b'))).toContain('999ms');
|
|
95
|
+
expect(lines.find((l) => l.includes('a.c'))).toContain('1.5s');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Diagnostics } from '@react-native-harness/tools';
|
|
2
|
+
import type { HarnessBridge } from '@react-native-harness/bridge/server';
|
|
3
|
+
import type { MetroInstance } from '@react-native-harness/bundler-metro';
|
|
4
|
+
/**
|
|
5
|
+
* Observes Metro's reporter event stream and derives:
|
|
6
|
+
* - `metro.bundle.build` spans, correlated by `buildID` across
|
|
7
|
+
* bundle_build_started -> bundle_build_done|bundle_build_failed.
|
|
8
|
+
* - `metro.bundle.request` marks from bundle_request_observed events.
|
|
9
|
+
*/
|
|
10
|
+
export declare const observeMetroEvents: (events: MetroInstance["events"], diagnostics: Diagnostics) => (() => void);
|
|
11
|
+
type MinimalBridge = Pick<HarnessBridge, 'on' | 'off'>;
|
|
12
|
+
/**
|
|
13
|
+
* Observes bridge connection lifecycle and device-reported bridge events and
|
|
14
|
+
* derives:
|
|
15
|
+
* - `bridge.connected` / `bridge.disconnected` marks.
|
|
16
|
+
* - `client.bundle.module`, `client.setup-file`, `client.collect`,
|
|
17
|
+
* `client.suite`, `client.test` completed spans, recorded via
|
|
18
|
+
* diagnostics.record() since these events arrive with a `duration` already
|
|
19
|
+
* measured on the (unsynced) device clock; we anchor them at host receipt
|
|
20
|
+
* time instead of trusting the device's absolute clock.
|
|
21
|
+
*/
|
|
22
|
+
export declare const observeBridgeEvents: (bridge: MinimalBridge, diagnostics: Diagnostics) => (() => void);
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=derivers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derivers.d.ts","sourceRoot":"","sources":["../../src/diagnostics/derivers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,6BAA6B,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,qCAAqC,CAAC;AAW1F;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,aAAa,CAAC,QAAQ,CAAC,EAC/B,aAAa,WAAW,KACvB,CAAC,MAAM,IAAI,CA+Cb,CAAC;AAEF,KAAK,aAAa,GAAG,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC;AAEvD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,aAAa,EACrB,aAAa,WAAW,KACvB,CAAC,MAAM,IAAI,CAkFb,CAAC"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span/mark derivers translate existing domain event streams (Metro build
|
|
3
|
+
* events, bridge/device events) into diagnostics entries. This is the only
|
|
4
|
+
* place in the jest package that maps event shapes to diagnostic labels -
|
|
5
|
+
* business logic elsewhere stays free of diagnostics calls.
|
|
6
|
+
*/
|
|
7
|
+
const noopDispose = () => undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Observes Metro's reporter event stream and derives:
|
|
10
|
+
* - `metro.bundle.build` spans, correlated by `buildID` across
|
|
11
|
+
* bundle_build_started -> bundle_build_done|bundle_build_failed.
|
|
12
|
+
* - `metro.bundle.request` marks from bundle_request_observed events.
|
|
13
|
+
*/
|
|
14
|
+
export const observeMetroEvents = (events, diagnostics) => {
|
|
15
|
+
if (!diagnostics.enabled) {
|
|
16
|
+
return noopDispose;
|
|
17
|
+
}
|
|
18
|
+
const inflightBuilds = new Map();
|
|
19
|
+
const listener = (event) => {
|
|
20
|
+
switch (event.type) {
|
|
21
|
+
case 'bundle_build_started': {
|
|
22
|
+
const span = diagnostics.start('metro.bundle.build', {
|
|
23
|
+
buildID: event.buildID,
|
|
24
|
+
platform: event.bundleDetails.platform,
|
|
25
|
+
entryFile: event.bundleDetails.entryFile,
|
|
26
|
+
dev: event.bundleDetails.dev,
|
|
27
|
+
});
|
|
28
|
+
inflightBuilds.set(event.buildID, span);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
case 'bundle_build_done': {
|
|
32
|
+
const span = inflightBuilds.get(event.buildID);
|
|
33
|
+
inflightBuilds.delete(event.buildID);
|
|
34
|
+
span?.end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
case 'bundle_build_failed': {
|
|
38
|
+
const span = inflightBuilds.get(event.buildID);
|
|
39
|
+
inflightBuilds.delete(event.buildID);
|
|
40
|
+
span?.fail(new Error(`Metro bundle build ${event.buildID} failed`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
case 'bundle_request_observed': {
|
|
44
|
+
diagnostics.mark('metro.bundle.request', {
|
|
45
|
+
requestKind: event.requestKind,
|
|
46
|
+
platform: event.platform,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
events.addListener(listener);
|
|
55
|
+
return () => {
|
|
56
|
+
events.removeListener(listener);
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Observes bridge connection lifecycle and device-reported bridge events and
|
|
61
|
+
* derives:
|
|
62
|
+
* - `bridge.connected` / `bridge.disconnected` marks.
|
|
63
|
+
* - `client.bundle.module`, `client.setup-file`, `client.collect`,
|
|
64
|
+
* `client.suite`, `client.test` completed spans, recorded via
|
|
65
|
+
* diagnostics.record() since these events arrive with a `duration` already
|
|
66
|
+
* measured on the (unsynced) device clock; we anchor them at host receipt
|
|
67
|
+
* time instead of trusting the device's absolute clock.
|
|
68
|
+
*/
|
|
69
|
+
export const observeBridgeEvents = (bridge, diagnostics) => {
|
|
70
|
+
if (!diagnostics.enabled) {
|
|
71
|
+
return noopDispose;
|
|
72
|
+
}
|
|
73
|
+
const onConnected = () => {
|
|
74
|
+
diagnostics.mark('bridge.connected');
|
|
75
|
+
};
|
|
76
|
+
const onDisconnected = () => {
|
|
77
|
+
diagnostics.mark('bridge.disconnected');
|
|
78
|
+
};
|
|
79
|
+
const onEvent = (event) => {
|
|
80
|
+
switch (event.type) {
|
|
81
|
+
case 'module-bundling-finished':
|
|
82
|
+
diagnostics.record({
|
|
83
|
+
label: 'client.bundle.module',
|
|
84
|
+
duration: event.duration,
|
|
85
|
+
attrs: { file: event.file },
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
case 'module-bundling-failed':
|
|
89
|
+
diagnostics.record({
|
|
90
|
+
label: 'client.bundle.module',
|
|
91
|
+
duration: event.duration,
|
|
92
|
+
attrs: { file: event.file },
|
|
93
|
+
status: 'error',
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
case 'setup-file-bundling-finished':
|
|
97
|
+
diagnostics.record({
|
|
98
|
+
label: 'client.setup-file',
|
|
99
|
+
duration: event.duration,
|
|
100
|
+
attrs: { file: event.file, setupType: event.setupType },
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
case 'setup-file-bundling-failed':
|
|
104
|
+
diagnostics.record({
|
|
105
|
+
label: 'client.setup-file',
|
|
106
|
+
duration: event.duration,
|
|
107
|
+
attrs: { file: event.file, setupType: event.setupType },
|
|
108
|
+
status: 'error',
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
case 'collection-finished':
|
|
112
|
+
diagnostics.record({
|
|
113
|
+
label: 'client.collect',
|
|
114
|
+
duration: event.duration,
|
|
115
|
+
attrs: { file: event.file, totalTests: event.totalTests },
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
case 'suite-finished':
|
|
119
|
+
diagnostics.record({
|
|
120
|
+
label: 'client.suite',
|
|
121
|
+
duration: event.duration,
|
|
122
|
+
attrs: { file: event.file, name: event.name, status: event.status },
|
|
123
|
+
status: event.status === 'failed' ? 'error' : 'ok',
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
case 'test-finished':
|
|
127
|
+
diagnostics.record({
|
|
128
|
+
label: 'client.test',
|
|
129
|
+
duration: event.duration,
|
|
130
|
+
attrs: { file: event.file, name: event.name, status: event.status },
|
|
131
|
+
status: event.status === 'failed' ? 'error' : 'ok',
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
default:
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
bridge.on('connected', onConnected);
|
|
139
|
+
bridge.on('disconnected', onDisconnected);
|
|
140
|
+
bridge.on('event', onEvent);
|
|
141
|
+
return () => {
|
|
142
|
+
bridge.off('connected', onConnected);
|
|
143
|
+
bridge.off('disconnected', onDisconnected);
|
|
144
|
+
bridge.off('event', onEvent);
|
|
145
|
+
};
|
|
146
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/diagnostics/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|