@react-native-harness/jest 1.3.0 → 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 +29 -4
- package/dist/__tests__/crash-monitor.test.js +194 -176
- 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__/errors.test.js +39 -0
- package/dist/__tests__/execute-run.test.js +439 -29
- 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 +217 -234
- package/dist/__tests__/index.test.js +62 -11
- 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/__tests__/test-file-platform-filter.test.d.ts +2 -0
- package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
- package/dist/__tests__/test-file-platform-filter.test.js +47 -0
- package/dist/crash-monitor.d.ts +12 -4
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +135 -102
- 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/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -2
- package/dist/execute-run.d.ts +4 -2
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +179 -49
- 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 +46 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +386 -118
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +22 -4
- package/dist/test-file-platform-filter.d.ts +5 -0
- package/dist/test-file-platform-filter.d.ts.map +1 -0
- package/dist/test-file-platform-filter.js +40 -0
- package/dist/toTestResult.d.ts +1 -0
- package/dist/toTestResult.d.ts.map +1 -1
- package/dist/toTestResult.js +2 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/bridge.test.ts +45 -5
- package/src/__tests__/crash-monitor.test.ts +234 -199
- package/src/__tests__/environment-reset.test.ts +308 -0
- package/src/__tests__/errors.test.ts +75 -0
- package/src/__tests__/execute-run.test.ts +561 -37
- 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 +303 -0
- package/src/__tests__/index.test.ts +92 -12
- package/src/__tests__/metro-port.test.ts +2 -0
- package/src/__tests__/run.test.ts +74 -0
- package/src/__tests__/test-file-platform-filter.test.ts +83 -0
- package/src/crash-monitor.ts +220 -131
- 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/errors.ts +62 -1
- package/src/execute-run.ts +230 -57
- package/src/format-harness-error.ts +73 -0
- package/src/harness-session.ts +535 -162
- package/src/index.ts +40 -11
- package/src/run.ts +24 -4
- package/src/test-file-platform-filter.ts +57 -0
- package/src/toTestResult.ts +3 -2
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { NativeCrashError, StartupStallError } from '../errors.js';
|
|
3
|
-
import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
|
|
3
|
+
import { AppBridgeDisconnectedError, DeviceNotRespondingError, } from '@react-native-harness/bridge/server';
|
|
4
|
+
import { createDiagnostics } from '@react-native-harness/tools';
|
|
4
5
|
import { executeRun } from '../execute-run.js';
|
|
5
6
|
const resolveUndefined = async () => undefined;
|
|
6
7
|
// Mock the file-runner so we control what jestResult/harnessResult each test returns
|
|
@@ -9,6 +10,15 @@ const mockRunHarnessTestFile = vi.hoisted(() => vi.fn());
|
|
|
9
10
|
vi.mock('../run.js', () => ({
|
|
10
11
|
runHarnessTestFile: mockRunHarnessTestFile,
|
|
11
12
|
}));
|
|
13
|
+
// Mock the reporting module so diagnostics tests can inspect the entries that
|
|
14
|
+
// would have been reported without touching the real filesystem, and so
|
|
15
|
+
// asserting on them doesn't race with executeRun's own diagnostics.flush().
|
|
16
|
+
const mockPrintSummary = vi.hoisted(() => vi.fn());
|
|
17
|
+
const mockWriteTraceFile = vi.hoisted(() => vi.fn(async () => '/fake/trace.json'));
|
|
18
|
+
vi.mock('../diagnostics/index.js', () => ({
|
|
19
|
+
printSummary: mockPrintSummary,
|
|
20
|
+
writeTraceFile: mockWriteTraceFile,
|
|
21
|
+
}));
|
|
12
22
|
// ---------------------------------------------------------------------------
|
|
13
23
|
// Helpers
|
|
14
24
|
// ---------------------------------------------------------------------------
|
|
@@ -66,18 +76,40 @@ const makeSession = (overrides = {}) => ({
|
|
|
66
76
|
host: undefined,
|
|
67
77
|
resetEnvironmentBetweenTestFiles: false,
|
|
68
78
|
detectNativeCrashes: true,
|
|
79
|
+
runners: [
|
|
80
|
+
{ platformId: 'android', name: 'android' },
|
|
81
|
+
{ platformId: 'ios', name: 'ios' },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
context: {
|
|
85
|
+
platform: {
|
|
86
|
+
platformId: 'android',
|
|
87
|
+
name: 'android',
|
|
88
|
+
runner: '/virtual/android-runner.js',
|
|
89
|
+
cli: '/virtual/android-cli.js',
|
|
90
|
+
config: {},
|
|
91
|
+
},
|
|
69
92
|
},
|
|
70
|
-
|
|
93
|
+
diagnostics: createDiagnostics({ enabled: false }),
|
|
71
94
|
ensureAppReady: vi.fn(resolveUndefined),
|
|
72
95
|
runTestFile: vi.fn(async () => makeHarnessResult()),
|
|
73
96
|
restartApp: vi.fn(resolveUndefined),
|
|
97
|
+
resetEnvironment: vi.fn(resolveUndefined),
|
|
74
98
|
resetCrashState: vi.fn(),
|
|
75
99
|
flushClientLogs: vi.fn(() => []),
|
|
76
100
|
callHook: vi.fn(resolveUndefined),
|
|
101
|
+
onTestRunnerEvent: vi.fn(() => () => undefined),
|
|
77
102
|
setRunState: vi.fn(),
|
|
78
103
|
dispose: vi.fn(resolveUndefined),
|
|
79
104
|
...overrides,
|
|
80
105
|
});
|
|
106
|
+
const makeEmitEvent = () => {
|
|
107
|
+
const calls = [];
|
|
108
|
+
const emitEvent = (async (...eventData) => {
|
|
109
|
+
calls.push(eventData);
|
|
110
|
+
});
|
|
111
|
+
return { emitEvent, calls };
|
|
112
|
+
};
|
|
81
113
|
// ---------------------------------------------------------------------------
|
|
82
114
|
// Tests
|
|
83
115
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +124,7 @@ describe('executeRun', () => {
|
|
|
92
124
|
const session = makeSession({
|
|
93
125
|
callHook: vi.fn(async (name) => { hookNames.push(name); }),
|
|
94
126
|
});
|
|
95
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
127
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
96
128
|
expect(hookNames[0]).toBe('run:started');
|
|
97
129
|
expect(hookNames[hookNames.length - 1]).toBe('run:finished');
|
|
98
130
|
});
|
|
@@ -101,7 +133,7 @@ describe('executeRun', () => {
|
|
|
101
133
|
const session = makeSession({
|
|
102
134
|
callHook: vi.fn(async (name) => { hookNames.push(name); }),
|
|
103
135
|
});
|
|
104
|
-
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(),
|
|
136
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
105
137
|
const fileHooks = hookNames.filter((n) => n === 'test-file:started' || n === 'test-file:finished');
|
|
106
138
|
expect(fileHooks).toEqual([
|
|
107
139
|
'test-file:started', 'test-file:finished',
|
|
@@ -114,24 +146,79 @@ describe('executeRun', () => {
|
|
|
114
146
|
callHook: vi.fn(async (name) => { hookNames.push(name); }),
|
|
115
147
|
ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
|
|
116
148
|
});
|
|
117
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
149
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
118
150
|
expect(hookNames).toContain('run:finished');
|
|
119
151
|
});
|
|
120
152
|
});
|
|
153
|
+
describe('diagnostics', () => {
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
mockPrintSummary.mockClear();
|
|
156
|
+
mockWriteTraceFile.mockClear();
|
|
157
|
+
});
|
|
158
|
+
it('records run.total and run.file spans when diagnostics is enabled', async () => {
|
|
159
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
160
|
+
const session = makeSession({ diagnostics });
|
|
161
|
+
await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
162
|
+
// executeRun flushes and hands entries to the (mocked) reporter in its
|
|
163
|
+
// finally block, so we inspect what was reported rather than flushing
|
|
164
|
+
// the diagnostics instance ourselves.
|
|
165
|
+
expect(mockPrintSummary).toHaveBeenCalledTimes(1);
|
|
166
|
+
const entries = mockPrintSummary.mock.calls[0]?.[0];
|
|
167
|
+
const runEntry = entries.find((e) => e.label === 'run.total');
|
|
168
|
+
const fileEntry = entries.find((e) => e.label === 'run.file');
|
|
169
|
+
expect(runEntry).toMatchObject({ status: 'ok', attrs: { status: 'passed' } });
|
|
170
|
+
expect(runEntry?.attrs?.runId).toBeTypeOf('string');
|
|
171
|
+
expect(fileEntry).toMatchObject({ status: 'ok', attrs: { file: '../a.ts', status: 'passed' } });
|
|
172
|
+
expect(fileEntry?.attrs?.runId).toBeTypeOf('string');
|
|
173
|
+
expect(mockWriteTraceFile).toHaveBeenCalledWith(entries, expect.objectContaining({ runId: expect.any(String) }));
|
|
174
|
+
});
|
|
175
|
+
it('still records run.total when a test throws', async () => {
|
|
176
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
177
|
+
const session = makeSession({
|
|
178
|
+
diagnostics,
|
|
179
|
+
ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
|
|
180
|
+
});
|
|
181
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
182
|
+
const entries = mockPrintSummary.mock.calls[0]?.[0];
|
|
183
|
+
expect(entries.find((e) => e.label === 'run.total')).toBeDefined();
|
|
184
|
+
expect(entries.find((e) => e.label === 'run.file')).toMatchObject({
|
|
185
|
+
attrs: { status: 'failed' },
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
it('is a no-op and does not generate a report when diagnostics is disabled', async () => {
|
|
189
|
+
const diagnostics = createDiagnostics({ enabled: false });
|
|
190
|
+
const session = makeSession({ diagnostics });
|
|
191
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
192
|
+
expect(diagnostics.flush()).toEqual([]);
|
|
193
|
+
expect(mockPrintSummary).not.toHaveBeenCalled();
|
|
194
|
+
expect(mockWriteTraceFile).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
121
197
|
describe('happy path', () => {
|
|
122
|
-
it('
|
|
198
|
+
it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
|
|
123
199
|
const order = [];
|
|
124
200
|
const session = makeSession({
|
|
125
201
|
ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
|
|
126
202
|
});
|
|
127
|
-
const
|
|
128
|
-
|
|
203
|
+
const emitEvent = (async (eventName) => {
|
|
204
|
+
if (eventName === 'test-file-start') {
|
|
205
|
+
order.push('test-file-start');
|
|
206
|
+
}
|
|
207
|
+
if (eventName === 'test-file-success') {
|
|
208
|
+
order.push('test-file-success');
|
|
209
|
+
}
|
|
210
|
+
});
|
|
129
211
|
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
130
212
|
order.push('runTestFile');
|
|
131
213
|
return makeFileRunResult();
|
|
132
214
|
});
|
|
133
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
134
|
-
expect(order).toEqual([
|
|
215
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
216
|
+
expect(order).toEqual([
|
|
217
|
+
'test-file-start',
|
|
218
|
+
'ensureAppReady',
|
|
219
|
+
'runTestFile',
|
|
220
|
+
'test-file-success',
|
|
221
|
+
]);
|
|
135
222
|
});
|
|
136
223
|
it('accumulates passing test counts in run:finished summary', async () => {
|
|
137
224
|
let finishedPayload;
|
|
@@ -142,11 +229,19 @@ describe('executeRun', () => {
|
|
|
142
229
|
}),
|
|
143
230
|
});
|
|
144
231
|
mockRunHarnessTestFile.mockResolvedValue(makeFileRunResult({ jestResult: makeJestResult({ numPassingTests: 3 }) }));
|
|
145
|
-
await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(),
|
|
232
|
+
await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
146
233
|
const payload = finishedPayload;
|
|
147
234
|
expect(payload.summary.passed).toBe(6);
|
|
148
235
|
expect(payload.status).toBe('passed');
|
|
149
236
|
});
|
|
237
|
+
it('marks the final run state as completed', async () => {
|
|
238
|
+
const session = makeSession();
|
|
239
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
240
|
+
expect(session.setRunState).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
241
|
+
completed: true,
|
|
242
|
+
status: 'passed',
|
|
243
|
+
}));
|
|
244
|
+
});
|
|
150
245
|
it('attaches buffered client logs to the Jest result', async () => {
|
|
151
246
|
const clientLogs = [{ message: 'Loaded screen', origin: '', type: 'warn' }];
|
|
152
247
|
const session = makeSession({
|
|
@@ -154,34 +249,174 @@ describe('executeRun', () => {
|
|
|
154
249
|
.mockReturnValueOnce([])
|
|
155
250
|
.mockReturnValueOnce(clientLogs),
|
|
156
251
|
});
|
|
157
|
-
const
|
|
158
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
252
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
253
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
159
254
|
expect(session.flushClientLogs).toHaveBeenCalledTimes(2);
|
|
160
|
-
expect(
|
|
255
|
+
expect(calls).toContainEqual([
|
|
256
|
+
'test-file-success',
|
|
257
|
+
expect.anything(),
|
|
258
|
+
expect.objectContaining({ console: clientLogs }),
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
it('emits test-case events before file success', async () => {
|
|
262
|
+
let testRunnerListener;
|
|
263
|
+
const { emitEvent, calls: emittedEvents } = makeEmitEvent();
|
|
264
|
+
const session = makeSession({
|
|
265
|
+
onTestRunnerEvent: vi.fn((listener) => {
|
|
266
|
+
testRunnerListener = listener;
|
|
267
|
+
return () => undefined;
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
271
|
+
testRunnerListener?.({
|
|
272
|
+
type: 'test-started',
|
|
273
|
+
file: 'example.ts',
|
|
274
|
+
suite: 'suite',
|
|
275
|
+
name: 'works',
|
|
276
|
+
ancestorTitles: ['suite'],
|
|
277
|
+
fullName: 'suite works',
|
|
278
|
+
startedAt: 10,
|
|
279
|
+
declarationMode: 'only',
|
|
280
|
+
});
|
|
281
|
+
testRunnerListener?.({
|
|
282
|
+
type: 'test-finished',
|
|
283
|
+
file: 'example.ts',
|
|
284
|
+
suite: 'suite',
|
|
285
|
+
name: 'works',
|
|
286
|
+
ancestorTitles: ['suite'],
|
|
287
|
+
fullName: 'suite works',
|
|
288
|
+
startedAt: 10,
|
|
289
|
+
declarationMode: 'only',
|
|
290
|
+
duration: 5,
|
|
291
|
+
status: 'passed',
|
|
292
|
+
});
|
|
293
|
+
return makeFileRunResult();
|
|
294
|
+
});
|
|
295
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
296
|
+
expect(emittedEvents).toEqual([
|
|
297
|
+
['test-file-start', expect.anything()],
|
|
298
|
+
[
|
|
299
|
+
'test-case-start',
|
|
300
|
+
'example.ts',
|
|
301
|
+
expect.objectContaining({
|
|
302
|
+
ancestorTitles: ['suite'],
|
|
303
|
+
fullName: 'suite works',
|
|
304
|
+
mode: 'only',
|
|
305
|
+
title: 'works',
|
|
306
|
+
startedAt: 10,
|
|
307
|
+
}),
|
|
308
|
+
],
|
|
309
|
+
[
|
|
310
|
+
'test-case-result',
|
|
311
|
+
'example.ts',
|
|
312
|
+
expect.objectContaining({
|
|
313
|
+
ancestorTitles: ['suite'],
|
|
314
|
+
fullName: 'suite works',
|
|
315
|
+
numPassingAsserts: 1,
|
|
316
|
+
startedAt: 10,
|
|
317
|
+
status: 'passed',
|
|
318
|
+
title: 'works',
|
|
319
|
+
}),
|
|
320
|
+
],
|
|
321
|
+
['test-file-success', expect.anything(), expect.anything()],
|
|
322
|
+
]);
|
|
323
|
+
});
|
|
324
|
+
it('includes pending promise diagnostics in live test-case failures', async () => {
|
|
325
|
+
let testRunnerListener;
|
|
326
|
+
const { emitEvent, calls: emittedEvents } = makeEmitEvent();
|
|
327
|
+
const session = makeSession({
|
|
328
|
+
onTestRunnerEvent: vi.fn((listener) => {
|
|
329
|
+
testRunnerListener = listener;
|
|
330
|
+
return () => undefined;
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
334
|
+
testRunnerListener?.({
|
|
335
|
+
type: 'test-finished',
|
|
336
|
+
file: 'example.ts',
|
|
337
|
+
suite: 'suite',
|
|
338
|
+
name: 'hangs',
|
|
339
|
+
ancestorTitles: ['suite'],
|
|
340
|
+
fullName: 'suite hangs',
|
|
341
|
+
startedAt: 100,
|
|
342
|
+
duration: 50,
|
|
343
|
+
status: 'failed',
|
|
344
|
+
error: {
|
|
345
|
+
name: 'TestCaseTimeoutError',
|
|
346
|
+
message: 'Test timed out after 50ms: suite hangs',
|
|
347
|
+
diagnostics: {
|
|
348
|
+
pendingPromises: {
|
|
349
|
+
total: 1,
|
|
350
|
+
items: [
|
|
351
|
+
{
|
|
352
|
+
id: 7,
|
|
353
|
+
createdAt: 110,
|
|
354
|
+
stack: 'Error: Promise created\n at hangs (example.ts:10:5)',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
return makeFileRunResult();
|
|
362
|
+
});
|
|
363
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
364
|
+
expect(emittedEvents).toContainEqual([
|
|
365
|
+
'test-case-result',
|
|
366
|
+
'example.ts',
|
|
367
|
+
expect.objectContaining({
|
|
368
|
+
failureMessages: [
|
|
369
|
+
expect.stringContaining('Pending promises at timeout: 1\n\nPromise #7, created 10ms after test start:'),
|
|
370
|
+
],
|
|
371
|
+
}),
|
|
372
|
+
]);
|
|
161
373
|
});
|
|
162
374
|
});
|
|
163
375
|
describe('runtime failures', () => {
|
|
164
376
|
it('passes StartupStallError to onFailure with an empty stack', async () => {
|
|
165
|
-
const
|
|
377
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
166
378
|
const session = makeSession({
|
|
167
379
|
ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1500, 3)),
|
|
168
380
|
});
|
|
169
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
170
|
-
expect(
|
|
381
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
382
|
+
expect(calls).toContainEqual([
|
|
383
|
+
'test-file-failure',
|
|
384
|
+
expect.objectContaining({ path: '/test/example.ts' }),
|
|
385
|
+
expect.objectContaining({ message: expect.stringContaining('1500'), stack: '' }),
|
|
386
|
+
]);
|
|
171
387
|
});
|
|
172
388
|
it('passes DeviceNotRespondingError to onFailure with an empty stack', async () => {
|
|
173
|
-
const
|
|
389
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
174
390
|
const session = makeSession({
|
|
175
391
|
ensureAppReady: vi.fn().mockRejectedValue(new DeviceNotRespondingError('runTests', [])),
|
|
176
392
|
});
|
|
177
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
178
|
-
expect(
|
|
393
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
394
|
+
expect(calls).toContainEqual([
|
|
395
|
+
'test-file-failure',
|
|
396
|
+
expect.anything(),
|
|
397
|
+
expect.objectContaining({ stack: '' }),
|
|
398
|
+
]);
|
|
399
|
+
});
|
|
400
|
+
it('passes AppBridgeDisconnectedError to onFailure with an empty stack', async () => {
|
|
401
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
402
|
+
const session = makeSession({
|
|
403
|
+
ensureAppReady: vi.fn().mockRejectedValue(new AppBridgeDisconnectedError('app-disconnected')),
|
|
404
|
+
});
|
|
405
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
406
|
+
expect(calls).toContainEqual([
|
|
407
|
+
'test-file-failure',
|
|
408
|
+
expect.anything(),
|
|
409
|
+
expect.objectContaining({
|
|
410
|
+
message: expect.stringContaining('The app bridge disconnected during test execution.'),
|
|
411
|
+
stack: '',
|
|
412
|
+
}),
|
|
413
|
+
]);
|
|
179
414
|
});
|
|
180
415
|
it('calls resetCrashState after a NativeCrashError', async () => {
|
|
181
416
|
const session = makeSession({
|
|
182
417
|
ensureAppReady: vi.fn().mockRejectedValue(new NativeCrashError('example.ts', { phase: 'execution', source: 'polling' })),
|
|
183
418
|
});
|
|
184
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
419
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
185
420
|
expect(session.resetCrashState).toHaveBeenCalled();
|
|
186
421
|
});
|
|
187
422
|
it('counts runtime failures in the summary', async () => {
|
|
@@ -193,7 +428,7 @@ describe('executeRun', () => {
|
|
|
193
428
|
}),
|
|
194
429
|
ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1000, 2)),
|
|
195
430
|
});
|
|
196
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
431
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
197
432
|
const payload = finishedPayload;
|
|
198
433
|
expect(payload.summary.failed).toBe(1);
|
|
199
434
|
expect(payload.status).toBe('failed');
|
|
@@ -205,25 +440,200 @@ describe('executeRun', () => {
|
|
|
205
440
|
const session = makeSession({
|
|
206
441
|
callHook: vi.fn(async (name) => { hookNames.push(name); }),
|
|
207
442
|
});
|
|
208
|
-
const
|
|
209
|
-
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(true /* interrupted */),
|
|
443
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
444
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(true /* interrupted */), emitEvent, makeGlobalConfig());
|
|
210
445
|
// No test was started; watcher was already interrupted.
|
|
211
|
-
expect(
|
|
446
|
+
expect(calls).not.toContainEqual(['test-file-start', expect.anything()]);
|
|
212
447
|
expect(hookNames).toContain('run:finished');
|
|
213
448
|
});
|
|
214
449
|
});
|
|
215
450
|
describe('resetEnvironmentBetweenTestFiles', () => {
|
|
216
|
-
it('calls
|
|
451
|
+
it('calls resetEnvironment before the second test but not the first', async () => {
|
|
452
|
+
const session = makeSession({
|
|
453
|
+
config: {
|
|
454
|
+
metroPort: 8081,
|
|
455
|
+
resetEnvironmentBetweenTestFiles: true,
|
|
456
|
+
detectNativeCrashes: false,
|
|
457
|
+
runners: [
|
|
458
|
+
{ platformId: 'android', name: 'android' },
|
|
459
|
+
{ platformId: 'ios', name: 'ios' },
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
464
|
+
// resetEnvironment should be called for tests 2 and 3, not test 1.
|
|
465
|
+
expect(session.resetEnvironment).toHaveBeenCalledTimes(2);
|
|
466
|
+
expect(session.restartApp).not.toHaveBeenCalled();
|
|
467
|
+
});
|
|
468
|
+
it('calls resetEnvironment with the "runtime" strategy configured', async () => {
|
|
469
|
+
const session = makeSession({
|
|
470
|
+
config: {
|
|
471
|
+
metroPort: 8081,
|
|
472
|
+
resetEnvironmentBetweenTestFiles: 'runtime',
|
|
473
|
+
detectNativeCrashes: false,
|
|
474
|
+
runners: [
|
|
475
|
+
{ platformId: 'android', name: 'android' },
|
|
476
|
+
{ platformId: 'ios', name: 'ios' },
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
481
|
+
expect(session.resetEnvironment).toHaveBeenCalledTimes(1);
|
|
482
|
+
expect(session.resetEnvironment).toHaveBeenCalledWith('/b.ts');
|
|
483
|
+
});
|
|
484
|
+
it('does not call resetEnvironment when resetEnvironmentBetweenTestFiles is false', async () => {
|
|
485
|
+
const session = makeSession({
|
|
486
|
+
config: {
|
|
487
|
+
metroPort: 8081,
|
|
488
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
489
|
+
detectNativeCrashes: false,
|
|
490
|
+
runners: [
|
|
491
|
+
{ platformId: 'android', name: 'android' },
|
|
492
|
+
{ platformId: 'ios', name: 'ios' },
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
497
|
+
expect(session.resetEnvironment).not.toHaveBeenCalled();
|
|
498
|
+
});
|
|
499
|
+
it('restarts after a test case timeout before the next runnable file', async () => {
|
|
500
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
501
|
+
timedOutResult.tests = [
|
|
502
|
+
{
|
|
503
|
+
name: 'hangs',
|
|
504
|
+
status: 'failed',
|
|
505
|
+
duration: 10,
|
|
506
|
+
error: {
|
|
507
|
+
name: 'TestCaseTimeoutError',
|
|
508
|
+
message: 'Test timed out after 10ms: hangs',
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
mockRunHarnessTestFile
|
|
513
|
+
.mockResolvedValueOnce(makeFileRunResult({
|
|
514
|
+
harnessResult: timedOutResult,
|
|
515
|
+
jestResult: makeJestResult({
|
|
516
|
+
numFailingTests: 1,
|
|
517
|
+
numPassingTests: 0,
|
|
518
|
+
}),
|
|
519
|
+
}))
|
|
520
|
+
.mockResolvedValueOnce(makeFileRunResult());
|
|
521
|
+
const session = makeSession({
|
|
522
|
+
config: {
|
|
523
|
+
metroPort: 8081,
|
|
524
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
525
|
+
detectNativeCrashes: false,
|
|
526
|
+
runners: [
|
|
527
|
+
{ platformId: 'android', name: 'android' },
|
|
528
|
+
{ platformId: 'ios', name: 'ios' },
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
533
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
534
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
535
|
+
});
|
|
536
|
+
it('restarts after a timeout in the last runnable file', async () => {
|
|
537
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
538
|
+
timedOutResult.tests = [
|
|
539
|
+
{
|
|
540
|
+
name: 'hangs',
|
|
541
|
+
status: 'failed',
|
|
542
|
+
duration: 10,
|
|
543
|
+
error: {
|
|
544
|
+
name: 'TestCaseTimeoutError',
|
|
545
|
+
message: 'Test timed out after 10ms: hangs',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
];
|
|
549
|
+
mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
|
|
550
|
+
harnessResult: timedOutResult,
|
|
551
|
+
jestResult: makeJestResult({
|
|
552
|
+
numFailingTests: 1,
|
|
553
|
+
numPassingTests: 0,
|
|
554
|
+
}),
|
|
555
|
+
}));
|
|
556
|
+
const session = makeSession({
|
|
557
|
+
config: {
|
|
558
|
+
metroPort: 8081,
|
|
559
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
560
|
+
detectNativeCrashes: false,
|
|
561
|
+
runners: [
|
|
562
|
+
{ platformId: 'android', name: 'android' },
|
|
563
|
+
{ platformId: 'ios', name: 'ios' },
|
|
564
|
+
],
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig({ watch: true }));
|
|
568
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
569
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
570
|
+
});
|
|
571
|
+
it('restarts after a suite hook timeout', async () => {
|
|
572
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
573
|
+
timedOutResult.suites = [
|
|
574
|
+
{
|
|
575
|
+
name: 'suite',
|
|
576
|
+
tests: [],
|
|
577
|
+
suites: [],
|
|
578
|
+
status: 'failed',
|
|
579
|
+
duration: 10,
|
|
580
|
+
error: {
|
|
581
|
+
name: 'SuiteHookTimeoutError',
|
|
582
|
+
message: 'beforeAll hook timed out after 10ms in suite: suite',
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
];
|
|
586
|
+
mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
|
|
587
|
+
harnessResult: timedOutResult,
|
|
588
|
+
jestResult: makeJestResult({
|
|
589
|
+
numFailingTests: 1,
|
|
590
|
+
numPassingTests: 0,
|
|
591
|
+
}),
|
|
592
|
+
}));
|
|
593
|
+
const session = makeSession();
|
|
594
|
+
await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
595
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
596
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
describe('platform-specific test files', () => {
|
|
600
|
+
it('skips files for other platforms without running them on the device', async () => {
|
|
601
|
+
const session = makeSession();
|
|
602
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
603
|
+
await executeRun(session, [
|
|
604
|
+
makeTest('/project/smoke.harness.ts'),
|
|
605
|
+
makeTest('/project/kotlin.android.harness.ts'),
|
|
606
|
+
makeTest('/project/swift.ios.harness.ts'),
|
|
607
|
+
], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
608
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(2);
|
|
609
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledWith(expect.objectContaining({ testPath: '/project/smoke.harness.ts' }));
|
|
610
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledWith(expect.objectContaining({ testPath: '/project/kotlin.android.harness.ts' }));
|
|
611
|
+
expect(session.ensureAppReady).toHaveBeenCalledTimes(2);
|
|
612
|
+
expect(calls).toContainEqual([
|
|
613
|
+
'test-file-success',
|
|
614
|
+
expect.objectContaining({ path: '/project/swift.ios.harness.ts' }),
|
|
615
|
+
expect.objectContaining({ skipped: true }),
|
|
616
|
+
]);
|
|
617
|
+
});
|
|
618
|
+
it('does not restart the app for skipped platform-specific files', async () => {
|
|
217
619
|
const session = makeSession({
|
|
218
620
|
config: {
|
|
219
621
|
metroPort: 8081,
|
|
220
622
|
resetEnvironmentBetweenTestFiles: true,
|
|
221
623
|
detectNativeCrashes: false,
|
|
624
|
+
runners: [
|
|
625
|
+
{ platformId: 'android', name: 'android' },
|
|
626
|
+
{ platformId: 'ios', name: 'ios' },
|
|
627
|
+
],
|
|
222
628
|
},
|
|
223
629
|
});
|
|
224
|
-
await executeRun(session, [
|
|
225
|
-
|
|
226
|
-
|
|
630
|
+
await executeRun(session, [
|
|
631
|
+
makeTest('/project/swift.ios.harness.ts'),
|
|
632
|
+
makeTest('/project/kotlin.android.harness.ts'),
|
|
633
|
+
], makeWatcher(), vi.fn(), makeGlobalConfig());
|
|
634
|
+
expect(session.restartApp).not.toHaveBeenCalled();
|
|
635
|
+
expect(session.resetEnvironment).not.toHaveBeenCalled();
|
|
636
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
|
|
227
637
|
});
|
|
228
638
|
});
|
|
229
639
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-harness-error.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/format-harness-error.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { formatHarnessErrorMessage } from '../format-harness-error.js';
|
|
3
|
+
describe('formatHarnessErrorMessage', () => {
|
|
4
|
+
it('formats pending promise diagnostics for timeout errors', () => {
|
|
5
|
+
const message = formatHarnessErrorMessage({
|
|
6
|
+
name: 'TestCaseTimeoutError',
|
|
7
|
+
message: 'Test timed out after 50ms: suite hangs',
|
|
8
|
+
diagnostics: {
|
|
9
|
+
pendingPromises: {
|
|
10
|
+
total: 2,
|
|
11
|
+
items: [
|
|
12
|
+
{
|
|
13
|
+
id: 7,
|
|
14
|
+
createdAt: 110,
|
|
15
|
+
stack: 'Error: Promise created\n at hangs (example.ts:10:5)',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}, { testStartedAt: 100 });
|
|
21
|
+
expect(message).toContain('Test timed out after 50ms: suite hangs');
|
|
22
|
+
expect(message).toContain('Pending promises at timeout: 2');
|
|
23
|
+
expect(message).toContain('Showing 1 of 2 pending promises.');
|
|
24
|
+
expect(message).toContain('Promise #7, created 10ms after test start:');
|
|
25
|
+
expect(message).toContain(' Error: Promise created');
|
|
26
|
+
});
|
|
27
|
+
});
|