@react-native-harness/jest 1.0.0-canary.1766225407244 → 1.0.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__/client-log-handler.test.d.ts +2 -0
- package/dist/__tests__/client-log-handler.test.d.ts.map +1 -0
- package/dist/__tests__/client-log-handler.test.js +262 -0
- package/dist/cli-args.test.d.ts +2 -0
- package/dist/cli-args.test.d.ts.map +1 -0
- package/dist/cli-args.test.js +1 -0
- package/dist/client-log-handler.d.ts +23 -0
- package/dist/client-log-handler.d.ts.map +1 -0
- package/dist/client-log-handler.js +84 -0
- package/dist/crash-monitor.d.ts +16 -0
- package/dist/crash-monitor.d.ts.map +1 -0
- package/dist/crash-monitor.js +75 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -0
- package/dist/event-mapper.d.ts +16 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +120 -0
- package/dist/global-setup.d.ts +3 -0
- package/dist/global-setup.d.ts.map +1 -0
- package/dist/global-setup.js +60 -0
- package/dist/global-teardown.d.ts +3 -0
- package/dist/global-teardown.d.ts.map +1 -0
- package/dist/global-teardown.js +11 -0
- package/dist/harness-reporter.d.ts +8 -0
- package/dist/harness-reporter.d.ts.map +1 -0
- package/dist/harness-reporter.js +12 -0
- package/dist/harness.d.ts +17 -3
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +114 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -8
- package/dist/jest-options.d.ts +2 -0
- package/dist/jest-options.d.ts.map +1 -0
- package/dist/jest-options.js +1 -0
- package/dist/logs.d.ts +1 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +3 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +6 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/eslint.config.mjs +5 -2
- package/package.json +6 -7
- package/src/__tests__/client-log-handler.test.ts +328 -0
- package/src/client-log-handler.ts +101 -0
- package/src/crash-monitor.ts +109 -0
- package/src/errors.ts +20 -0
- package/src/harness.ts +173 -29
- package/src/index.ts +46 -10
- package/src/logs.ts +6 -0
- package/src/run.ts +1 -0
- package/src/setup.ts +8 -0
- package/tsconfig.json +3 -3
- package/vite.config.ts +18 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-log-handler.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/client-log-handler.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { formatClientLogMessage, formatClientLogLine, handleClientLogEvent, createClientLogListener, } from '../client-log-handler.js';
|
|
3
|
+
// Mock the log function
|
|
4
|
+
vi.mock('../logs.js', () => ({
|
|
5
|
+
log: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
import { log } from '../logs.js';
|
|
8
|
+
describe('client-log-handler', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
describe('formatClientLogMessage', () => {
|
|
13
|
+
it('should format a single string', () => {
|
|
14
|
+
const result = formatClientLogMessage(['Hello, world!']);
|
|
15
|
+
expect(result).toBe('Hello, world!');
|
|
16
|
+
});
|
|
17
|
+
it('should join multiple strings with spaces', () => {
|
|
18
|
+
const result = formatClientLogMessage(['Hello', 'world', '!']);
|
|
19
|
+
expect(result).toBe('Hello world !');
|
|
20
|
+
});
|
|
21
|
+
it('should format objects', () => {
|
|
22
|
+
const result = formatClientLogMessage([{ key: 'value' }]);
|
|
23
|
+
// util.format uses inspect-style output for objects
|
|
24
|
+
expect(result).toContain('key');
|
|
25
|
+
expect(result).toContain('value');
|
|
26
|
+
});
|
|
27
|
+
it('should format arrays', () => {
|
|
28
|
+
const result = formatClientLogMessage([[1, 2, 3]]);
|
|
29
|
+
expect(result).toContain('1');
|
|
30
|
+
expect(result).toContain('2');
|
|
31
|
+
expect(result).toContain('3');
|
|
32
|
+
});
|
|
33
|
+
it('should handle mixed types', () => {
|
|
34
|
+
const result = formatClientLogMessage([
|
|
35
|
+
'Message:',
|
|
36
|
+
{ count: 42 },
|
|
37
|
+
'items',
|
|
38
|
+
]);
|
|
39
|
+
expect(result).toContain('Message:');
|
|
40
|
+
expect(result).toContain('count');
|
|
41
|
+
expect(result).toContain('42');
|
|
42
|
+
expect(result).toContain('items');
|
|
43
|
+
});
|
|
44
|
+
it('should format numbers', () => {
|
|
45
|
+
const result = formatClientLogMessage([123, 456]);
|
|
46
|
+
expect(result).toBe('123 456');
|
|
47
|
+
});
|
|
48
|
+
it('should format booleans', () => {
|
|
49
|
+
const result = formatClientLogMessage([true, false]);
|
|
50
|
+
expect(result).toBe('true false');
|
|
51
|
+
});
|
|
52
|
+
it('should format null and undefined', () => {
|
|
53
|
+
const result = formatClientLogMessage([null, undefined]);
|
|
54
|
+
expect(result).toBe('null undefined');
|
|
55
|
+
});
|
|
56
|
+
it('should handle empty array', () => {
|
|
57
|
+
const result = formatClientLogMessage([]);
|
|
58
|
+
expect(result).toBe('');
|
|
59
|
+
});
|
|
60
|
+
describe('printf-style format specifiers', () => {
|
|
61
|
+
it('should handle %s string substitution', () => {
|
|
62
|
+
const result = formatClientLogMessage(['%s world', 'hello']);
|
|
63
|
+
expect(result).toBe('hello world');
|
|
64
|
+
});
|
|
65
|
+
it('should handle %d integer substitution', () => {
|
|
66
|
+
const result = formatClientLogMessage(['Count: %d', 42]);
|
|
67
|
+
expect(result).toBe('Count: 42');
|
|
68
|
+
});
|
|
69
|
+
it('should handle %i integer substitution', () => {
|
|
70
|
+
const result = formatClientLogMessage(['Value: %i', 123]);
|
|
71
|
+
expect(result).toBe('Value: 123');
|
|
72
|
+
});
|
|
73
|
+
it('should handle %f float substitution', () => {
|
|
74
|
+
const result = formatClientLogMessage(['Pi: %f', 3.14159]);
|
|
75
|
+
expect(result).toContain('3.14159');
|
|
76
|
+
});
|
|
77
|
+
it('should handle multiple substitutions', () => {
|
|
78
|
+
const result = formatClientLogMessage(['Hello %s, you have %d messages', 'Alice', 5]);
|
|
79
|
+
expect(result).toBe('Hello Alice, you have 5 messages');
|
|
80
|
+
});
|
|
81
|
+
it('should handle %j JSON substitution', () => {
|
|
82
|
+
const result = formatClientLogMessage(['Data: %j', { key: 'value' }]);
|
|
83
|
+
expect(result).toBe('Data: {"key":"value"}');
|
|
84
|
+
});
|
|
85
|
+
it('should handle %o object substitution', () => {
|
|
86
|
+
const result = formatClientLogMessage(['Object: %o', { a: 1 }]);
|
|
87
|
+
// %o produces inspect-style output, just check it contains the key
|
|
88
|
+
expect(result).toContain('a');
|
|
89
|
+
});
|
|
90
|
+
it('should handle %% as literal percent when substituting', () => {
|
|
91
|
+
// %% is only converted to % when there are substitutions
|
|
92
|
+
const result = formatClientLogMessage(['%s is 100%% complete', 'Task']);
|
|
93
|
+
expect(result).toBe('Task is 100% complete');
|
|
94
|
+
});
|
|
95
|
+
it('should append extra arguments after substitution', () => {
|
|
96
|
+
const result = formatClientLogMessage(['Hello %s', 'world', 'extra', 'args']);
|
|
97
|
+
expect(result).toBe('Hello world extra args');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('formatClientLogLine', () => {
|
|
102
|
+
it('should format log level event', () => {
|
|
103
|
+
const event = {
|
|
104
|
+
type: 'client_log',
|
|
105
|
+
level: 'log',
|
|
106
|
+
data: ['Test message'],
|
|
107
|
+
};
|
|
108
|
+
const result = formatClientLogLine(event);
|
|
109
|
+
// The result will contain ANSI codes for styling, so we check it contains the message
|
|
110
|
+
expect(result).toContain('Test message');
|
|
111
|
+
});
|
|
112
|
+
it('should format error level event', () => {
|
|
113
|
+
const event = {
|
|
114
|
+
type: 'client_log',
|
|
115
|
+
level: 'error',
|
|
116
|
+
data: ['Error occurred'],
|
|
117
|
+
};
|
|
118
|
+
const result = formatClientLogLine(event);
|
|
119
|
+
expect(result).toContain('Error occurred');
|
|
120
|
+
});
|
|
121
|
+
it('should format warn level event', () => {
|
|
122
|
+
const event = {
|
|
123
|
+
type: 'client_log',
|
|
124
|
+
level: 'warn',
|
|
125
|
+
data: ['Warning message'],
|
|
126
|
+
};
|
|
127
|
+
const result = formatClientLogLine(event);
|
|
128
|
+
expect(result).toContain('Warning message');
|
|
129
|
+
});
|
|
130
|
+
it('should format info level event', () => {
|
|
131
|
+
const event = {
|
|
132
|
+
type: 'client_log',
|
|
133
|
+
level: 'info',
|
|
134
|
+
data: ['Info message'],
|
|
135
|
+
};
|
|
136
|
+
const result = formatClientLogLine(event);
|
|
137
|
+
expect(result).toContain('Info message');
|
|
138
|
+
});
|
|
139
|
+
it('should format debug level event', () => {
|
|
140
|
+
const event = {
|
|
141
|
+
type: 'client_log',
|
|
142
|
+
level: 'debug',
|
|
143
|
+
data: ['Debug message'],
|
|
144
|
+
};
|
|
145
|
+
const result = formatClientLogLine(event);
|
|
146
|
+
expect(result).toContain('Debug message');
|
|
147
|
+
});
|
|
148
|
+
it('should format trace level event', () => {
|
|
149
|
+
const event = {
|
|
150
|
+
type: 'client_log',
|
|
151
|
+
level: 'trace',
|
|
152
|
+
data: ['Trace message'],
|
|
153
|
+
};
|
|
154
|
+
const result = formatClientLogLine(event);
|
|
155
|
+
expect(result).toContain('Trace message');
|
|
156
|
+
});
|
|
157
|
+
it('should handle multiple data items', () => {
|
|
158
|
+
const event = {
|
|
159
|
+
type: 'client_log',
|
|
160
|
+
level: 'log',
|
|
161
|
+
data: ['User:', { id: 1, name: 'Test' }],
|
|
162
|
+
};
|
|
163
|
+
const result = formatClientLogLine(event);
|
|
164
|
+
expect(result).toContain('User:');
|
|
165
|
+
expect(result).toContain('id');
|
|
166
|
+
expect(result).toContain('Test');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('handleClientLogEvent', () => {
|
|
170
|
+
it('should handle client_log events and call log', () => {
|
|
171
|
+
const event = {
|
|
172
|
+
type: 'client_log',
|
|
173
|
+
level: 'log',
|
|
174
|
+
data: ['Test message'],
|
|
175
|
+
};
|
|
176
|
+
const result = handleClientLogEvent(event);
|
|
177
|
+
expect(result).toBe(true);
|
|
178
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining('Test message'));
|
|
180
|
+
});
|
|
181
|
+
it('should return false for non-client_log events', () => {
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
183
|
+
const event = { type: 'bundle_build_started' };
|
|
184
|
+
const result = handleClientLogEvent(event);
|
|
185
|
+
expect(result).toBe(false);
|
|
186
|
+
expect(log).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
it('should handle error level logs', () => {
|
|
189
|
+
const event = {
|
|
190
|
+
type: 'client_log',
|
|
191
|
+
level: 'error',
|
|
192
|
+
data: ['Something went wrong'],
|
|
193
|
+
};
|
|
194
|
+
handleClientLogEvent(event);
|
|
195
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining('Something went wrong'));
|
|
196
|
+
});
|
|
197
|
+
it('should handle warn level logs', () => {
|
|
198
|
+
const event = {
|
|
199
|
+
type: 'client_log',
|
|
200
|
+
level: 'warn',
|
|
201
|
+
data: ['Deprecation warning'],
|
|
202
|
+
};
|
|
203
|
+
handleClientLogEvent(event);
|
|
204
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining('Deprecation warning'));
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('createClientLogListener', () => {
|
|
208
|
+
it('should create a listener function', () => {
|
|
209
|
+
const listener = createClientLogListener();
|
|
210
|
+
expect(typeof listener).toBe('function');
|
|
211
|
+
});
|
|
212
|
+
it('should handle client_log events when called', () => {
|
|
213
|
+
const listener = createClientLogListener();
|
|
214
|
+
const event = {
|
|
215
|
+
type: 'client_log',
|
|
216
|
+
level: 'info',
|
|
217
|
+
data: ['Listener test'],
|
|
218
|
+
};
|
|
219
|
+
listener(event);
|
|
220
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining('Listener test'));
|
|
221
|
+
});
|
|
222
|
+
it('should ignore non-client_log events', () => {
|
|
223
|
+
const listener = createClientLogListener();
|
|
224
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
225
|
+
const event = { type: 'initialize_done' };
|
|
226
|
+
listener(event);
|
|
227
|
+
expect(log).not.toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
describe('group events', () => {
|
|
231
|
+
it('should handle group events without logging', () => {
|
|
232
|
+
const event = {
|
|
233
|
+
type: 'client_log',
|
|
234
|
+
level: 'group',
|
|
235
|
+
data: ['Group label'],
|
|
236
|
+
};
|
|
237
|
+
const result = handleClientLogEvent(event);
|
|
238
|
+
expect(result).toBe(true);
|
|
239
|
+
expect(log).not.toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
it('should handle groupCollapsed events without logging', () => {
|
|
242
|
+
const event = {
|
|
243
|
+
type: 'client_log',
|
|
244
|
+
level: 'groupCollapsed',
|
|
245
|
+
data: ['Collapsed'],
|
|
246
|
+
};
|
|
247
|
+
const result = handleClientLogEvent(event);
|
|
248
|
+
expect(result).toBe(true);
|
|
249
|
+
expect(log).not.toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
it('should handle groupEnd events without logging', () => {
|
|
252
|
+
const event = {
|
|
253
|
+
type: 'client_log',
|
|
254
|
+
level: 'groupEnd',
|
|
255
|
+
data: [],
|
|
256
|
+
};
|
|
257
|
+
const result = handleClientLogEvent(event);
|
|
258
|
+
expect(result).toBe(true);
|
|
259
|
+
expect(log).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-args.test.d.ts","sourceRoot":"","sources":["../src/cli-args.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReportableEvent } from '@react-native-harness/bundler-metro';
|
|
2
|
+
export type ClientLogEvent = Extract<ReportableEvent, {
|
|
3
|
+
type: 'client_log';
|
|
4
|
+
}>;
|
|
5
|
+
/**
|
|
6
|
+
* Formats a client log event data array into a string message.
|
|
7
|
+
* Uses util.format for printf-style format specifier support (%s, %d, %j, etc.)
|
|
8
|
+
*/
|
|
9
|
+
export declare const formatClientLogMessage: (data: unknown[]) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Formats a client log event into a log line with styled level prefix.
|
|
12
|
+
*/
|
|
13
|
+
export declare const formatClientLogLine: (event: ClientLogEvent) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Handles a client_log event by formatting and logging it.
|
|
16
|
+
* Returns true if the event was handled, false otherwise.
|
|
17
|
+
*/
|
|
18
|
+
export declare const handleClientLogEvent: (event: ReportableEvent) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Creates a client log event listener that can be added to the Metro reporter.
|
|
21
|
+
*/
|
|
22
|
+
export declare const createClientLogListener: () => ((event: ReportableEvent) => void);
|
|
23
|
+
//# sourceMappingURL=client-log-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-log-handler.d.ts","sourceRoot":"","sources":["../src/client-log-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAK3E,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,eAAe,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AA6C9E;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,OAAO,EAAE,KAAG,MAKxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,cAAc,KAAG,MAI3D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,eAAe,KAAG,OAiB7D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,QAAO,CAAC,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAI3E,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import util from 'node:util';
|
|
3
|
+
import { log } from './logs.js';
|
|
4
|
+
/**
|
|
5
|
+
* Gets the display level for a log level.
|
|
6
|
+
* Note: Metro treats 'trace' as 'log' because Hermes doesn't include stack traces.
|
|
7
|
+
*/
|
|
8
|
+
const getDisplayLevel = (level) => {
|
|
9
|
+
// Metro converts trace to log for display (Hermes doesn't provide stack traces)
|
|
10
|
+
if (level === 'trace') {
|
|
11
|
+
return 'LOG';
|
|
12
|
+
}
|
|
13
|
+
return level.toUpperCase();
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates a styled tag for a log level with colored box appearance.
|
|
17
|
+
*/
|
|
18
|
+
const createLevelTag = (level) => {
|
|
19
|
+
const displayLevel = getDisplayLevel(level);
|
|
20
|
+
const label = ` ${displayLevel} `;
|
|
21
|
+
if (!chalk.supportsColor) {
|
|
22
|
+
return `[${displayLevel}]`;
|
|
23
|
+
}
|
|
24
|
+
switch (level) {
|
|
25
|
+
case 'error':
|
|
26
|
+
return chalk.reset.inverse.bold.red(label);
|
|
27
|
+
case 'warn':
|
|
28
|
+
return chalk.reset.inverse.bold.yellow(label);
|
|
29
|
+
case 'info':
|
|
30
|
+
return chalk.reset.inverse.bold.cyan(label);
|
|
31
|
+
case 'debug':
|
|
32
|
+
return chalk.reset.inverse.bold.blue(label);
|
|
33
|
+
case 'trace':
|
|
34
|
+
// Trace displays as LOG but with a distinct color
|
|
35
|
+
return chalk.reset.inverse.bold.magenta(label);
|
|
36
|
+
case 'log':
|
|
37
|
+
default:
|
|
38
|
+
return chalk.reset.inverse.bold.white(label);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Formats a client log event data array into a string message.
|
|
43
|
+
* Uses util.format for printf-style format specifier support (%s, %d, %j, etc.)
|
|
44
|
+
*/
|
|
45
|
+
export const formatClientLogMessage = (data) => {
|
|
46
|
+
if (data.length === 0) {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
return util.format(...data);
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Formats a client log event into a log line with styled level prefix.
|
|
53
|
+
*/
|
|
54
|
+
export const formatClientLogLine = (event) => {
|
|
55
|
+
const tag = createLevelTag(event.level);
|
|
56
|
+
const message = formatClientLogMessage(event.data);
|
|
57
|
+
return `${tag} ${message}`;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Handles a client_log event by formatting and logging it.
|
|
61
|
+
* Returns true if the event was handled, false otherwise.
|
|
62
|
+
*/
|
|
63
|
+
export const handleClientLogEvent = (event) => {
|
|
64
|
+
if (event.type !== 'client_log') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
// Skip group events - they don't produce output
|
|
68
|
+
if (event.level === 'group' ||
|
|
69
|
+
event.level === 'groupCollapsed' ||
|
|
70
|
+
event.level === 'groupEnd') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
const logLine = formatClientLogLine(event);
|
|
74
|
+
log(logLine);
|
|
75
|
+
return true;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Creates a client log event listener that can be added to the Metro reporter.
|
|
79
|
+
*/
|
|
80
|
+
export const createClientLogListener = () => {
|
|
81
|
+
return (event) => {
|
|
82
|
+
handleClientLogEvent(event);
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HarnessPlatformRunner } from '@react-native-harness/platforms';
|
|
2
|
+
import { BridgeServer } from '@react-native-harness/bridge/server';
|
|
3
|
+
export type CrashMonitor = {
|
|
4
|
+
startMonitoring(testFilePath: string): Promise<never>;
|
|
5
|
+
stopMonitoring(): void;
|
|
6
|
+
markIntentionalRestart(): void;
|
|
7
|
+
clearIntentionalRestart(): void;
|
|
8
|
+
dispose(): void;
|
|
9
|
+
};
|
|
10
|
+
export type CrashMonitorOptions = {
|
|
11
|
+
interval: number;
|
|
12
|
+
platformRunner: HarnessPlatformRunner;
|
|
13
|
+
bridgeServer: BridgeServer;
|
|
14
|
+
};
|
|
15
|
+
export declare const createCrashMonitor: ({ interval, platformRunner, bridgeServer, }: CrashMonitorOptions) => CrashMonitor;
|
|
16
|
+
//# sourceMappingURL=crash-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAInE,MAAM,MAAM,YAAY,GAAG;IACzB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACtD,cAAc,IAAI,IAAI,CAAC;IACvB,sBAAsB,IAAI,IAAI,CAAC;IAC/B,uBAAuB,IAAI,IAAI,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,qBAAqB,CAAC;IACtC,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,6CAIhC,mBAAmB,KAAG,YAqFxB,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NativeCrashError } from './errors.js';
|
|
2
|
+
import { logger } from '@react-native-harness/tools';
|
|
3
|
+
export const createCrashMonitor = ({ interval, platformRunner, bridgeServer, }) => {
|
|
4
|
+
let pollingInterval = null;
|
|
5
|
+
let isIntentionalRestart = false;
|
|
6
|
+
let currentTestFilePath = null;
|
|
7
|
+
let rejectFn = null;
|
|
8
|
+
const handleDisconnect = () => {
|
|
9
|
+
// Verify if it's actually a crash by checking if app is still running
|
|
10
|
+
if (!isIntentionalRestart && currentTestFilePath) {
|
|
11
|
+
// Capture the value to avoid it being null when setTimeout callback runs
|
|
12
|
+
const testFilePath = currentTestFilePath;
|
|
13
|
+
logger.debug('Bridge disconnected, checking if app crashed');
|
|
14
|
+
// Use a slight delay to allow the OS to clean up the process
|
|
15
|
+
setTimeout(async () => {
|
|
16
|
+
const isRunning = await platformRunner.isAppRunning();
|
|
17
|
+
if (!isRunning && !isIntentionalRestart && rejectFn) {
|
|
18
|
+
logger.debug(`Native crash detected during: ${testFilePath}`);
|
|
19
|
+
rejectFn(new NativeCrashError(testFilePath));
|
|
20
|
+
}
|
|
21
|
+
}, 100);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const startMonitoring = (testFilePath) => {
|
|
25
|
+
currentTestFilePath = testFilePath;
|
|
26
|
+
return new Promise((_, reject) => {
|
|
27
|
+
rejectFn = reject;
|
|
28
|
+
// Listen for bridge disconnect as early indicator
|
|
29
|
+
bridgeServer.on('disconnect', handleDisconnect);
|
|
30
|
+
// Poll for app running status
|
|
31
|
+
pollingInterval = setInterval(async () => {
|
|
32
|
+
// Skip check during intentional restarts
|
|
33
|
+
if (isIntentionalRestart) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const isRunning = await platformRunner.isAppRunning();
|
|
38
|
+
if (!isRunning && currentTestFilePath) {
|
|
39
|
+
logger.debug(`Native crash detected during: ${currentTestFilePath}`);
|
|
40
|
+
stopMonitoring();
|
|
41
|
+
reject(new NativeCrashError(currentTestFilePath));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.error('Error checking app status:', error);
|
|
46
|
+
}
|
|
47
|
+
}, interval);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const stopMonitoring = () => {
|
|
51
|
+
if (pollingInterval) {
|
|
52
|
+
clearInterval(pollingInterval);
|
|
53
|
+
pollingInterval = null;
|
|
54
|
+
}
|
|
55
|
+
bridgeServer.off('disconnect', handleDisconnect);
|
|
56
|
+
currentTestFilePath = null;
|
|
57
|
+
rejectFn = null;
|
|
58
|
+
};
|
|
59
|
+
const markIntentionalRestart = () => {
|
|
60
|
+
isIntentionalRestart = true;
|
|
61
|
+
};
|
|
62
|
+
const clearIntentionalRestart = () => {
|
|
63
|
+
isIntentionalRestart = false;
|
|
64
|
+
};
|
|
65
|
+
const dispose = () => {
|
|
66
|
+
stopMonitoring();
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
startMonitoring,
|
|
70
|
+
stopMonitoring,
|
|
71
|
+
markIntentionalRestart,
|
|
72
|
+
clearIntentionalRestart,
|
|
73
|
+
dispose,
|
|
74
|
+
};
|
|
75
|
+
};
|
package/dist/errors.d.ts
CHANGED
|
@@ -9,4 +9,12 @@ export declare class RunnerNotFoundError extends HarnessError {
|
|
|
9
9
|
export declare class InitializationTimeoutError extends HarnessError {
|
|
10
10
|
constructor();
|
|
11
11
|
}
|
|
12
|
+
export declare class MaxAppRestartsError extends HarnessError {
|
|
13
|
+
constructor(attempts: number);
|
|
14
|
+
}
|
|
15
|
+
export declare class NativeCrashError extends HarnessError {
|
|
16
|
+
readonly testFilePath: string;
|
|
17
|
+
readonly lastKnownTest?: string | undefined;
|
|
18
|
+
constructor(testFilePath: string, lastKnownTest?: string | undefined);
|
|
19
|
+
}
|
|
12
20
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,qBAAa,mBAAoB,SAAQ,YAAY;gBACvC,QAAQ,EAAE,MAAM;CAO7B;AAED,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,aAAa,CAAC,EAAE,MAAM;gBADtB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,YAAA;CAKzC"}
|
package/dist/errors.js
CHANGED
|
@@ -19,3 +19,20 @@ export class InitializationTimeoutError extends HarnessError {
|
|
|
19
19
|
this.name = 'InitializationTimeoutError';
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
export class MaxAppRestartsError extends HarnessError {
|
|
23
|
+
constructor(attempts) {
|
|
24
|
+
super(`App failed to start after ${attempts} attempts. ` +
|
|
25
|
+
`No bundling activity detected within timeout period.`);
|
|
26
|
+
this.name = 'MaxAppRestartsError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class NativeCrashError extends HarnessError {
|
|
30
|
+
testFilePath;
|
|
31
|
+
lastKnownTest;
|
|
32
|
+
constructor(testFilePath, lastKnownTest) {
|
|
33
|
+
super('The native app crashed during test execution.');
|
|
34
|
+
this.testFilePath = testFilePath;
|
|
35
|
+
this.lastKnownTest = lastKnownTest;
|
|
36
|
+
this.name = 'NativeCrashError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TestEvents } from '@jest/test-result';
|
|
2
|
+
import type { Test } from 'jest-runner';
|
|
3
|
+
import type { BridgeEvents } from '@react-native-harness/bridge';
|
|
4
|
+
import type { TestCaseResult } from '@jest/test-result';
|
|
5
|
+
export type EventMapperState = {
|
|
6
|
+
currentTest?: Test;
|
|
7
|
+
suiteHierarchy: string[];
|
|
8
|
+
testResults: Map<string, TestCaseResult>;
|
|
9
|
+
fileStartTime: number;
|
|
10
|
+
};
|
|
11
|
+
export type HarnessEventMapper = {
|
|
12
|
+
handleBridgeEvent: (event: BridgeEvents, test: Test) => void;
|
|
13
|
+
reset: () => void;
|
|
14
|
+
};
|
|
15
|
+
export declare const getHarnessEventMapper: (emit: <T extends keyof TestEvents>(eventName: T, eventData: TestEvents[T]) => void) => HarnessEventMapper;
|
|
16
|
+
//# sourceMappingURL=event-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-mapper.d.ts","sourceRoot":"","sources":["../src/event-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EASV,YAAY,EACb,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iBAAiB,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,CAAC,CAAC,SAAS,MAAM,UAAU,EAC/B,SAAS,EAAE,CAAC,EACZ,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,KACrB,IAAI,KACR,kBA8JF,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const getHarnessEventMapper = (emit) => {
|
|
2
|
+
const state = {
|
|
3
|
+
suiteHierarchy: [],
|
|
4
|
+
testResults: new Map(),
|
|
5
|
+
fileStartTime: 0,
|
|
6
|
+
};
|
|
7
|
+
const handleBridgeEvent = (event, test) => {
|
|
8
|
+
// Filter to only process test runner events
|
|
9
|
+
if (event.type.startsWith('test-') ||
|
|
10
|
+
event.type.startsWith('suite-') ||
|
|
11
|
+
event.type.startsWith('file-')) {
|
|
12
|
+
switch (event.type) {
|
|
13
|
+
case 'file-started':
|
|
14
|
+
handleFileStarted(event, test);
|
|
15
|
+
break;
|
|
16
|
+
case 'file-finished':
|
|
17
|
+
handleFileFinished(event, test);
|
|
18
|
+
break;
|
|
19
|
+
case 'suite-started':
|
|
20
|
+
handleSuiteStarted(event);
|
|
21
|
+
break;
|
|
22
|
+
case 'suite-finished':
|
|
23
|
+
handleSuiteFinished(event);
|
|
24
|
+
break;
|
|
25
|
+
case 'test-started':
|
|
26
|
+
handleTestStarted(event, test);
|
|
27
|
+
break;
|
|
28
|
+
case 'test-finished':
|
|
29
|
+
handleTestFinished(event, test);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const handleFileStarted = (event, test) => {
|
|
35
|
+
state.currentTest = test;
|
|
36
|
+
state.fileStartTime = Date.now();
|
|
37
|
+
state.suiteHierarchy = [];
|
|
38
|
+
state.testResults.clear();
|
|
39
|
+
// Don't emit test-file-start here - that should be handled by the Jest runner
|
|
40
|
+
// when it actually starts running the test file
|
|
41
|
+
};
|
|
42
|
+
const handleFileFinished = (_event, _test) => {
|
|
43
|
+
// File finished - all test results should already be emitted
|
|
44
|
+
// The final TestResult will be handled by the runHarnessTestFile function
|
|
45
|
+
};
|
|
46
|
+
const handleSuiteStarted = (event) => {
|
|
47
|
+
state.suiteHierarchy.push(event.name);
|
|
48
|
+
};
|
|
49
|
+
const handleSuiteFinished = (event) => {
|
|
50
|
+
// Remove the suite from hierarchy when it finishes
|
|
51
|
+
const index = state.suiteHierarchy.lastIndexOf(event.name);
|
|
52
|
+
if (index !== -1) {
|
|
53
|
+
state.suiteHierarchy.splice(index, 1);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const handleTestStarted = (event, test) => {
|
|
57
|
+
const testCaseStartInfo = {
|
|
58
|
+
ancestorTitles: [...state.suiteHierarchy],
|
|
59
|
+
fullName: `${state.suiteHierarchy.join(' ')} ${event.name}`.trim(),
|
|
60
|
+
title: event.name,
|
|
61
|
+
mode: 'skip', // Default mode, will be updated based on test status
|
|
62
|
+
};
|
|
63
|
+
emit('test-case-start', [test.path, testCaseStartInfo]);
|
|
64
|
+
};
|
|
65
|
+
const handleTestFinished = (event, test) => {
|
|
66
|
+
const testCaseResult = {
|
|
67
|
+
ancestorTitles: [...state.suiteHierarchy],
|
|
68
|
+
duration: event.duration,
|
|
69
|
+
failureDetails: [],
|
|
70
|
+
failureMessages: event.error ? [formatError(event.error)] : [],
|
|
71
|
+
fullName: `${state.suiteHierarchy.join(' ')} ${event.name}`.trim(),
|
|
72
|
+
numPassingAsserts: event.status === 'passed' ? 1 : 0,
|
|
73
|
+
status: mapStatus(event.status),
|
|
74
|
+
title: event.name,
|
|
75
|
+
startedAt: Date.now() - event.duration,
|
|
76
|
+
};
|
|
77
|
+
state.testResults.set(event.name, testCaseResult);
|
|
78
|
+
emit('test-case-result', [test.path, testCaseResult]);
|
|
79
|
+
};
|
|
80
|
+
const mapStatus = (status) => {
|
|
81
|
+
return status;
|
|
82
|
+
};
|
|
83
|
+
const formatError = (error) => {
|
|
84
|
+
let errorMessage = `${error.name}: ${error.message}`;
|
|
85
|
+
if (error.codeFrame) {
|
|
86
|
+
errorMessage += '\n\n' + formatCodeFrame(error.codeFrame);
|
|
87
|
+
}
|
|
88
|
+
return errorMessage;
|
|
89
|
+
};
|
|
90
|
+
const formatCodeFrame = (codeFrame) => {
|
|
91
|
+
const lines = codeFrame.content.split('\n');
|
|
92
|
+
const lineNumber = codeFrame.location?.row || 1;
|
|
93
|
+
const column = codeFrame.location?.column || 1;
|
|
94
|
+
let formatted = `\n${codeFrame.fileName}:${lineNumber}:${column}\n`;
|
|
95
|
+
// Add context lines around the error
|
|
96
|
+
const startLine = Math.max(0, lineNumber - 3);
|
|
97
|
+
const endLine = Math.min(lines.length, lineNumber + 2);
|
|
98
|
+
for (let i = startLine; i < endLine; i++) {
|
|
99
|
+
const lineNum = i + 1;
|
|
100
|
+
const line = lines[i];
|
|
101
|
+
const isErrorLine = lineNum === lineNumber;
|
|
102
|
+
formatted += `${lineNum.toString().padStart(3)} | ${line}\n`;
|
|
103
|
+
if (isErrorLine && column > 0) {
|
|
104
|
+
const pointer = ' '.repeat(column + 5) + '^';
|
|
105
|
+
formatted += pointer + '\n';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return formatted;
|
|
109
|
+
};
|
|
110
|
+
const reset = () => {
|
|
111
|
+
state.suiteHierarchy = [];
|
|
112
|
+
state.testResults.clear();
|
|
113
|
+
state.fileStartTime = 0;
|
|
114
|
+
state.currentTest = undefined;
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
handleBridgeEvent,
|
|
118
|
+
reset,
|
|
119
|
+
};
|
|
120
|
+
};
|