@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,12 +1,26 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
2
|
import type { Config, Test, TestWatcher } from 'jest-runner';
|
|
3
3
|
import type { TestResult as JestTestResult } from '@jest/test-result';
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
TestRunnerTestFinishedEvent,
|
|
6
|
+
TestRunnerTestStartedEvent,
|
|
7
|
+
TestSuiteResult,
|
|
8
|
+
} from '@react-native-harness/bridge';
|
|
5
9
|
import { NativeCrashError, StartupStallError } from '../errors.js';
|
|
6
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
AppBridgeDisconnectedError,
|
|
12
|
+
DeviceNotRespondingError,
|
|
13
|
+
} from '@react-native-harness/bridge/server';
|
|
14
|
+
import { createDiagnostics } from '@react-native-harness/tools';
|
|
7
15
|
import type { HarnessSession } from '../harness-session.js';
|
|
8
16
|
import { executeRun } from '../execute-run.js';
|
|
9
17
|
|
|
18
|
+
type EmitEvent = Parameters<typeof executeRun>[3];
|
|
19
|
+
type RecordedEmitEvent = {
|
|
20
|
+
emitEvent: EmitEvent;
|
|
21
|
+
calls: Array<unknown[]>;
|
|
22
|
+
};
|
|
23
|
+
|
|
10
24
|
const resolveUndefined = async () => undefined;
|
|
11
25
|
|
|
12
26
|
// Mock the file-runner so we control what jestResult/harnessResult each test returns
|
|
@@ -16,6 +30,16 @@ vi.mock('../run.js', () => ({
|
|
|
16
30
|
runHarnessTestFile: mockRunHarnessTestFile,
|
|
17
31
|
}));
|
|
18
32
|
|
|
33
|
+
// Mock the reporting module so diagnostics tests can inspect the entries that
|
|
34
|
+
// would have been reported without touching the real filesystem, and so
|
|
35
|
+
// asserting on them doesn't race with executeRun's own diagnostics.flush().
|
|
36
|
+
const mockPrintSummary = vi.hoisted(() => vi.fn());
|
|
37
|
+
const mockWriteTraceFile = vi.hoisted(() => vi.fn(async () => '/fake/trace.json'));
|
|
38
|
+
vi.mock('../diagnostics/index.js', () => ({
|
|
39
|
+
printSummary: mockPrintSummary,
|
|
40
|
+
writeTraceFile: mockWriteTraceFile,
|
|
41
|
+
}));
|
|
42
|
+
|
|
19
43
|
// ---------------------------------------------------------------------------
|
|
20
44
|
// Helpers
|
|
21
45
|
// ---------------------------------------------------------------------------
|
|
@@ -85,19 +109,43 @@ const makeSession = (overrides: Partial<HarnessSession> = {}): HarnessSession =>
|
|
|
85
109
|
host: undefined,
|
|
86
110
|
resetEnvironmentBetweenTestFiles: false,
|
|
87
111
|
detectNativeCrashes: true,
|
|
112
|
+
runners: [
|
|
113
|
+
{ platformId: 'android', name: 'android' },
|
|
114
|
+
{ platformId: 'ios', name: 'ios' },
|
|
115
|
+
],
|
|
88
116
|
} as HarnessSession['config'],
|
|
89
|
-
context: {
|
|
117
|
+
context: {
|
|
118
|
+
platform: {
|
|
119
|
+
platformId: 'android',
|
|
120
|
+
name: 'android',
|
|
121
|
+
runner: '/virtual/android-runner.js',
|
|
122
|
+
cli: '/virtual/android-cli.js',
|
|
123
|
+
config: {},
|
|
124
|
+
},
|
|
125
|
+
} as HarnessSession['context'],
|
|
126
|
+
diagnostics: createDiagnostics({ enabled: false }),
|
|
90
127
|
ensureAppReady: vi.fn(resolveUndefined),
|
|
91
128
|
runTestFile: vi.fn(async () => makeHarnessResult()),
|
|
92
129
|
restartApp: vi.fn(resolveUndefined),
|
|
130
|
+
resetEnvironment: vi.fn(resolveUndefined),
|
|
93
131
|
resetCrashState: vi.fn(),
|
|
94
132
|
flushClientLogs: vi.fn(() => []),
|
|
95
133
|
callHook: vi.fn(resolveUndefined),
|
|
134
|
+
onTestRunnerEvent: vi.fn(() => () => undefined),
|
|
96
135
|
setRunState: vi.fn(),
|
|
97
136
|
dispose: vi.fn(resolveUndefined),
|
|
98
137
|
...overrides,
|
|
99
138
|
});
|
|
100
139
|
|
|
140
|
+
const makeEmitEvent = (): RecordedEmitEvent => {
|
|
141
|
+
const calls: Array<unknown[]> = [];
|
|
142
|
+
const emitEvent: EmitEvent = (async (...eventData) => {
|
|
143
|
+
calls.push(eventData);
|
|
144
|
+
}) as EmitEvent;
|
|
145
|
+
|
|
146
|
+
return { emitEvent, calls };
|
|
147
|
+
};
|
|
148
|
+
|
|
101
149
|
// ---------------------------------------------------------------------------
|
|
102
150
|
// Tests
|
|
103
151
|
// ---------------------------------------------------------------------------
|
|
@@ -115,7 +163,7 @@ describe('executeRun', () => {
|
|
|
115
163
|
callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
|
|
116
164
|
});
|
|
117
165
|
|
|
118
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
166
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
119
167
|
|
|
120
168
|
expect(hookNames[0]).toBe('run:started');
|
|
121
169
|
expect(hookNames[hookNames.length - 1]).toBe('run:finished');
|
|
@@ -127,7 +175,7 @@ describe('executeRun', () => {
|
|
|
127
175
|
callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
|
|
128
176
|
});
|
|
129
177
|
|
|
130
|
-
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(),
|
|
178
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
131
179
|
|
|
132
180
|
const fileHooks = hookNames.filter((n) =>
|
|
133
181
|
n === 'test-file:started' || n === 'test-file:finished',
|
|
@@ -145,29 +193,96 @@ describe('executeRun', () => {
|
|
|
145
193
|
ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
|
|
146
194
|
});
|
|
147
195
|
|
|
148
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
196
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
149
197
|
|
|
150
198
|
expect(hookNames).toContain('run:finished');
|
|
151
199
|
});
|
|
152
200
|
});
|
|
153
201
|
|
|
202
|
+
describe('diagnostics', () => {
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
mockPrintSummary.mockClear();
|
|
205
|
+
mockWriteTraceFile.mockClear();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('records run.total and run.file spans when diagnostics is enabled', async () => {
|
|
209
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
210
|
+
const session = makeSession({ diagnostics });
|
|
211
|
+
|
|
212
|
+
await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
213
|
+
|
|
214
|
+
// executeRun flushes and hands entries to the (mocked) reporter in its
|
|
215
|
+
// finally block, so we inspect what was reported rather than flushing
|
|
216
|
+
// the diagnostics instance ourselves.
|
|
217
|
+
expect(mockPrintSummary).toHaveBeenCalledTimes(1);
|
|
218
|
+
const entries = mockPrintSummary.mock.calls[0]?.[0] as Array<{ label: string; status: string; attrs?: Record<string, unknown> }>;
|
|
219
|
+
const runEntry = entries.find((e) => e.label === 'run.total');
|
|
220
|
+
const fileEntry = entries.find((e) => e.label === 'run.file');
|
|
221
|
+
|
|
222
|
+
expect(runEntry).toMatchObject({ status: 'ok', attrs: { status: 'passed' } });
|
|
223
|
+
expect(runEntry?.attrs?.runId).toBeTypeOf('string');
|
|
224
|
+
expect(fileEntry).toMatchObject({ status: 'ok', attrs: { file: '../a.ts', status: 'passed' } });
|
|
225
|
+
expect(fileEntry?.attrs?.runId).toBeTypeOf('string');
|
|
226
|
+
expect(mockWriteTraceFile).toHaveBeenCalledWith(entries, expect.objectContaining({ runId: expect.any(String) }));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('still records run.total when a test throws', async () => {
|
|
230
|
+
const diagnostics = createDiagnostics({ enabled: true });
|
|
231
|
+
const session = makeSession({
|
|
232
|
+
diagnostics,
|
|
233
|
+
ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
237
|
+
|
|
238
|
+
const entries = mockPrintSummary.mock.calls[0]?.[0] as Array<{ label: string; attrs?: Record<string, unknown> }>;
|
|
239
|
+
expect(entries.find((e) => e.label === 'run.total')).toBeDefined();
|
|
240
|
+
expect(entries.find((e) => e.label === 'run.file')).toMatchObject({
|
|
241
|
+
attrs: { status: 'failed' },
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('is a no-op and does not generate a report when diagnostics is disabled', async () => {
|
|
246
|
+
const diagnostics = createDiagnostics({ enabled: false });
|
|
247
|
+
const session = makeSession({ diagnostics });
|
|
248
|
+
|
|
249
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
250
|
+
|
|
251
|
+
expect(diagnostics.flush()).toEqual([]);
|
|
252
|
+
expect(mockPrintSummary).not.toHaveBeenCalled();
|
|
253
|
+
expect(mockWriteTraceFile).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
154
257
|
describe('happy path', () => {
|
|
155
|
-
it('
|
|
258
|
+
it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
|
|
156
259
|
const order: string[] = [];
|
|
157
260
|
const session = makeSession({
|
|
158
261
|
ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
|
|
159
262
|
});
|
|
160
|
-
const
|
|
161
|
-
|
|
263
|
+
const emitEvent: EmitEvent = (async (eventName) => {
|
|
264
|
+
if (eventName === 'test-file-start') {
|
|
265
|
+
order.push('test-file-start');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (eventName === 'test-file-success') {
|
|
269
|
+
order.push('test-file-success');
|
|
270
|
+
}
|
|
271
|
+
}) as EmitEvent;
|
|
162
272
|
|
|
163
273
|
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
164
274
|
order.push('runTestFile');
|
|
165
275
|
return makeFileRunResult();
|
|
166
276
|
});
|
|
167
277
|
|
|
168
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
278
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
169
279
|
|
|
170
|
-
expect(order).toEqual([
|
|
280
|
+
expect(order).toEqual([
|
|
281
|
+
'test-file-start',
|
|
282
|
+
'ensureAppReady',
|
|
283
|
+
'runTestFile',
|
|
284
|
+
'test-file-success',
|
|
285
|
+
]);
|
|
171
286
|
});
|
|
172
287
|
|
|
173
288
|
it('accumulates passing test counts in run:finished summary', async () => {
|
|
@@ -182,13 +297,32 @@ describe('executeRun', () => {
|
|
|
182
297
|
makeFileRunResult({ jestResult: makeJestResult({ numPassingTests: 3 }) }),
|
|
183
298
|
);
|
|
184
299
|
|
|
185
|
-
await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(),
|
|
300
|
+
await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
186
301
|
|
|
187
302
|
const payload = finishedPayload as { summary: { passed: number }; status: string };
|
|
188
303
|
expect(payload.summary.passed).toBe(6);
|
|
189
304
|
expect(payload.status).toBe('passed');
|
|
190
305
|
});
|
|
191
306
|
|
|
307
|
+
it('marks the final run state as completed', async () => {
|
|
308
|
+
const session = makeSession();
|
|
309
|
+
|
|
310
|
+
await executeRun(
|
|
311
|
+
session,
|
|
312
|
+
[makeTest()],
|
|
313
|
+
makeWatcher(),
|
|
314
|
+
makeEmitEvent().emitEvent,
|
|
315
|
+
makeGlobalConfig(),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(session.setRunState).toHaveBeenLastCalledWith(
|
|
319
|
+
expect.objectContaining({
|
|
320
|
+
completed: true,
|
|
321
|
+
status: 'passed',
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
192
326
|
it('attaches buffered client logs to the Jest result', async () => {
|
|
193
327
|
const clientLogs = [{ message: 'Loaded screen', origin: '', type: 'warn' }] satisfies NonNullable<JestTestResult['console']>;
|
|
194
328
|
const session = makeSession({
|
|
@@ -196,55 +330,205 @@ describe('executeRun', () => {
|
|
|
196
330
|
.mockReturnValueOnce([])
|
|
197
331
|
.mockReturnValueOnce(clientLogs),
|
|
198
332
|
});
|
|
199
|
-
const
|
|
333
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
200
334
|
|
|
201
335
|
await executeRun(
|
|
202
336
|
session,
|
|
203
337
|
[makeTest()],
|
|
204
338
|
makeWatcher(),
|
|
205
|
-
|
|
206
|
-
onResult,
|
|
207
|
-
vi.fn(),
|
|
339
|
+
emitEvent,
|
|
208
340
|
makeGlobalConfig(),
|
|
209
341
|
);
|
|
210
342
|
|
|
211
343
|
expect(session.flushClientLogs).toHaveBeenCalledTimes(2);
|
|
212
|
-
expect(
|
|
344
|
+
expect(calls).toContainEqual([
|
|
345
|
+
'test-file-success',
|
|
213
346
|
expect.anything(),
|
|
214
347
|
expect.objectContaining({ console: clientLogs }),
|
|
215
|
-
);
|
|
348
|
+
]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('emits test-case events before file success', async () => {
|
|
352
|
+
let testRunnerListener:
|
|
353
|
+
| ((event: TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent) => void)
|
|
354
|
+
| undefined;
|
|
355
|
+
const { emitEvent, calls: emittedEvents } = makeEmitEvent();
|
|
356
|
+
const session = makeSession({
|
|
357
|
+
onTestRunnerEvent: vi.fn((listener) => {
|
|
358
|
+
testRunnerListener = listener as typeof testRunnerListener;
|
|
359
|
+
return () => undefined;
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
364
|
+
testRunnerListener?.({
|
|
365
|
+
type: 'test-started',
|
|
366
|
+
file: 'example.ts',
|
|
367
|
+
suite: 'suite',
|
|
368
|
+
name: 'works',
|
|
369
|
+
ancestorTitles: ['suite'],
|
|
370
|
+
fullName: 'suite works',
|
|
371
|
+
startedAt: 10,
|
|
372
|
+
declarationMode: 'only',
|
|
373
|
+
});
|
|
374
|
+
testRunnerListener?.({
|
|
375
|
+
type: 'test-finished',
|
|
376
|
+
file: 'example.ts',
|
|
377
|
+
suite: 'suite',
|
|
378
|
+
name: 'works',
|
|
379
|
+
ancestorTitles: ['suite'],
|
|
380
|
+
fullName: 'suite works',
|
|
381
|
+
startedAt: 10,
|
|
382
|
+
declarationMode: 'only',
|
|
383
|
+
duration: 5,
|
|
384
|
+
status: 'passed',
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return makeFileRunResult();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
391
|
+
|
|
392
|
+
expect(emittedEvents).toEqual([
|
|
393
|
+
['test-file-start', expect.anything()],
|
|
394
|
+
[
|
|
395
|
+
'test-case-start',
|
|
396
|
+
'example.ts',
|
|
397
|
+
expect.objectContaining({
|
|
398
|
+
ancestorTitles: ['suite'],
|
|
399
|
+
fullName: 'suite works',
|
|
400
|
+
mode: 'only',
|
|
401
|
+
title: 'works',
|
|
402
|
+
startedAt: 10,
|
|
403
|
+
}),
|
|
404
|
+
],
|
|
405
|
+
[
|
|
406
|
+
'test-case-result',
|
|
407
|
+
'example.ts',
|
|
408
|
+
expect.objectContaining({
|
|
409
|
+
ancestorTitles: ['suite'],
|
|
410
|
+
fullName: 'suite works',
|
|
411
|
+
numPassingAsserts: 1,
|
|
412
|
+
startedAt: 10,
|
|
413
|
+
status: 'passed',
|
|
414
|
+
title: 'works',
|
|
415
|
+
}),
|
|
416
|
+
],
|
|
417
|
+
['test-file-success', expect.anything(), expect.anything()],
|
|
418
|
+
]);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('includes pending promise diagnostics in live test-case failures', async () => {
|
|
422
|
+
let testRunnerListener:
|
|
423
|
+
| ((event: TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent) => void)
|
|
424
|
+
| undefined;
|
|
425
|
+
const { emitEvent, calls: emittedEvents } = makeEmitEvent();
|
|
426
|
+
const session = makeSession({
|
|
427
|
+
onTestRunnerEvent: vi.fn((listener) => {
|
|
428
|
+
testRunnerListener = listener as typeof testRunnerListener;
|
|
429
|
+
return () => undefined;
|
|
430
|
+
}),
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
434
|
+
testRunnerListener?.({
|
|
435
|
+
type: 'test-finished',
|
|
436
|
+
file: 'example.ts',
|
|
437
|
+
suite: 'suite',
|
|
438
|
+
name: 'hangs',
|
|
439
|
+
ancestorTitles: ['suite'],
|
|
440
|
+
fullName: 'suite hangs',
|
|
441
|
+
startedAt: 100,
|
|
442
|
+
duration: 50,
|
|
443
|
+
status: 'failed',
|
|
444
|
+
error: {
|
|
445
|
+
name: 'TestCaseTimeoutError',
|
|
446
|
+
message: 'Test timed out after 50ms: suite hangs',
|
|
447
|
+
diagnostics: {
|
|
448
|
+
pendingPromises: {
|
|
449
|
+
total: 1,
|
|
450
|
+
items: [
|
|
451
|
+
{
|
|
452
|
+
id: 7,
|
|
453
|
+
createdAt: 110,
|
|
454
|
+
stack: 'Error: Promise created\n at hangs (example.ts:10:5)',
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return makeFileRunResult();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
466
|
+
|
|
467
|
+
expect(emittedEvents).toContainEqual([
|
|
468
|
+
'test-case-result',
|
|
469
|
+
'example.ts',
|
|
470
|
+
expect.objectContaining({
|
|
471
|
+
failureMessages: [
|
|
472
|
+
expect.stringContaining(
|
|
473
|
+
'Pending promises at timeout: 1\n\nPromise #7, created 10ms after test start:',
|
|
474
|
+
),
|
|
475
|
+
],
|
|
476
|
+
}),
|
|
477
|
+
]);
|
|
216
478
|
});
|
|
217
479
|
});
|
|
218
480
|
|
|
219
481
|
describe('runtime failures', () => {
|
|
220
482
|
it('passes StartupStallError to onFailure with an empty stack', async () => {
|
|
221
|
-
const
|
|
483
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
222
484
|
const session = makeSession({
|
|
223
485
|
ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1500, 3)),
|
|
224
486
|
});
|
|
225
487
|
|
|
226
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
488
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
227
489
|
|
|
228
|
-
expect(
|
|
490
|
+
expect(calls).toContainEqual([
|
|
491
|
+
'test-file-failure',
|
|
229
492
|
expect.objectContaining({ path: '/test/example.ts' }),
|
|
230
493
|
expect.objectContaining({ message: expect.stringContaining('1500'), stack: '' }),
|
|
231
|
-
);
|
|
494
|
+
]);
|
|
232
495
|
});
|
|
233
496
|
|
|
234
497
|
it('passes DeviceNotRespondingError to onFailure with an empty stack', async () => {
|
|
235
|
-
const
|
|
498
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
236
499
|
const session = makeSession({
|
|
237
500
|
ensureAppReady: vi.fn().mockRejectedValue(
|
|
238
501
|
new DeviceNotRespondingError('runTests', []),
|
|
239
502
|
),
|
|
240
503
|
});
|
|
241
504
|
|
|
242
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
505
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
243
506
|
|
|
244
|
-
expect(
|
|
507
|
+
expect(calls).toContainEqual([
|
|
508
|
+
'test-file-failure',
|
|
245
509
|
expect.anything(),
|
|
246
510
|
expect.objectContaining({ stack: '' }),
|
|
247
|
-
);
|
|
511
|
+
]);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('passes AppBridgeDisconnectedError to onFailure with an empty stack', async () => {
|
|
515
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
516
|
+
const session = makeSession({
|
|
517
|
+
ensureAppReady: vi.fn().mockRejectedValue(
|
|
518
|
+
new AppBridgeDisconnectedError('app-disconnected'),
|
|
519
|
+
),
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
523
|
+
|
|
524
|
+
expect(calls).toContainEqual([
|
|
525
|
+
'test-file-failure',
|
|
526
|
+
expect.anything(),
|
|
527
|
+
expect.objectContaining({
|
|
528
|
+
message: expect.stringContaining('The app bridge disconnected during test execution.'),
|
|
529
|
+
stack: '',
|
|
530
|
+
}),
|
|
531
|
+
]);
|
|
248
532
|
});
|
|
249
533
|
|
|
250
534
|
it('calls resetCrashState after a NativeCrashError', async () => {
|
|
@@ -254,7 +538,7 @@ describe('executeRun', () => {
|
|
|
254
538
|
),
|
|
255
539
|
});
|
|
256
540
|
|
|
257
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
541
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
258
542
|
|
|
259
543
|
expect(session.resetCrashState).toHaveBeenCalled();
|
|
260
544
|
});
|
|
@@ -268,7 +552,7 @@ describe('executeRun', () => {
|
|
|
268
552
|
ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1000, 2)),
|
|
269
553
|
});
|
|
270
554
|
|
|
271
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
555
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
272
556
|
|
|
273
557
|
const payload = finishedPayload as { summary: { failed: number }; status: string };
|
|
274
558
|
expect(payload.summary.failed).toBe(1);
|
|
@@ -282,31 +566,33 @@ describe('executeRun', () => {
|
|
|
282
566
|
const session = makeSession({
|
|
283
567
|
callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
|
|
284
568
|
});
|
|
285
|
-
const
|
|
569
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
286
570
|
|
|
287
571
|
await executeRun(
|
|
288
572
|
session,
|
|
289
573
|
[makeTest('/a.ts'), makeTest('/b.ts')],
|
|
290
574
|
makeWatcher(true /* interrupted */),
|
|
291
|
-
|
|
292
|
-
vi.fn(),
|
|
293
|
-
vi.fn(),
|
|
575
|
+
emitEvent,
|
|
294
576
|
makeGlobalConfig(),
|
|
295
577
|
);
|
|
296
578
|
|
|
297
579
|
// No test was started; watcher was already interrupted.
|
|
298
|
-
expect(
|
|
580
|
+
expect(calls).not.toContainEqual(['test-file-start', expect.anything()]);
|
|
299
581
|
expect(hookNames).toContain('run:finished');
|
|
300
582
|
});
|
|
301
583
|
});
|
|
302
584
|
|
|
303
585
|
describe('resetEnvironmentBetweenTestFiles', () => {
|
|
304
|
-
it('calls
|
|
586
|
+
it('calls resetEnvironment before the second test but not the first', async () => {
|
|
305
587
|
const session = makeSession({
|
|
306
588
|
config: {
|
|
307
589
|
metroPort: 8081,
|
|
308
590
|
resetEnvironmentBetweenTestFiles: true,
|
|
309
591
|
detectNativeCrashes: false,
|
|
592
|
+
runners: [
|
|
593
|
+
{ platformId: 'android', name: 'android' },
|
|
594
|
+
{ platformId: 'ios', name: 'ios' },
|
|
595
|
+
],
|
|
310
596
|
} as HarnessSession['config'],
|
|
311
597
|
});
|
|
312
598
|
|
|
@@ -314,12 +600,250 @@ describe('executeRun', () => {
|
|
|
314
600
|
session,
|
|
315
601
|
[makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')],
|
|
316
602
|
makeWatcher(),
|
|
317
|
-
|
|
603
|
+
makeEmitEvent().emitEvent,
|
|
604
|
+
makeGlobalConfig(),
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
// resetEnvironment should be called for tests 2 and 3, not test 1.
|
|
608
|
+
expect(session.resetEnvironment).toHaveBeenCalledTimes(2);
|
|
609
|
+
expect(session.restartApp).not.toHaveBeenCalled();
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('calls resetEnvironment with the "runtime" strategy configured', async () => {
|
|
613
|
+
const session = makeSession({
|
|
614
|
+
config: {
|
|
615
|
+
metroPort: 8081,
|
|
616
|
+
resetEnvironmentBetweenTestFiles: 'runtime',
|
|
617
|
+
detectNativeCrashes: false,
|
|
618
|
+
runners: [
|
|
619
|
+
{ platformId: 'android', name: 'android' },
|
|
620
|
+
{ platformId: 'ios', name: 'ios' },
|
|
621
|
+
],
|
|
622
|
+
} as HarnessSession['config'],
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
await executeRun(
|
|
626
|
+
session,
|
|
627
|
+
[makeTest('/a.ts'), makeTest('/b.ts')],
|
|
628
|
+
makeWatcher(),
|
|
629
|
+
makeEmitEvent().emitEvent,
|
|
630
|
+
makeGlobalConfig(),
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
expect(session.resetEnvironment).toHaveBeenCalledTimes(1);
|
|
634
|
+
expect(session.resetEnvironment).toHaveBeenCalledWith('/b.ts');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('does not call resetEnvironment when resetEnvironmentBetweenTestFiles is false', async () => {
|
|
638
|
+
const session = makeSession({
|
|
639
|
+
config: {
|
|
640
|
+
metroPort: 8081,
|
|
641
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
642
|
+
detectNativeCrashes: false,
|
|
643
|
+
runners: [
|
|
644
|
+
{ platformId: 'android', name: 'android' },
|
|
645
|
+
{ platformId: 'ios', name: 'ios' },
|
|
646
|
+
],
|
|
647
|
+
} as HarnessSession['config'],
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
await executeRun(
|
|
651
|
+
session,
|
|
652
|
+
[makeTest('/a.ts'), makeTest('/b.ts')],
|
|
653
|
+
makeWatcher(),
|
|
654
|
+
makeEmitEvent().emitEvent,
|
|
655
|
+
makeGlobalConfig(),
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
expect(session.resetEnvironment).not.toHaveBeenCalled();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('restarts after a test case timeout before the next runnable file', async () => {
|
|
662
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
663
|
+
timedOutResult.tests = [
|
|
664
|
+
{
|
|
665
|
+
name: 'hangs',
|
|
666
|
+
status: 'failed',
|
|
667
|
+
duration: 10,
|
|
668
|
+
error: {
|
|
669
|
+
name: 'TestCaseTimeoutError',
|
|
670
|
+
message: 'Test timed out after 10ms: hangs',
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
];
|
|
674
|
+
mockRunHarnessTestFile
|
|
675
|
+
.mockResolvedValueOnce(makeFileRunResult({
|
|
676
|
+
harnessResult: timedOutResult,
|
|
677
|
+
jestResult: makeJestResult({
|
|
678
|
+
numFailingTests: 1,
|
|
679
|
+
numPassingTests: 0,
|
|
680
|
+
}),
|
|
681
|
+
}))
|
|
682
|
+
.mockResolvedValueOnce(makeFileRunResult());
|
|
683
|
+
const session = makeSession({
|
|
684
|
+
config: {
|
|
685
|
+
metroPort: 8081,
|
|
686
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
687
|
+
detectNativeCrashes: false,
|
|
688
|
+
runners: [
|
|
689
|
+
{ platformId: 'android', name: 'android' },
|
|
690
|
+
{ platformId: 'ios', name: 'ios' },
|
|
691
|
+
],
|
|
692
|
+
} as HarnessSession['config'],
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
await executeRun(
|
|
696
|
+
session,
|
|
697
|
+
[makeTest('/a.ts'), makeTest('/b.ts')],
|
|
698
|
+
makeWatcher(),
|
|
699
|
+
makeEmitEvent().emitEvent,
|
|
700
|
+
makeGlobalConfig(),
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
704
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('restarts after a timeout in the last runnable file', async () => {
|
|
708
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
709
|
+
timedOutResult.tests = [
|
|
710
|
+
{
|
|
711
|
+
name: 'hangs',
|
|
712
|
+
status: 'failed',
|
|
713
|
+
duration: 10,
|
|
714
|
+
error: {
|
|
715
|
+
name: 'TestCaseTimeoutError',
|
|
716
|
+
message: 'Test timed out after 10ms: hangs',
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
];
|
|
720
|
+
mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
|
|
721
|
+
harnessResult: timedOutResult,
|
|
722
|
+
jestResult: makeJestResult({
|
|
723
|
+
numFailingTests: 1,
|
|
724
|
+
numPassingTests: 0,
|
|
725
|
+
}),
|
|
726
|
+
}));
|
|
727
|
+
const session = makeSession({
|
|
728
|
+
config: {
|
|
729
|
+
metroPort: 8081,
|
|
730
|
+
resetEnvironmentBetweenTestFiles: false,
|
|
731
|
+
detectNativeCrashes: false,
|
|
732
|
+
runners: [
|
|
733
|
+
{ platformId: 'android', name: 'android' },
|
|
734
|
+
{ platformId: 'ios', name: 'ios' },
|
|
735
|
+
],
|
|
736
|
+
} as HarnessSession['config'],
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
await executeRun(
|
|
740
|
+
session,
|
|
741
|
+
[makeTest('/a.ts')],
|
|
742
|
+
makeWatcher(),
|
|
743
|
+
makeEmitEvent().emitEvent,
|
|
744
|
+
makeGlobalConfig({ watch: true }),
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
748
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('restarts after a suite hook timeout', async () => {
|
|
752
|
+
const timedOutResult = makeHarnessResult('failed');
|
|
753
|
+
timedOutResult.suites = [
|
|
754
|
+
{
|
|
755
|
+
name: 'suite',
|
|
756
|
+
tests: [],
|
|
757
|
+
suites: [],
|
|
758
|
+
status: 'failed',
|
|
759
|
+
duration: 10,
|
|
760
|
+
error: {
|
|
761
|
+
name: 'SuiteHookTimeoutError',
|
|
762
|
+
message: 'beforeAll hook timed out after 10ms in suite: suite',
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
];
|
|
766
|
+
mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
|
|
767
|
+
harnessResult: timedOutResult,
|
|
768
|
+
jestResult: makeJestResult({
|
|
769
|
+
numFailingTests: 1,
|
|
770
|
+
numPassingTests: 0,
|
|
771
|
+
}),
|
|
772
|
+
}));
|
|
773
|
+
const session = makeSession();
|
|
774
|
+
|
|
775
|
+
await executeRun(
|
|
776
|
+
session,
|
|
777
|
+
[makeTest('/a.ts')],
|
|
778
|
+
makeWatcher(),
|
|
779
|
+
makeEmitEvent().emitEvent,
|
|
780
|
+
makeGlobalConfig(),
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
expect(session.restartApp).toHaveBeenCalledTimes(1);
|
|
784
|
+
expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
describe('platform-specific test files', () => {
|
|
789
|
+
it('skips files for other platforms without running them on the device', async () => {
|
|
790
|
+
const session = makeSession();
|
|
791
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
792
|
+
|
|
793
|
+
await executeRun(
|
|
794
|
+
session,
|
|
795
|
+
[
|
|
796
|
+
makeTest('/project/smoke.harness.ts'),
|
|
797
|
+
makeTest('/project/kotlin.android.harness.ts'),
|
|
798
|
+
makeTest('/project/swift.ios.harness.ts'),
|
|
799
|
+
],
|
|
800
|
+
makeWatcher(),
|
|
801
|
+
emitEvent,
|
|
802
|
+
makeGlobalConfig(),
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(2);
|
|
806
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledWith(
|
|
807
|
+
expect.objectContaining({ testPath: '/project/smoke.harness.ts' }),
|
|
808
|
+
);
|
|
809
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledWith(
|
|
810
|
+
expect.objectContaining({ testPath: '/project/kotlin.android.harness.ts' }),
|
|
811
|
+
);
|
|
812
|
+
expect(session.ensureAppReady).toHaveBeenCalledTimes(2);
|
|
813
|
+
expect(calls).toContainEqual([
|
|
814
|
+
'test-file-success',
|
|
815
|
+
expect.objectContaining({ path: '/project/swift.ios.harness.ts' }),
|
|
816
|
+
expect.objectContaining({ skipped: true }),
|
|
817
|
+
]);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('does not restart the app for skipped platform-specific files', async () => {
|
|
821
|
+
const session = makeSession({
|
|
822
|
+
config: {
|
|
823
|
+
metroPort: 8081,
|
|
824
|
+
resetEnvironmentBetweenTestFiles: true,
|
|
825
|
+
detectNativeCrashes: false,
|
|
826
|
+
runners: [
|
|
827
|
+
{ platformId: 'android', name: 'android' },
|
|
828
|
+
{ platformId: 'ios', name: 'ios' },
|
|
829
|
+
],
|
|
830
|
+
} as HarnessSession['config'],
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
await executeRun(
|
|
834
|
+
session,
|
|
835
|
+
[
|
|
836
|
+
makeTest('/project/swift.ios.harness.ts'),
|
|
837
|
+
makeTest('/project/kotlin.android.harness.ts'),
|
|
838
|
+
],
|
|
839
|
+
makeWatcher(),
|
|
840
|
+
vi.fn(),
|
|
318
841
|
makeGlobalConfig(),
|
|
319
842
|
);
|
|
320
843
|
|
|
321
|
-
|
|
322
|
-
expect(session.
|
|
844
|
+
expect(session.restartApp).not.toHaveBeenCalled();
|
|
845
|
+
expect(session.resetEnvironment).not.toHaveBeenCalled();
|
|
846
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
|
|
323
847
|
});
|
|
324
848
|
});
|
|
325
849
|
});
|