@react-native-harness/jest 1.3.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__/harness-session.test.js +63 -239
- 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
|
});
|
|
@@ -1,250 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const createHarnessBridgeMock = vi.hoisted(() => vi.fn());
|
|
10
|
-
const createCrashMonitorMock = vi.hoisted(() => vi.fn());
|
|
11
|
-
vi.mock('@react-native-harness/plugins', async () => {
|
|
12
|
-
const actual = await vi.importActual('@react-native-harness/plugins');
|
|
13
|
-
return {
|
|
14
|
-
...actual,
|
|
15
|
-
createHarnessPluginManager: vi.fn(() => ({
|
|
16
|
-
callHook: pluginManagerMocks.callHook,
|
|
17
|
-
})),
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
vi.mock('@react-native-harness/config', async () => {
|
|
21
|
-
const actual = await vi.importActual('@react-native-harness/config');
|
|
22
|
-
return {
|
|
23
|
-
...actual,
|
|
24
|
-
getConfig: getConfigMock,
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
vi.mock('../metro-port.js', async () => {
|
|
28
|
-
const actual = await vi.importActual('../metro-port.js');
|
|
29
|
-
return {
|
|
30
|
-
...actual,
|
|
31
|
-
resolveHarnessMetroPort: resolveHarnessMetroPortMock,
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
vi.mock('@react-native-harness/bundler-metro', async () => {
|
|
35
|
-
const actual = await vi.importActual('@react-native-harness/bundler-metro');
|
|
36
|
-
return {
|
|
37
|
-
...actual,
|
|
38
|
-
getMetroInstance: getMetroInstanceMock,
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
vi.mock('@react-native-harness/bridge/server', async () => {
|
|
42
|
-
const actual = await vi.importActual('@react-native-harness/bridge/server');
|
|
43
|
-
return {
|
|
44
|
-
...actual,
|
|
45
|
-
createHarnessBridge: createHarnessBridgeMock,
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
vi.mock('../crash-monitor.js', async () => {
|
|
49
|
-
const actual = await vi.importActual('../crash-monitor.js');
|
|
50
|
-
return {
|
|
51
|
-
...actual,
|
|
52
|
-
createCrashMonitor: createCrashMonitorMock,
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
const createAppMonitor = () => ({
|
|
56
|
-
start: vi.fn(async () => undefined),
|
|
57
|
-
stop: vi.fn(async () => undefined),
|
|
58
|
-
dispose: vi.fn(async () => undefined),
|
|
59
|
-
addListener: vi.fn(),
|
|
60
|
-
removeListener: vi.fn(),
|
|
61
|
-
});
|
|
62
|
-
const makeBridge = () => ({
|
|
63
|
-
ws: {},
|
|
64
|
-
connection: null,
|
|
65
|
-
on: vi.fn(),
|
|
66
|
-
off: vi.fn(),
|
|
67
|
-
nextConnection: vi.fn(),
|
|
68
|
-
dispose: vi.fn(async () => undefined),
|
|
69
|
-
});
|
|
70
|
-
const makeMetroInstance = () => ({
|
|
71
|
-
events: {
|
|
72
|
-
addListener: vi.fn(),
|
|
73
|
-
removeListener: vi.fn(),
|
|
74
|
-
},
|
|
75
|
-
waitUntilHealthy: vi.fn(async () => 'packager-status:running'),
|
|
76
|
-
prewarm: vi.fn(async () => false),
|
|
77
|
-
dispose: vi.fn(async () => undefined),
|
|
78
|
-
});
|
|
79
|
-
const makeLockManager = () => ({
|
|
80
|
-
acquire: vi.fn(async () => ({
|
|
81
|
-
release: vi.fn(async () => undefined),
|
|
82
|
-
})),
|
|
83
|
-
});
|
|
84
|
-
const makeCrashMonitor = () => ({
|
|
85
|
-
watch: vi.fn(),
|
|
86
|
-
isAlive: vi.fn(() => false),
|
|
87
|
-
stop: vi.fn(async () => undefined),
|
|
88
|
-
start: vi.fn(async () => undefined),
|
|
89
|
-
reset: vi.fn(),
|
|
90
|
-
dispose: vi.fn(async () => undefined),
|
|
91
|
-
});
|
|
92
|
-
const makeDeviceState = () => ({
|
|
93
|
-
permissions: {
|
|
94
|
-
apply: vi.fn(async () => ({ mutation: null })),
|
|
95
|
-
revert: vi.fn(),
|
|
96
|
-
resetOutstanding: vi.fn(async () => undefined),
|
|
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',
|
|
97
9
|
},
|
|
10
|
+
runTests: vi.fn(),
|
|
98
11
|
});
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
createAppMonitor: vi.fn(() => createAppMonitor()),
|
|
106
|
-
...overrides,
|
|
107
|
-
});
|
|
108
|
-
const globalConfig = {
|
|
109
|
-
rootDir: '/project',
|
|
110
|
-
watch: false,
|
|
111
|
-
watchAll: false,
|
|
112
|
-
collectCoverage: false,
|
|
113
|
-
};
|
|
114
|
-
const harnessConfig = {
|
|
115
|
-
entryPoint: 'index.js',
|
|
116
|
-
appRegistryComponentName: 'App',
|
|
117
|
-
runners: [
|
|
118
|
-
{
|
|
119
|
-
name: 'ios',
|
|
120
|
-
platformId: 'ios',
|
|
121
|
-
runner: './__tests__/fixtures/mock-platform-runner.ts',
|
|
122
|
-
config: {},
|
|
12
|
+
const createBridge = (connection) => {
|
|
13
|
+
let currentConnection = connection;
|
|
14
|
+
let disconnectedListener = null;
|
|
15
|
+
return {
|
|
16
|
+
get connection() {
|
|
17
|
+
return currentConnection;
|
|
123
18
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
};
|
|
139
34
|
};
|
|
140
|
-
describe('
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
metroInstance = makeMetroInstance();
|
|
149
|
-
crashMonitor = makeCrashMonitor();
|
|
150
|
-
lockManager = makeLockManager();
|
|
151
|
-
getConfigMock.mockResolvedValue({ config: harnessConfig });
|
|
152
|
-
resolveHarnessMetroPortMock.mockResolvedValue({
|
|
153
|
-
config: harnessConfig,
|
|
154
|
-
initialMetroPort: 8081,
|
|
155
|
-
didFallback: false,
|
|
156
|
-
metroPortLease: { release: vi.fn(async () => undefined) },
|
|
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,
|
|
157
43
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
createCrashMonitorMock.mockReturnValue(crashMonitor);
|
|
44
|
+
bridge.disconnect();
|
|
45
|
+
await expect(waitPromise).resolves.toBe(true);
|
|
161
46
|
});
|
|
162
|
-
|
|
163
|
-
|
|
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);
|
|
164
55
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
bridge.connection = {};
|
|
172
|
-
crashMonitor.isAlive.mockReturnValue(true);
|
|
173
|
-
globalThis.__HARNESS_TEST_PLATFORM_RUNNER__ = platformRunner;
|
|
174
|
-
const session = await createHarnessSession(globalConfig, { lockManager });
|
|
175
|
-
const callOrder = [];
|
|
176
|
-
deviceState.permissions.resetOutstanding.mockImplementation(async () => {
|
|
177
|
-
callOrder.push('resetOutstanding');
|
|
178
|
-
});
|
|
179
|
-
platformRunner.stopApp = vi.fn(async () => {
|
|
180
|
-
callOrder.push('stopApp');
|
|
181
|
-
});
|
|
182
|
-
await session.restartApp('/test/example.ts');
|
|
183
|
-
expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
|
|
184
|
-
expect(deviceState.permissions.apply).not.toHaveBeenCalled();
|
|
185
|
-
expect(callOrder).toEqual(['resetOutstanding', 'stopApp']);
|
|
186
|
-
await session.dispose();
|
|
187
|
-
});
|
|
188
|
-
it('reapplies configured permissions after cleanup before restart between test files', async () => {
|
|
189
|
-
const deviceState = makeDeviceState();
|
|
190
|
-
const platformRunner = makePlatformRunner({
|
|
191
|
-
deviceState,
|
|
192
|
-
isAppRunning: vi.fn(async () => true),
|
|
193
|
-
});
|
|
194
|
-
bridge.connection = {};
|
|
195
|
-
crashMonitor.isAlive.mockReturnValue(true);
|
|
196
|
-
globalThis.__HARNESS_TEST_PLATFORM_RUNNER__ = platformRunner;
|
|
197
|
-
const permissionsEnabledConfig = {
|
|
198
|
-
...harnessConfig,
|
|
199
|
-
permissions: true,
|
|
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,
|
|
200
62
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
208
|
-
const session = await createHarnessSession(globalConfig, { lockManager });
|
|
209
|
-
const callOrder = [];
|
|
210
|
-
deviceState.permissions.apply.mockClear();
|
|
211
|
-
deviceState.permissions.resetOutstanding.mockImplementation(async () => {
|
|
212
|
-
callOrder.push('resetOutstanding');
|
|
213
|
-
});
|
|
214
|
-
deviceState.permissions.apply.mockImplementation(async () => {
|
|
215
|
-
callOrder.push('applyConfiguredPermissions');
|
|
216
|
-
return { mutation: null };
|
|
217
|
-
});
|
|
218
|
-
platformRunner.stopApp = vi.fn(async () => {
|
|
219
|
-
callOrder.push('stopApp');
|
|
220
|
-
});
|
|
221
|
-
await session.restartApp('/test/example.ts');
|
|
222
|
-
expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
|
|
223
|
-
expect(deviceState.permissions.apply).toHaveBeenCalledWith({
|
|
224
|
-
kind: 'permission-all',
|
|
225
|
-
decision: 'grant',
|
|
226
|
-
});
|
|
227
|
-
expect(callOrder).toEqual([
|
|
228
|
-
'resetOutstanding',
|
|
229
|
-
'applyConfiguredPermissions',
|
|
230
|
-
'stopApp',
|
|
231
|
-
]);
|
|
232
|
-
await session.dispose();
|
|
233
|
-
});
|
|
234
|
-
it('resets outstanding permission mutations during dispose', async () => {
|
|
235
|
-
const deviceState = makeDeviceState();
|
|
236
|
-
const callOrder = [];
|
|
237
|
-
const platformRunner = makePlatformRunner({ deviceState });
|
|
238
|
-
deviceState.permissions.resetOutstanding.mockImplementation(async () => {
|
|
239
|
-
callOrder.push('resetOutstanding');
|
|
240
|
-
});
|
|
241
|
-
platformRunner.dispose = vi.fn(async () => {
|
|
242
|
-
callOrder.push('dispose');
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const waitPromise = waitForStartupCrash({
|
|
65
|
+
crashMonitor,
|
|
66
|
+
detectNativeCrashes: false,
|
|
67
|
+
testFilePath: '/test.harness.ts',
|
|
68
|
+
signal: controller.signal,
|
|
243
69
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
|
|
248
|
-
expect(callOrder).toEqual(['resetOutstanding', 'dispose']);
|
|
70
|
+
controller.abort(new DOMException('Aborted', 'AbortError'));
|
|
71
|
+
await expect(waitPromise).rejects.toThrow('Aborted');
|
|
72
|
+
expect(watch).not.toHaveBeenCalled();
|
|
249
73
|
});
|
|
250
74
|
});
|