@react-native-harness/jest 1.2.0 → 1.4.0-rc.1
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 +23 -2
- package/dist/__tests__/crash-monitor.test.js +146 -192
- package/dist/__tests__/errors.test.js +39 -0
- package/dist/__tests__/execute-run.test.js +190 -26
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts +7 -0
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-platform-runner.js +6 -0
- package/dist/__tests__/harness-session.test.d.ts +2 -0
- package/dist/__tests__/harness-session.test.d.ts.map +1 -0
- package/dist/__tests__/harness-session.test.js +74 -0
- package/dist/__tests__/index.test.js +59 -10
- 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 +5 -4
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +116 -102
- 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 +85 -7
- package/dist/harness-session.d.ts +21 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +134 -39
- 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 +1 -0
- 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 +7 -7
- package/src/__tests__/bridge.test.ts +37 -2
- package/src/__tests__/crash-monitor.test.ts +182 -231
- package/src/__tests__/errors.test.ts +75 -0
- package/src/__tests__/execute-run.test.ts +235 -34
- package/src/__tests__/harness-session.test.ts +93 -0
- package/src/__tests__/index.test.ts +89 -11
- package/src/__tests__/test-file-platform-filter.test.ts +83 -0
- package/src/crash-monitor.ts +192 -131
- package/src/errors.ts +62 -1
- package/src/execute-run.ts +125 -11
- package/src/harness-session.ts +177 -42
- package/src/index.ts +40 -11
- package/src/run.ts +1 -0
- package/src/test-file-platform-filter.ts +57 -0
- package/src/toTestResult.ts +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
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
4
|
import { executeRun } from '../execute-run.js';
|
|
5
5
|
const resolveUndefined = async () => undefined;
|
|
6
6
|
// Mock the file-runner so we control what jestResult/harnessResult each test returns
|
|
@@ -66,18 +66,38 @@ const makeSession = (overrides = {}) => ({
|
|
|
66
66
|
host: undefined,
|
|
67
67
|
resetEnvironmentBetweenTestFiles: false,
|
|
68
68
|
detectNativeCrashes: true,
|
|
69
|
+
runners: [
|
|
70
|
+
{ platformId: 'android', name: 'android' },
|
|
71
|
+
{ platformId: 'ios', name: 'ios' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
context: {
|
|
75
|
+
platform: {
|
|
76
|
+
platformId: 'android',
|
|
77
|
+
name: 'android',
|
|
78
|
+
runner: '/virtual/android-runner.js',
|
|
79
|
+
cli: '/virtual/android-cli.js',
|
|
80
|
+
config: {},
|
|
81
|
+
},
|
|
69
82
|
},
|
|
70
|
-
context: {},
|
|
71
83
|
ensureAppReady: vi.fn(resolveUndefined),
|
|
72
84
|
runTestFile: vi.fn(async () => makeHarnessResult()),
|
|
73
85
|
restartApp: vi.fn(resolveUndefined),
|
|
74
86
|
resetCrashState: vi.fn(),
|
|
75
87
|
flushClientLogs: vi.fn(() => []),
|
|
76
88
|
callHook: vi.fn(resolveUndefined),
|
|
89
|
+
onTestRunnerEvent: vi.fn(() => () => undefined),
|
|
77
90
|
setRunState: vi.fn(),
|
|
78
91
|
dispose: vi.fn(resolveUndefined),
|
|
79
92
|
...overrides,
|
|
80
93
|
});
|
|
94
|
+
const makeEmitEvent = () => {
|
|
95
|
+
const calls = [];
|
|
96
|
+
const emitEvent = (async (...eventData) => {
|
|
97
|
+
calls.push(eventData);
|
|
98
|
+
});
|
|
99
|
+
return { emitEvent, calls };
|
|
100
|
+
};
|
|
81
101
|
// ---------------------------------------------------------------------------
|
|
82
102
|
// Tests
|
|
83
103
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +112,7 @@ describe('executeRun', () => {
|
|
|
92
112
|
const session = makeSession({
|
|
93
113
|
callHook: vi.fn(async (name) => { hookNames.push(name); }),
|
|
94
114
|
});
|
|
95
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
115
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
96
116
|
expect(hookNames[0]).toBe('run:started');
|
|
97
117
|
expect(hookNames[hookNames.length - 1]).toBe('run:finished');
|
|
98
118
|
});
|
|
@@ -101,7 +121,7 @@ describe('executeRun', () => {
|
|
|
101
121
|
const session = makeSession({
|
|
102
122
|
callHook: vi.fn(async (name) => { hookNames.push(name); }),
|
|
103
123
|
});
|
|
104
|
-
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(),
|
|
124
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
105
125
|
const fileHooks = hookNames.filter((n) => n === 'test-file:started' || n === 'test-file:finished');
|
|
106
126
|
expect(fileHooks).toEqual([
|
|
107
127
|
'test-file:started', 'test-file:finished',
|
|
@@ -114,24 +134,35 @@ describe('executeRun', () => {
|
|
|
114
134
|
callHook: vi.fn(async (name) => { hookNames.push(name); }),
|
|
115
135
|
ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
|
|
116
136
|
});
|
|
117
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
137
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
118
138
|
expect(hookNames).toContain('run:finished');
|
|
119
139
|
});
|
|
120
140
|
});
|
|
121
141
|
describe('happy path', () => {
|
|
122
|
-
it('
|
|
142
|
+
it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
|
|
123
143
|
const order = [];
|
|
124
144
|
const session = makeSession({
|
|
125
145
|
ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
|
|
126
146
|
});
|
|
127
|
-
const
|
|
128
|
-
|
|
147
|
+
const emitEvent = (async (eventName, ..._eventData) => {
|
|
148
|
+
if (eventName === 'test-file-start') {
|
|
149
|
+
order.push('test-file-start');
|
|
150
|
+
}
|
|
151
|
+
if (eventName === 'test-file-success') {
|
|
152
|
+
order.push('test-file-success');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
129
155
|
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
130
156
|
order.push('runTestFile');
|
|
131
157
|
return makeFileRunResult();
|
|
132
158
|
});
|
|
133
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
134
|
-
expect(order).toEqual([
|
|
159
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
160
|
+
expect(order).toEqual([
|
|
161
|
+
'test-file-start',
|
|
162
|
+
'ensureAppReady',
|
|
163
|
+
'runTestFile',
|
|
164
|
+
'test-file-success',
|
|
165
|
+
]);
|
|
135
166
|
});
|
|
136
167
|
it('accumulates passing test counts in run:finished summary', async () => {
|
|
137
168
|
let finishedPayload;
|
|
@@ -142,7 +173,7 @@ describe('executeRun', () => {
|
|
|
142
173
|
}),
|
|
143
174
|
});
|
|
144
175
|
mockRunHarnessTestFile.mockResolvedValue(makeFileRunResult({ jestResult: makeJestResult({ numPassingTests: 3 }) }));
|
|
145
|
-
await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(),
|
|
176
|
+
await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
146
177
|
const payload = finishedPayload;
|
|
147
178
|
expect(payload.summary.passed).toBe(6);
|
|
148
179
|
expect(payload.status).toBe('passed');
|
|
@@ -154,34 +185,124 @@ describe('executeRun', () => {
|
|
|
154
185
|
.mockReturnValueOnce([])
|
|
155
186
|
.mockReturnValueOnce(clientLogs),
|
|
156
187
|
});
|
|
157
|
-
const
|
|
158
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
188
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
189
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
159
190
|
expect(session.flushClientLogs).toHaveBeenCalledTimes(2);
|
|
160
|
-
expect(
|
|
191
|
+
expect(calls).toContainEqual([
|
|
192
|
+
'test-file-success',
|
|
193
|
+
expect.anything(),
|
|
194
|
+
expect.objectContaining({ console: clientLogs }),
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
it('emits test-case events before file success', async () => {
|
|
198
|
+
let testRunnerListener;
|
|
199
|
+
const { emitEvent, calls: emittedEvents } = makeEmitEvent();
|
|
200
|
+
const session = makeSession({
|
|
201
|
+
onTestRunnerEvent: vi.fn((listener) => {
|
|
202
|
+
testRunnerListener = listener;
|
|
203
|
+
return () => undefined;
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
207
|
+
testRunnerListener?.({
|
|
208
|
+
type: 'test-started',
|
|
209
|
+
file: 'example.ts',
|
|
210
|
+
suite: 'suite',
|
|
211
|
+
name: 'works',
|
|
212
|
+
ancestorTitles: ['suite'],
|
|
213
|
+
fullName: 'suite works',
|
|
214
|
+
startedAt: 10,
|
|
215
|
+
declarationMode: 'only',
|
|
216
|
+
});
|
|
217
|
+
testRunnerListener?.({
|
|
218
|
+
type: 'test-finished',
|
|
219
|
+
file: 'example.ts',
|
|
220
|
+
suite: 'suite',
|
|
221
|
+
name: 'works',
|
|
222
|
+
ancestorTitles: ['suite'],
|
|
223
|
+
fullName: 'suite works',
|
|
224
|
+
startedAt: 10,
|
|
225
|
+
declarationMode: 'only',
|
|
226
|
+
duration: 5,
|
|
227
|
+
status: 'passed',
|
|
228
|
+
});
|
|
229
|
+
return makeFileRunResult();
|
|
230
|
+
});
|
|
231
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
232
|
+
expect(emittedEvents).toEqual([
|
|
233
|
+
['test-file-start', expect.anything()],
|
|
234
|
+
[
|
|
235
|
+
'test-case-start',
|
|
236
|
+
'example.ts',
|
|
237
|
+
expect.objectContaining({
|
|
238
|
+
ancestorTitles: ['suite'],
|
|
239
|
+
fullName: 'suite works',
|
|
240
|
+
mode: 'only',
|
|
241
|
+
title: 'works',
|
|
242
|
+
startedAt: 10,
|
|
243
|
+
}),
|
|
244
|
+
],
|
|
245
|
+
[
|
|
246
|
+
'test-case-result',
|
|
247
|
+
'example.ts',
|
|
248
|
+
expect.objectContaining({
|
|
249
|
+
ancestorTitles: ['suite'],
|
|
250
|
+
fullName: 'suite works',
|
|
251
|
+
numPassingAsserts: 1,
|
|
252
|
+
startedAt: 10,
|
|
253
|
+
status: 'passed',
|
|
254
|
+
title: 'works',
|
|
255
|
+
}),
|
|
256
|
+
],
|
|
257
|
+
['test-file-success', expect.anything(), expect.anything()],
|
|
258
|
+
]);
|
|
161
259
|
});
|
|
162
260
|
});
|
|
163
261
|
describe('runtime failures', () => {
|
|
164
262
|
it('passes StartupStallError to onFailure with an empty stack', async () => {
|
|
165
|
-
const
|
|
263
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
166
264
|
const session = makeSession({
|
|
167
265
|
ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1500, 3)),
|
|
168
266
|
});
|
|
169
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
170
|
-
expect(
|
|
267
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
268
|
+
expect(calls).toContainEqual([
|
|
269
|
+
'test-file-failure',
|
|
270
|
+
expect.objectContaining({ path: '/test/example.ts' }),
|
|
271
|
+
expect.objectContaining({ message: expect.stringContaining('1500'), stack: '' }),
|
|
272
|
+
]);
|
|
171
273
|
});
|
|
172
274
|
it('passes DeviceNotRespondingError to onFailure with an empty stack', async () => {
|
|
173
|
-
const
|
|
275
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
174
276
|
const session = makeSession({
|
|
175
277
|
ensureAppReady: vi.fn().mockRejectedValue(new DeviceNotRespondingError('runTests', [])),
|
|
176
278
|
});
|
|
177
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
178
|
-
expect(
|
|
279
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
280
|
+
expect(calls).toContainEqual([
|
|
281
|
+
'test-file-failure',
|
|
282
|
+
expect.anything(),
|
|
283
|
+
expect.objectContaining({ stack: '' }),
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
it('passes AppBridgeDisconnectedError to onFailure with an empty stack', async () => {
|
|
287
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
288
|
+
const session = makeSession({
|
|
289
|
+
ensureAppReady: vi.fn().mockRejectedValue(new AppBridgeDisconnectedError('app-disconnected')),
|
|
290
|
+
});
|
|
291
|
+
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
292
|
+
expect(calls).toContainEqual([
|
|
293
|
+
'test-file-failure',
|
|
294
|
+
expect.anything(),
|
|
295
|
+
expect.objectContaining({
|
|
296
|
+
message: expect.stringContaining('The app bridge disconnected during test execution.'),
|
|
297
|
+
stack: '',
|
|
298
|
+
}),
|
|
299
|
+
]);
|
|
179
300
|
});
|
|
180
301
|
it('calls resetCrashState after a NativeCrashError', async () => {
|
|
181
302
|
const session = makeSession({
|
|
182
303
|
ensureAppReady: vi.fn().mockRejectedValue(new NativeCrashError('example.ts', { phase: 'execution', source: 'polling' })),
|
|
183
304
|
});
|
|
184
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
305
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
185
306
|
expect(session.resetCrashState).toHaveBeenCalled();
|
|
186
307
|
});
|
|
187
308
|
it('counts runtime failures in the summary', async () => {
|
|
@@ -193,7 +314,7 @@ describe('executeRun', () => {
|
|
|
193
314
|
}),
|
|
194
315
|
ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1000, 2)),
|
|
195
316
|
});
|
|
196
|
-
await executeRun(session, [makeTest()], makeWatcher(),
|
|
317
|
+
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
197
318
|
const payload = finishedPayload;
|
|
198
319
|
expect(payload.summary.failed).toBe(1);
|
|
199
320
|
expect(payload.status).toBe('failed');
|
|
@@ -205,10 +326,10 @@ describe('executeRun', () => {
|
|
|
205
326
|
const session = makeSession({
|
|
206
327
|
callHook: vi.fn(async (name) => { hookNames.push(name); }),
|
|
207
328
|
});
|
|
208
|
-
const
|
|
209
|
-
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(true /* interrupted */),
|
|
329
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
330
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(true /* interrupted */), emitEvent, makeGlobalConfig());
|
|
210
331
|
// No test was started; watcher was already interrupted.
|
|
211
|
-
expect(
|
|
332
|
+
expect(calls).not.toContainEqual(['test-file-start', expect.anything()]);
|
|
212
333
|
expect(hookNames).toContain('run:finished');
|
|
213
334
|
});
|
|
214
335
|
});
|
|
@@ -219,11 +340,54 @@ describe('executeRun', () => {
|
|
|
219
340
|
metroPort: 8081,
|
|
220
341
|
resetEnvironmentBetweenTestFiles: true,
|
|
221
342
|
detectNativeCrashes: false,
|
|
343
|
+
runners: [
|
|
344
|
+
{ platformId: 'android', name: 'android' },
|
|
345
|
+
{ platformId: 'ios', name: 'ios' },
|
|
346
|
+
],
|
|
222
347
|
},
|
|
223
348
|
});
|
|
224
|
-
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')], makeWatcher(),
|
|
349
|
+
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
225
350
|
// restartApp should be called for tests 2 and 3, not test 1.
|
|
226
351
|
expect(session.restartApp).toHaveBeenCalledTimes(2);
|
|
227
352
|
});
|
|
228
353
|
});
|
|
354
|
+
describe('platform-specific test files', () => {
|
|
355
|
+
it('skips files for other platforms without running them on the device', async () => {
|
|
356
|
+
const session = makeSession();
|
|
357
|
+
const { emitEvent, calls } = makeEmitEvent();
|
|
358
|
+
await executeRun(session, [
|
|
359
|
+
makeTest('/project/smoke.harness.ts'),
|
|
360
|
+
makeTest('/project/kotlin.android.harness.ts'),
|
|
361
|
+
makeTest('/project/swift.ios.harness.ts'),
|
|
362
|
+
], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
363
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(2);
|
|
364
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledWith(expect.objectContaining({ testPath: '/project/smoke.harness.ts' }));
|
|
365
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledWith(expect.objectContaining({ testPath: '/project/kotlin.android.harness.ts' }));
|
|
366
|
+
expect(session.ensureAppReady).toHaveBeenCalledTimes(2);
|
|
367
|
+
expect(calls).toContainEqual([
|
|
368
|
+
'test-file-success',
|
|
369
|
+
expect.objectContaining({ path: '/project/swift.ios.harness.ts' }),
|
|
370
|
+
expect.objectContaining({ skipped: true }),
|
|
371
|
+
]);
|
|
372
|
+
});
|
|
373
|
+
it('does not restart the app for skipped platform-specific files', async () => {
|
|
374
|
+
const session = makeSession({
|
|
375
|
+
config: {
|
|
376
|
+
metroPort: 8081,
|
|
377
|
+
resetEnvironmentBetweenTestFiles: true,
|
|
378
|
+
detectNativeCrashes: false,
|
|
379
|
+
runners: [
|
|
380
|
+
{ platformId: 'android', name: 'android' },
|
|
381
|
+
{ platformId: 'ios', name: 'ios' },
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
await executeRun(session, [
|
|
386
|
+
makeTest('/project/swift.ios.harness.ts'),
|
|
387
|
+
makeTest('/project/kotlin.android.harness.ts'),
|
|
388
|
+
], makeWatcher(), vi.fn(), makeGlobalConfig());
|
|
389
|
+
expect(session.restartApp).not.toHaveBeenCalled();
|
|
390
|
+
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
229
393
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HarnessPlatformInitOptions, HarnessPlatformRunner } from '@react-native-harness/platforms';
|
|
2
|
+
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
3
|
+
declare global {
|
|
4
|
+
var __HARNESS_TEST_PLATFORM_RUNNER__: HarnessPlatformRunner | undefined;
|
|
5
|
+
}
|
|
6
|
+
export default function mockPlatformRunner(_platformConfig: Record<string, unknown>, _harnessConfig: HarnessConfig, _init: HarnessPlatformInitOptions): Promise<HarnessPlatformRunner>;
|
|
7
|
+
//# sourceMappingURL=mock-platform-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-platform-runner.d.ts","sourceRoot":"","sources":["../../../src/__tests__/fixtures/mock-platform-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,gCAAgC,EAChC,qBAAqB,GACrB,SAAS,CAAC;CACf;AAED,wBAA8B,kBAAkB,CAC9C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,cAAc,EAAE,aAAa,EAC7B,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,qBAAqB,CAAC,CAMhC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export default async function mockPlatformRunner(_platformConfig, _harnessConfig, _init) {
|
|
2
|
+
if (!globalThis.__HARNESS_TEST_PLATFORM_RUNNER__) {
|
|
3
|
+
throw new Error('Missing __HARNESS_TEST_PLATFORM_RUNNER__ test fixture');
|
|
4
|
+
}
|
|
5
|
+
return globalThis.__HARNESS_TEST_PLATFORM_RUNNER__;
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-session.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/harness-session.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { waitForBridgeDisconnectOrTimeout, waitForStartupCrash, } from '../harness-session.js';
|
|
3
|
+
const createConnection = () => ({
|
|
4
|
+
device: {
|
|
5
|
+
platform: 'ios',
|
|
6
|
+
manufacturer: 'Apple',
|
|
7
|
+
model: 'iPhone',
|
|
8
|
+
osVersion: '18.0',
|
|
9
|
+
},
|
|
10
|
+
runTests: vi.fn(),
|
|
11
|
+
});
|
|
12
|
+
const createBridge = (connection) => {
|
|
13
|
+
let currentConnection = connection;
|
|
14
|
+
let disconnectedListener = null;
|
|
15
|
+
return {
|
|
16
|
+
get connection() {
|
|
17
|
+
return currentConnection;
|
|
18
|
+
},
|
|
19
|
+
on: vi.fn((event, listener) => {
|
|
20
|
+
if (event === 'disconnected') {
|
|
21
|
+
disconnectedListener = listener;
|
|
22
|
+
}
|
|
23
|
+
}),
|
|
24
|
+
off: vi.fn((event, listener) => {
|
|
25
|
+
if (event === 'disconnected' && disconnectedListener === listener) {
|
|
26
|
+
disconnectedListener = null;
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
disconnect: () => {
|
|
30
|
+
currentConnection = null;
|
|
31
|
+
disconnectedListener?.();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
describe('waitForBridgeDisconnectOrTimeout', () => {
|
|
36
|
+
it('returns true when the bridge disconnects before the timeout', async () => {
|
|
37
|
+
const connection = createConnection();
|
|
38
|
+
const bridge = createBridge(connection);
|
|
39
|
+
const waitPromise = waitForBridgeDisconnectOrTimeout({
|
|
40
|
+
bridge,
|
|
41
|
+
connection,
|
|
42
|
+
timeoutMs: 50,
|
|
43
|
+
});
|
|
44
|
+
bridge.disconnect();
|
|
45
|
+
await expect(waitPromise).resolves.toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('returns false when the bridge stays connected through the timeout', async () => {
|
|
48
|
+
const connection = createConnection();
|
|
49
|
+
const bridge = createBridge(connection);
|
|
50
|
+
await expect(waitForBridgeDisconnectOrTimeout({
|
|
51
|
+
bridge,
|
|
52
|
+
connection,
|
|
53
|
+
timeoutMs: 10,
|
|
54
|
+
})).resolves.toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('waitForStartupCrash', () => {
|
|
58
|
+
it('does not install a startup crash watch when native crash detection is disabled', async () => {
|
|
59
|
+
const watch = vi.fn();
|
|
60
|
+
const crashMonitor = {
|
|
61
|
+
watch,
|
|
62
|
+
};
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const waitPromise = waitForStartupCrash({
|
|
65
|
+
crashMonitor,
|
|
66
|
+
detectNativeCrashes: false,
|
|
67
|
+
testFilePath: '/test.harness.ts',
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
controller.abort(new DOMException('Aborted', 'AbortError'));
|
|
71
|
+
await expect(waitPromise).rejects.toThrow('Aborted');
|
|
72
|
+
expect(watch).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -8,6 +8,7 @@ const resolveUndefined = async () => undefined;
|
|
|
8
8
|
const mockSession = {
|
|
9
9
|
config: { metroPort: 8081 },
|
|
10
10
|
context: {},
|
|
11
|
+
onTestRunnerEvent: vi.fn(() => () => undefined),
|
|
11
12
|
ensureAppReady: vi.fn(resolveUndefined),
|
|
12
13
|
runTestFile: vi.fn(async () => ({
|
|
13
14
|
name: '',
|
|
@@ -41,40 +42,88 @@ const makeTest = () => ({
|
|
|
41
42
|
context: { config: {} },
|
|
42
43
|
});
|
|
43
44
|
const makeGlobalConfig = (overrides = {}) => ({ rootDir: '/project', watch: false, watchAll: false, collectCoverage: false, ...overrides });
|
|
45
|
+
const typedMockExecuteRun = mockExecuteRun;
|
|
46
|
+
const makeTestCaseStartPayload = () => [
|
|
47
|
+
'/test/example.ts',
|
|
48
|
+
{
|
|
49
|
+
ancestorTitles: [],
|
|
50
|
+
fullName: 'suite case',
|
|
51
|
+
mode: undefined,
|
|
52
|
+
title: 'case',
|
|
53
|
+
startedAt: 1,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const makeTestCaseResultPayload = () => [
|
|
57
|
+
'/test/example.ts',
|
|
58
|
+
{
|
|
59
|
+
ancestorTitles: [],
|
|
60
|
+
duration: 1,
|
|
61
|
+
failureDetails: [],
|
|
62
|
+
failureMessages: [],
|
|
63
|
+
fullName: 'suite case',
|
|
64
|
+
numPassingAsserts: 1,
|
|
65
|
+
startedAt: 1,
|
|
66
|
+
status: 'passed',
|
|
67
|
+
title: 'case',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
44
70
|
// ---------------------------------------------------------------------------
|
|
45
71
|
// Tests
|
|
46
72
|
// ---------------------------------------------------------------------------
|
|
47
73
|
beforeEach(() => {
|
|
48
74
|
vi.clearAllMocks();
|
|
49
75
|
mockSession.dispose.mockResolvedValue(undefined);
|
|
50
|
-
|
|
76
|
+
typedMockExecuteRun.mockResolvedValue(undefined);
|
|
51
77
|
mockCreateHarnessSession.mockResolvedValue(mockSession);
|
|
52
78
|
});
|
|
53
79
|
describe('JestHarness', () => {
|
|
54
80
|
describe('session lifecycle', () => {
|
|
55
81
|
it('creates a session on the first run', async () => {
|
|
56
82
|
const runner = new JestHarness(makeGlobalConfig());
|
|
57
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
83
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
58
84
|
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
59
85
|
});
|
|
60
86
|
it('reuses the session across runs in watch mode', async () => {
|
|
61
87
|
const runner = new JestHarness(makeGlobalConfig({ watch: true }));
|
|
62
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
63
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
88
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
89
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
64
90
|
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
65
91
|
expect(mockSession.dispose).not.toHaveBeenCalled();
|
|
66
92
|
});
|
|
67
93
|
it('disposes the session after each run in normal mode', async () => {
|
|
68
94
|
const runner = new JestHarness(makeGlobalConfig());
|
|
69
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
95
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
70
96
|
expect(mockSession.dispose).toHaveBeenCalledOnce();
|
|
71
97
|
});
|
|
72
98
|
it('creates a fresh session for each run in normal mode', async () => {
|
|
73
99
|
const runner = new JestHarness(makeGlobalConfig());
|
|
74
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
75
|
-
await runner.runTests([makeTest()], makeWatcher(),
|
|
100
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
101
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
76
102
|
expect(mockCreateHarnessSession).toHaveBeenCalledTimes(2);
|
|
77
103
|
});
|
|
104
|
+
it('emits registered Jest events', async () => {
|
|
105
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
106
|
+
const listener = vi.fn();
|
|
107
|
+
const eventPayload = makeTestCaseStartPayload();
|
|
108
|
+
runner.on('test-case-start', listener);
|
|
109
|
+
typedMockExecuteRun.mockImplementationOnce(async (_session, _tests, _watcher, emitEvent) => {
|
|
110
|
+
await emitEvent('test-case-start', ...eventPayload);
|
|
111
|
+
});
|
|
112
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
113
|
+
expect(listener).toHaveBeenCalledWith(eventPayload);
|
|
114
|
+
});
|
|
115
|
+
it('stops emitting after listener unsubscribe', async () => {
|
|
116
|
+
const runner = new JestHarness(makeGlobalConfig());
|
|
117
|
+
const listener = vi.fn();
|
|
118
|
+
const unsubscribe = runner.on('test-case-result', listener);
|
|
119
|
+
const eventPayload = makeTestCaseResultPayload();
|
|
120
|
+
unsubscribe();
|
|
121
|
+
typedMockExecuteRun.mockImplementationOnce(async (_session, _tests, _watcher, emitEvent) => {
|
|
122
|
+
await emitEvent('test-case-result', ...eventPayload);
|
|
123
|
+
});
|
|
124
|
+
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
125
|
+
expect(listener).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
78
127
|
});
|
|
79
128
|
describe('error handling', () => {
|
|
80
129
|
it('converts HarnessError into a formatted string before throwing', async () => {
|
|
@@ -83,17 +132,17 @@ describe('JestHarness', () => {
|
|
|
83
132
|
}
|
|
84
133
|
mockCreateHarnessSession.mockRejectedValue(new TestHarnessError());
|
|
85
134
|
const runner = new JestHarness(makeGlobalConfig());
|
|
86
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
135
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), makeOptions())).rejects.toBeTypeOf('string');
|
|
87
136
|
});
|
|
88
137
|
it('propagates non-HarnessError exceptions as-is', async () => {
|
|
89
138
|
const cause = new TypeError('unexpected');
|
|
90
139
|
mockCreateHarnessSession.mockRejectedValue(cause);
|
|
91
140
|
const runner = new JestHarness(makeGlobalConfig());
|
|
92
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
141
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), makeOptions())).rejects.toBe(cause);
|
|
93
142
|
});
|
|
94
143
|
it('throws when called without serial flag', async () => {
|
|
95
144
|
const runner = new JestHarness(makeGlobalConfig());
|
|
96
|
-
await expect(runner.runTests([makeTest()], makeWatcher(),
|
|
145
|
+
await expect(runner.runTests([makeTest()], makeWatcher(), { serial: false })).rejects.toThrow('Parallel test running is not supported');
|
|
97
146
|
});
|
|
98
147
|
});
|
|
99
148
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-file-platform-filter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/test-file-platform-filter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createPlatformSkippedTestResult, getHarnessTestFilePlatform, shouldRunHarnessTestFile, } from '../test-file-platform-filter.js';
|
|
3
|
+
const knownPlatformIds = new Set(['android', 'ios', 'web', 'vega']);
|
|
4
|
+
describe('getHarnessTestFilePlatform', () => {
|
|
5
|
+
it('returns null for shared harness test files', () => {
|
|
6
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.ts', knownPlatformIds)).toBeNull();
|
|
7
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.tsx', knownPlatformIds)).toBeNull();
|
|
8
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.mjs', knownPlatformIds)).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
it('returns the platform id for platform-specific harness test files', () => {
|
|
11
|
+
expect(getHarnessTestFilePlatform('/tests/kotlin.android.harness.ts', knownPlatformIds)).toBe('android');
|
|
12
|
+
expect(getHarnessTestFilePlatform('/tests/swift.ios.harness.ts', knownPlatformIds)).toBe('ios');
|
|
13
|
+
expect(getHarnessTestFilePlatform('/tests/browser.web.harness.ts', knownPlatformIds)).toBe('web');
|
|
14
|
+
});
|
|
15
|
+
it('returns null when the segment before .harness is not a known platform id', () => {
|
|
16
|
+
expect(getHarnessTestFilePlatform('/tests/smoke.harness.ts', knownPlatformIds)).toBeNull();
|
|
17
|
+
expect(getHarnessTestFilePlatform('/tests/custom.foo.harness.ts', knownPlatformIds)).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('shouldRunHarnessTestFile', () => {
|
|
21
|
+
it('runs shared harness test files on every platform', () => {
|
|
22
|
+
expect(shouldRunHarnessTestFile('/tests/smoke.harness.ts', 'android', knownPlatformIds)).toBe(true);
|
|
23
|
+
expect(shouldRunHarnessTestFile('/tests/smoke.harness.ts', 'ios', knownPlatformIds)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('runs platform-specific files only on the matching platform', () => {
|
|
26
|
+
expect(shouldRunHarnessTestFile('/tests/kotlin.android.harness.ts', 'android', knownPlatformIds)).toBe(true);
|
|
27
|
+
expect(shouldRunHarnessTestFile('/tests/kotlin.android.harness.ts', 'ios', knownPlatformIds)).toBe(false);
|
|
28
|
+
expect(shouldRunHarnessTestFile('/tests/swift.ios.harness.ts', 'ios', knownPlatformIds)).toBe(true);
|
|
29
|
+
expect(shouldRunHarnessTestFile('/tests/swift.ios.harness.ts', 'android', knownPlatformIds)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('createPlatformSkippedTestResult', () => {
|
|
33
|
+
it('marks the entire test file as skipped', () => {
|
|
34
|
+
const result = createPlatformSkippedTestResult('/tests/swift.ios.harness.ts');
|
|
35
|
+
expect(result.skipped).toBe(true);
|
|
36
|
+
expect(result.numPassingTests).toBe(0);
|
|
37
|
+
expect(result.numFailingTests).toBe(0);
|
|
38
|
+
expect(result.numPendingTests).toBe(1);
|
|
39
|
+
expect(result.testResults).toHaveLength(1);
|
|
40
|
+
expect(result.testResults[0]).toEqual(expect.objectContaining({
|
|
41
|
+
status: 'skipped',
|
|
42
|
+
title: 'swift.ios.harness.ts',
|
|
43
|
+
fullName: 'swift.ios.harness.ts',
|
|
44
|
+
}));
|
|
45
|
+
expect(result.testFilePath).toBe('/tests/swift.ios.harness.ts');
|
|
46
|
+
});
|
|
47
|
+
});
|
package/dist/crash-monitor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type AppSession } from '@react-native-harness/platforms';
|
|
2
2
|
import { type NativeCrashPhase } from './errors.js';
|
|
3
3
|
export declare class CrashWatchCancelledError extends Error {
|
|
4
4
|
constructor();
|
|
@@ -13,11 +13,12 @@ export type CrashMonitor = {
|
|
|
13
13
|
stop: () => Promise<void>;
|
|
14
14
|
start: () => Promise<void>;
|
|
15
15
|
reset: () => void;
|
|
16
|
+
setAppSession: (session: AppSession | null) => void;
|
|
17
|
+
handleBridgeDisconnect: () => void;
|
|
16
18
|
dispose: () => Promise<void>;
|
|
17
19
|
};
|
|
18
20
|
export type CrashMonitorOptions = {
|
|
19
|
-
|
|
20
|
-
platformRunner: HarnessPlatformRunner;
|
|
21
|
+
appSession?: AppSession | null;
|
|
21
22
|
};
|
|
22
|
-
export declare const createCrashMonitor: ({
|
|
23
|
+
export declare const createCrashMonitor: ({ appSession: initialAppSession, }?: CrashMonitorOptions) => CrashMonitor;
|
|
23
24
|
//# sourceMappingURL=crash-monitor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAKhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAC;AAOrB,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,UAAU,CAAC;IACrE,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IACpD,sBAAsB,EAAE,MAAM,IAAI,CAAC;IACnC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC,CAAC;AA0EF,eAAO,MAAM,kBAAkB,GAAI,qCAEhC,mBAAwB,KAAG,YAyL7B,CAAC"}
|