@react-native-harness/jest 1.4.0-rc.1 → 1.4.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/crash-monitor.d.ts +7 -0
- package/dist/crash-monitor.d.ts.map +1 -1
- package/dist/crash-monitor.js +19 -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/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +121 -69
- 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 +26 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +260 -87
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +21 -4
- package/package.json +8 -7
- package/src/crash-monitor.ts +28 -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/execute-run.ts +136 -77
- package/src/format-harness-error.ts +73 -0
- package/src/harness-session.ts +369 -131
- package/src/run.ts +23 -4
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
- package/dist/__tests__/bridge.test.d.ts +0 -2
- package/dist/__tests__/bridge.test.d.ts.map +0 -1
- package/dist/__tests__/bridge.test.js +0 -163
- package/dist/__tests__/client-log-handler.test.d.ts +0 -2
- package/dist/__tests__/client-log-handler.test.d.ts.map +0 -1
- package/dist/__tests__/client-log-handler.test.js +0 -306
- package/dist/__tests__/crash-monitor.test.d.ts +0 -2
- package/dist/__tests__/crash-monitor.test.d.ts.map +0 -1
- package/dist/__tests__/crash-monitor.test.js +0 -166
- package/dist/__tests__/crash-supervisor.test.d.ts +0 -2
- package/dist/__tests__/crash-supervisor.test.d.ts.map +0 -1
- package/dist/__tests__/crash-supervisor.test.js +0 -151
- package/dist/__tests__/errors.test.d.ts +0 -2
- package/dist/__tests__/errors.test.d.ts.map +0 -1
- package/dist/__tests__/errors.test.js +0 -98
- package/dist/__tests__/execute-run.test.d.ts +0 -2
- package/dist/__tests__/execute-run.test.d.ts.map +0 -1
- package/dist/__tests__/execute-run.test.js +0 -393
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts +0 -7
- package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +0 -1
- package/dist/__tests__/fixtures/mock-platform-runner.js +0 -6
- package/dist/__tests__/harness-cache.test.d.ts +0 -2
- package/dist/__tests__/harness-cache.test.d.ts.map +0 -1
- package/dist/__tests__/harness-cache.test.js +0 -51
- package/dist/__tests__/harness-session.test.d.ts +0 -2
- package/dist/__tests__/harness-session.test.d.ts.map +0 -1
- package/dist/__tests__/harness-session.test.js +0 -74
- package/dist/__tests__/harness.test.d.ts +0 -2
- package/dist/__tests__/harness.test.d.ts.map +0 -1
- package/dist/__tests__/harness.test.js +0 -636
- package/dist/__tests__/hook-queue.test.d.ts +0 -2
- package/dist/__tests__/hook-queue.test.d.ts.map +0 -1
- package/dist/__tests__/hook-queue.test.js +0 -56
- package/dist/__tests__/index.test.d.ts +0 -2
- package/dist/__tests__/index.test.d.ts.map +0 -1
- package/dist/__tests__/index.test.js +0 -148
- package/dist/__tests__/metro-port.test.d.ts +0 -2
- package/dist/__tests__/metro-port.test.d.ts.map +0 -1
- package/dist/__tests__/metro-port.test.js +0 -52
- package/dist/__tests__/resource-lock.test.d.ts +0 -2
- package/dist/__tests__/resource-lock.test.d.ts.map +0 -1
- package/dist/__tests__/resource-lock.test.js +0 -138
- package/dist/__tests__/test-file-platform-filter.test.d.ts +0 -2
- package/dist/__tests__/test-file-platform-filter.test.d.ts.map +0 -1
- package/dist/__tests__/test-file-platform-filter.test.js +0 -47
- package/dist/cli-args.test.d.ts +0 -2
- package/dist/cli-args.test.d.ts.map +0 -1
- package/dist/cli-args.test.js +0 -1
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
- package/src/__tests__/bridge.test.ts +0 -219
- package/src/__tests__/client-log-handler.test.ts +0 -364
- package/src/__tests__/crash-monitor.test.ts +0 -239
- package/src/__tests__/errors.test.ts +0 -157
- package/src/__tests__/execute-run.test.ts +0 -526
- package/src/__tests__/harness-session.test.ts +0 -93
- package/src/__tests__/hook-queue.test.ts +0 -77
- package/src/__tests__/index.test.ts +0 -219
- package/src/__tests__/metro-port.test.ts +0 -61
- package/src/__tests__/resource-lock.test.ts +0 -179
- package/src/__tests__/test-file-platform-filter.test.ts +0 -83
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import type { AppConnection } from '@react-native-harness/bridge/server';
|
|
3
|
-
import {
|
|
4
|
-
waitForBridgeDisconnectOrTimeout,
|
|
5
|
-
waitForStartupCrash,
|
|
6
|
-
} from '../harness-session.js';
|
|
7
|
-
import type { CrashMonitor } from '../crash-monitor.js';
|
|
8
|
-
|
|
9
|
-
const createConnection = (): AppConnection => ({
|
|
10
|
-
device: {
|
|
11
|
-
platform: 'ios',
|
|
12
|
-
manufacturer: 'Apple',
|
|
13
|
-
model: 'iPhone',
|
|
14
|
-
osVersion: '18.0',
|
|
15
|
-
},
|
|
16
|
-
runTests: vi.fn(),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const createBridge = (connection: AppConnection | null) => {
|
|
20
|
-
let currentConnection = connection;
|
|
21
|
-
let disconnectedListener: (() => void) | null = null;
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
get connection() {
|
|
25
|
-
return currentConnection;
|
|
26
|
-
},
|
|
27
|
-
on: vi.fn((event: 'disconnected', listener: () => void) => {
|
|
28
|
-
if (event === 'disconnected') {
|
|
29
|
-
disconnectedListener = listener;
|
|
30
|
-
}
|
|
31
|
-
}),
|
|
32
|
-
off: vi.fn((event: 'disconnected', listener: () => void) => {
|
|
33
|
-
if (event === 'disconnected' && disconnectedListener === listener) {
|
|
34
|
-
disconnectedListener = null;
|
|
35
|
-
}
|
|
36
|
-
}),
|
|
37
|
-
disconnect: () => {
|
|
38
|
-
currentConnection = null;
|
|
39
|
-
disconnectedListener?.();
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
describe('waitForBridgeDisconnectOrTimeout', () => {
|
|
45
|
-
it('returns true when the bridge disconnects before the timeout', async () => {
|
|
46
|
-
const connection = createConnection();
|
|
47
|
-
const bridge = createBridge(connection);
|
|
48
|
-
|
|
49
|
-
const waitPromise = waitForBridgeDisconnectOrTimeout({
|
|
50
|
-
bridge,
|
|
51
|
-
connection,
|
|
52
|
-
timeoutMs: 50,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
bridge.disconnect();
|
|
56
|
-
|
|
57
|
-
await expect(waitPromise).resolves.toBe(true);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('returns false when the bridge stays connected through the timeout', async () => {
|
|
61
|
-
const connection = createConnection();
|
|
62
|
-
const bridge = createBridge(connection);
|
|
63
|
-
|
|
64
|
-
await expect(
|
|
65
|
-
waitForBridgeDisconnectOrTimeout({
|
|
66
|
-
bridge,
|
|
67
|
-
connection,
|
|
68
|
-
timeoutMs: 10,
|
|
69
|
-
}),
|
|
70
|
-
).resolves.toBe(false);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('waitForStartupCrash', () => {
|
|
75
|
-
it('does not install a startup crash watch when native crash detection is disabled', async () => {
|
|
76
|
-
const watch = vi.fn();
|
|
77
|
-
const crashMonitor = {
|
|
78
|
-
watch,
|
|
79
|
-
} as unknown as CrashMonitor;
|
|
80
|
-
const controller = new AbortController();
|
|
81
|
-
const waitPromise = waitForStartupCrash({
|
|
82
|
-
crashMonitor,
|
|
83
|
-
detectNativeCrashes: false,
|
|
84
|
-
testFilePath: '/test.harness.ts',
|
|
85
|
-
signal: controller.signal,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
controller.abort(new DOMException('Aborted', 'AbortError'));
|
|
89
|
-
|
|
90
|
-
await expect(waitPromise).rejects.toThrow('Aborted');
|
|
91
|
-
expect(watch).not.toHaveBeenCalled();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { createHookQueue } from '../hook-queue.js';
|
|
3
|
-
|
|
4
|
-
describe('createHookQueue', () => {
|
|
5
|
-
it('executes scheduled work in FIFO order', async () => {
|
|
6
|
-
const queue = createHookQueue();
|
|
7
|
-
const order: number[] = [];
|
|
8
|
-
|
|
9
|
-
queue.schedule(async () => { order.push(1); });
|
|
10
|
-
queue.schedule(async () => { order.push(2); });
|
|
11
|
-
queue.schedule(async () => { order.push(3); });
|
|
12
|
-
|
|
13
|
-
await queue.drain();
|
|
14
|
-
|
|
15
|
-
expect(order).toEqual([1, 2, 3]);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('drain awaits async work before resolving', async () => {
|
|
19
|
-
const queue = createHookQueue();
|
|
20
|
-
let resolved = false;
|
|
21
|
-
|
|
22
|
-
queue.schedule(async () => {
|
|
23
|
-
await new Promise<void>((r) => setTimeout(r, 20));
|
|
24
|
-
resolved = true;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
expect(resolved).toBe(false);
|
|
28
|
-
await queue.drain();
|
|
29
|
-
expect(resolved).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('drain resolves immediately when nothing is scheduled', async () => {
|
|
33
|
-
const queue = createHookQueue();
|
|
34
|
-
await expect(queue.drain()).resolves.toBeUndefined();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('captures the first error and re-throws it on drain', async () => {
|
|
38
|
-
const queue = createHookQueue();
|
|
39
|
-
const err = new Error('hook failed');
|
|
40
|
-
|
|
41
|
-
queue.schedule(async () => { throw err; });
|
|
42
|
-
|
|
43
|
-
await expect(queue.drain()).rejects.toBe(err);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('continues executing subsequent hooks after one throws', async () => {
|
|
47
|
-
const queue = createHookQueue();
|
|
48
|
-
const ran: string[] = [];
|
|
49
|
-
|
|
50
|
-
queue.schedule(async () => { throw new Error('first'); });
|
|
51
|
-
queue.schedule(async () => { ran.push('second'); });
|
|
52
|
-
queue.schedule(async () => { ran.push('third'); });
|
|
53
|
-
|
|
54
|
-
await expect(queue.drain()).rejects.toThrow('first');
|
|
55
|
-
|
|
56
|
-
expect(ran).toEqual(['second', 'third']);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('error is cleared after drain re-throws it', async () => {
|
|
60
|
-
const queue = createHookQueue();
|
|
61
|
-
|
|
62
|
-
queue.schedule(async () => { throw new Error('once'); });
|
|
63
|
-
await expect(queue.drain()).rejects.toThrow('once');
|
|
64
|
-
|
|
65
|
-
// Second drain should see no accumulated error.
|
|
66
|
-
await expect(queue.drain()).resolves.toBeUndefined();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('surfaces only the first error when multiple hooks throw', async () => {
|
|
70
|
-
const queue = createHookQueue();
|
|
71
|
-
|
|
72
|
-
queue.schedule(async () => { throw new Error('first'); });
|
|
73
|
-
queue.schedule(async () => { throw new Error('second'); });
|
|
74
|
-
|
|
75
|
-
await expect(queue.drain()).rejects.toThrow('first');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
-
import type { Config, Test, TestEvents, TestRunnerOptions, TestWatcher } from 'jest-runner';
|
|
3
|
-
import type { TestSuiteResult } from '@react-native-harness/bridge';
|
|
4
|
-
import type { HarnessSession } from '../harness-session.js';
|
|
5
|
-
import type { executeRun as ExecuteRun } from '../execute-run.js';
|
|
6
|
-
import { HarnessError } from '@react-native-harness/tools';
|
|
7
|
-
import JestHarness from '../index.js';
|
|
8
|
-
|
|
9
|
-
const resolveUndefined = async () => undefined;
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Module mocks
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
const mockSession: HarnessSession = {
|
|
16
|
-
config: { metroPort: 8081 } as HarnessSession['config'],
|
|
17
|
-
context: {} as HarnessSession['context'],
|
|
18
|
-
onTestRunnerEvent: vi.fn(() => () => undefined),
|
|
19
|
-
ensureAppReady: vi.fn(resolveUndefined),
|
|
20
|
-
runTestFile: vi.fn(async (): Promise<TestSuiteResult> => ({
|
|
21
|
-
name: '',
|
|
22
|
-
tests: [],
|
|
23
|
-
suites: [],
|
|
24
|
-
status: 'passed',
|
|
25
|
-
duration: 0,
|
|
26
|
-
})),
|
|
27
|
-
restartApp: vi.fn(resolveUndefined),
|
|
28
|
-
resetCrashState: vi.fn(),
|
|
29
|
-
flushClientLogs: vi.fn(() => []),
|
|
30
|
-
callHook: vi.fn(resolveUndefined),
|
|
31
|
-
setRunState: vi.fn(),
|
|
32
|
-
dispose: vi.fn(resolveUndefined),
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const mockCreateHarnessSession = vi.hoisted(() => vi.fn(async () => mockSession));
|
|
36
|
-
const mockExecuteRun = vi.hoisted(() => vi.fn(async () => undefined));
|
|
37
|
-
|
|
38
|
-
vi.mock('../harness-session.js', () => ({
|
|
39
|
-
createHarnessSession: mockCreateHarnessSession,
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
vi.mock('../execute-run.js', () => ({
|
|
43
|
-
executeRun: mockExecuteRun,
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Helpers
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
const makeOptions = (): TestRunnerOptions => ({ serial: true });
|
|
51
|
-
const makeWatcher = (): TestWatcher => ({ isInterrupted: () => false } as TestWatcher);
|
|
52
|
-
const makeTest = (): Test => ({
|
|
53
|
-
path: '/test/example.ts',
|
|
54
|
-
context: { config: {} as Config.ProjectConfig },
|
|
55
|
-
} as Test);
|
|
56
|
-
|
|
57
|
-
const makeGlobalConfig = (overrides: Partial<Config.GlobalConfig> = {}): Config.GlobalConfig =>
|
|
58
|
-
({ rootDir: '/project', watch: false, watchAll: false, collectCoverage: false, ...overrides } as Config.GlobalConfig);
|
|
59
|
-
|
|
60
|
-
type ExecuteRunMock = typeof ExecuteRun;
|
|
61
|
-
type TypedExecuteRunMock = typeof mockExecuteRun & {
|
|
62
|
-
mockImplementationOnce: (implementation: ExecuteRunMock) => typeof mockExecuteRun;
|
|
63
|
-
mockResolvedValue: (value: Awaited<ReturnType<ExecuteRunMock>>) => typeof mockExecuteRun;
|
|
64
|
-
};
|
|
65
|
-
type ExecuteRunParams = Parameters<ExecuteRunMock>;
|
|
66
|
-
|
|
67
|
-
const typedMockExecuteRun = mockExecuteRun as TypedExecuteRunMock;
|
|
68
|
-
|
|
69
|
-
const makeTestCaseStartPayload = (): TestEvents['test-case-start'] => [
|
|
70
|
-
'/test/example.ts',
|
|
71
|
-
{
|
|
72
|
-
ancestorTitles: [],
|
|
73
|
-
fullName: 'suite case',
|
|
74
|
-
mode: undefined,
|
|
75
|
-
title: 'case',
|
|
76
|
-
startedAt: 1,
|
|
77
|
-
},
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
const makeTestCaseResultPayload = (): TestEvents['test-case-result'] => [
|
|
81
|
-
'/test/example.ts',
|
|
82
|
-
{
|
|
83
|
-
ancestorTitles: [],
|
|
84
|
-
duration: 1,
|
|
85
|
-
failureDetails: [],
|
|
86
|
-
failureMessages: [],
|
|
87
|
-
fullName: 'suite case',
|
|
88
|
-
numPassingAsserts: 1,
|
|
89
|
-
startedAt: 1,
|
|
90
|
-
status: 'passed',
|
|
91
|
-
title: 'case',
|
|
92
|
-
},
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Tests
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
beforeEach(() => {
|
|
100
|
-
vi.clearAllMocks();
|
|
101
|
-
(mockSession.dispose as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
102
|
-
typedMockExecuteRun.mockResolvedValue(undefined);
|
|
103
|
-
mockCreateHarnessSession.mockResolvedValue(mockSession);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('JestHarness', () => {
|
|
107
|
-
describe('session lifecycle', () => {
|
|
108
|
-
it('creates a session on the first run', async () => {
|
|
109
|
-
const runner = new JestHarness(makeGlobalConfig());
|
|
110
|
-
|
|
111
|
-
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
112
|
-
|
|
113
|
-
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('reuses the session across runs in watch mode', async () => {
|
|
117
|
-
const runner = new JestHarness(makeGlobalConfig({ watch: true }));
|
|
118
|
-
|
|
119
|
-
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
120
|
-
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
121
|
-
|
|
122
|
-
expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
|
|
123
|
-
expect(mockSession.dispose).not.toHaveBeenCalled();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('disposes the session after each run in normal mode', async () => {
|
|
127
|
-
const runner = new JestHarness(makeGlobalConfig());
|
|
128
|
-
|
|
129
|
-
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
130
|
-
|
|
131
|
-
expect(mockSession.dispose).toHaveBeenCalledOnce();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('creates a fresh session for each run in normal mode', async () => {
|
|
135
|
-
const runner = new JestHarness(makeGlobalConfig());
|
|
136
|
-
|
|
137
|
-
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
138
|
-
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
139
|
-
|
|
140
|
-
expect(mockCreateHarnessSession).toHaveBeenCalledTimes(2);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('emits registered Jest events', async () => {
|
|
144
|
-
const runner = new JestHarness(makeGlobalConfig());
|
|
145
|
-
const listener = vi.fn();
|
|
146
|
-
const eventPayload = makeTestCaseStartPayload();
|
|
147
|
-
|
|
148
|
-
runner.on('test-case-start', listener);
|
|
149
|
-
typedMockExecuteRun.mockImplementationOnce(async (
|
|
150
|
-
_session: ExecuteRunParams[0],
|
|
151
|
-
_tests: ExecuteRunParams[1],
|
|
152
|
-
_watcher: ExecuteRunParams[2],
|
|
153
|
-
emitEvent: ExecuteRunParams[3],
|
|
154
|
-
) => {
|
|
155
|
-
await emitEvent('test-case-start', ...eventPayload);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
159
|
-
|
|
160
|
-
expect(listener).toHaveBeenCalledWith(eventPayload);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('stops emitting after listener unsubscribe', async () => {
|
|
164
|
-
const runner = new JestHarness(makeGlobalConfig());
|
|
165
|
-
const listener = vi.fn();
|
|
166
|
-
const unsubscribe = runner.on('test-case-result', listener);
|
|
167
|
-
const eventPayload = makeTestCaseResultPayload();
|
|
168
|
-
|
|
169
|
-
unsubscribe();
|
|
170
|
-
typedMockExecuteRun.mockImplementationOnce(async (
|
|
171
|
-
_session: ExecuteRunParams[0],
|
|
172
|
-
_tests: ExecuteRunParams[1],
|
|
173
|
-
_watcher: ExecuteRunParams[2],
|
|
174
|
-
emitEvent: ExecuteRunParams[3],
|
|
175
|
-
) => {
|
|
176
|
-
await emitEvent('test-case-result', ...eventPayload);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
await runner.runTests([makeTest()], makeWatcher(), makeOptions());
|
|
180
|
-
|
|
181
|
-
expect(listener).not.toHaveBeenCalled();
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe('error handling', () => {
|
|
186
|
-
it('converts HarnessError into a formatted string before throwing', async () => {
|
|
187
|
-
class TestHarnessError extends HarnessError {
|
|
188
|
-
constructor() { super('something went wrong'); this.name = 'TestHarnessError'; }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
mockCreateHarnessSession.mockRejectedValue(new TestHarnessError());
|
|
192
|
-
|
|
193
|
-
const runner = new JestHarness(makeGlobalConfig());
|
|
194
|
-
|
|
195
|
-
await expect(
|
|
196
|
-
runner.runTests([makeTest()], makeWatcher(), makeOptions()),
|
|
197
|
-
).rejects.toBeTypeOf('string');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('propagates non-HarnessError exceptions as-is', async () => {
|
|
201
|
-
const cause = new TypeError('unexpected');
|
|
202
|
-
mockCreateHarnessSession.mockRejectedValue(cause);
|
|
203
|
-
|
|
204
|
-
const runner = new JestHarness(makeGlobalConfig());
|
|
205
|
-
|
|
206
|
-
await expect(
|
|
207
|
-
runner.runTests([makeTest()], makeWatcher(), makeOptions()),
|
|
208
|
-
).rejects.toBe(cause);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('throws when called without serial flag', async () => {
|
|
212
|
-
const runner = new JestHarness(makeGlobalConfig());
|
|
213
|
-
|
|
214
|
-
await expect(
|
|
215
|
-
runner.runTests([makeTest()], makeWatcher(), { serial: false }),
|
|
216
|
-
).rejects.toThrow('Parallel test running is not supported');
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import type { Config as HarnessConfig } from '@react-native-harness/config';
|
|
3
|
-
import type { HarnessPlatform } from '@react-native-harness/platforms';
|
|
4
|
-
import { resolveHarnessMetroPort } from '../metro-port.js';
|
|
5
|
-
|
|
6
|
-
const mocks = vi.hoisted(() => ({
|
|
7
|
-
isPortAvailable: vi.fn(async () => true),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
vi.mock('@react-native-harness/bundler-metro', () => ({
|
|
11
|
-
isPortAvailable: mocks.isPortAvailable,
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
const createConfig = (overrides: Partial<HarnessConfig> = {}): HarnessConfig =>
|
|
15
|
-
({
|
|
16
|
-
appRegistryComponentName: 'App',
|
|
17
|
-
bridgeTimeout: 60_000,
|
|
18
|
-
bundleStartTimeout: 60_000,
|
|
19
|
-
crashDetectionInterval: 500,
|
|
20
|
-
defaultRunner: 'ios-device',
|
|
21
|
-
detectNativeCrashes: true,
|
|
22
|
-
disableViewFlattening: false,
|
|
23
|
-
entryPoint: 'index.js',
|
|
24
|
-
forwardClientLogs: false,
|
|
25
|
-
maxAppRestarts: 2,
|
|
26
|
-
metroPort: 8081,
|
|
27
|
-
platformReadyTimeout: 300_000,
|
|
28
|
-
resetEnvironmentBetweenTestFiles: true,
|
|
29
|
-
runners: [],
|
|
30
|
-
unstable__enableMetroCache: false,
|
|
31
|
-
unstable__skipAlreadyIncludedModules: false,
|
|
32
|
-
...overrides,
|
|
33
|
-
} as HarnessConfig);
|
|
34
|
-
|
|
35
|
-
describe('resolveHarnessMetroPort', () => {
|
|
36
|
-
it('skips fallback allocation for iOS physical device runners', async () => {
|
|
37
|
-
const acquire = vi.fn();
|
|
38
|
-
const config = createConfig();
|
|
39
|
-
const platform: HarnessPlatform = {
|
|
40
|
-
config: {},
|
|
41
|
-
name: 'ios-device',
|
|
42
|
-
platformId: 'ios',
|
|
43
|
-
runner: 'unused',
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const result = await resolveHarnessMetroPort({
|
|
47
|
-
config,
|
|
48
|
-
platform,
|
|
49
|
-
resourceLockManager: {
|
|
50
|
-
acquire,
|
|
51
|
-
},
|
|
52
|
-
signal: new AbortController().signal,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(result.config).toBe(config);
|
|
56
|
-
expect(result.metroPortLease).toBeNull();
|
|
57
|
-
expect(result.didFallback).toBe(false);
|
|
58
|
-
expect(acquire).not.toHaveBeenCalled();
|
|
59
|
-
expect(mocks.isPortAvailable).not.toHaveBeenCalled();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { createResourceLockManager } from '../resource-lock.js';
|
|
6
|
-
|
|
7
|
-
describe('resource lock manager', () => {
|
|
8
|
-
let rootDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
rootDir = await fs.mkdtemp(
|
|
12
|
-
path.join(os.tmpdir(), 'react-native-harness-resource-lock-test-'),
|
|
13
|
-
);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(async () => {
|
|
17
|
-
await fs.rm(rootDir, { recursive: true, force: true });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('queues access in FIFO order', async () => {
|
|
21
|
-
const manager = createResourceLockManager({
|
|
22
|
-
rootDir,
|
|
23
|
-
pollIntervalMs: 5,
|
|
24
|
-
heartbeatIntervalMs: 20,
|
|
25
|
-
staleLockTimeoutMs: 200,
|
|
26
|
-
});
|
|
27
|
-
const order: string[] = [];
|
|
28
|
-
|
|
29
|
-
const firstLease = await manager.acquire(
|
|
30
|
-
'ios:simulator:iPhone 17 Pro:26.2',
|
|
31
|
-
);
|
|
32
|
-
const secondAcquire = manager
|
|
33
|
-
.acquire('ios:simulator:iPhone 17 Pro:26.2', {
|
|
34
|
-
onWait: () => {
|
|
35
|
-
order.push('waiting');
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
.then(async (lease) => {
|
|
39
|
-
order.push('acquired');
|
|
40
|
-
await lease.release();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
44
|
-
expect(order).toEqual(['waiting']);
|
|
45
|
-
|
|
46
|
-
await firstLease.release();
|
|
47
|
-
await secondAcquire;
|
|
48
|
-
|
|
49
|
-
expect(order).toEqual(['waiting', 'acquired']);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('removes the queued ticket when waiting is aborted', async () => {
|
|
53
|
-
const manager = createResourceLockManager({
|
|
54
|
-
rootDir,
|
|
55
|
-
pollIntervalMs: 5,
|
|
56
|
-
heartbeatIntervalMs: 20,
|
|
57
|
-
staleLockTimeoutMs: 200,
|
|
58
|
-
});
|
|
59
|
-
const key = 'android:emulator:Pixel_8_API_35';
|
|
60
|
-
const firstLease = await manager.acquire(key);
|
|
61
|
-
const controller = new AbortController();
|
|
62
|
-
|
|
63
|
-
const acquirePromise = manager.acquire(key, {
|
|
64
|
-
signal: controller.signal,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
68
|
-
controller.abort();
|
|
69
|
-
|
|
70
|
-
await expect(acquirePromise).rejects.toMatchObject({
|
|
71
|
-
name: 'AbortError',
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// The aborted waiter must have cleaned up its ticket. Verify by releasing
|
|
75
|
-
// the first lock and confirming a fresh acquire completes immediately.
|
|
76
|
-
await firstLease.release();
|
|
77
|
-
const cleanupLease = await manager.acquire(key);
|
|
78
|
-
await cleanupLease.release();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('keeps queued tickets alive while the waiting process is still active', async () => {
|
|
82
|
-
const manager = createResourceLockManager({
|
|
83
|
-
rootDir,
|
|
84
|
-
pollIntervalMs: 5,
|
|
85
|
-
heartbeatIntervalMs: 20,
|
|
86
|
-
staleLockTimeoutMs: 30,
|
|
87
|
-
isProcessActive: () => true,
|
|
88
|
-
});
|
|
89
|
-
const key = 'ios:simulator:iPhone 17 Pro:26.2';
|
|
90
|
-
const firstLease = await manager.acquire(key);
|
|
91
|
-
|
|
92
|
-
const secondAcquire = manager.acquire(key);
|
|
93
|
-
|
|
94
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
95
|
-
|
|
96
|
-
await firstLease.release();
|
|
97
|
-
const secondLease = await secondAcquire;
|
|
98
|
-
await secondLease.release();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('reclaims a stale owner before granting the lock', async () => {
|
|
102
|
-
const key = 'web:browser:chromium';
|
|
103
|
-
|
|
104
|
-
// Simulate a live process holding the lock.
|
|
105
|
-
const manager1 = createResourceLockManager({
|
|
106
|
-
rootDir,
|
|
107
|
-
pollIntervalMs: 5,
|
|
108
|
-
heartbeatIntervalMs: 20,
|
|
109
|
-
});
|
|
110
|
-
const staleLease = await manager1.acquire(key);
|
|
111
|
-
|
|
112
|
-
// A second manager whose isProcessActive always returns false will consider
|
|
113
|
-
// any owner — including the live one above — immediately stale and reclaim it.
|
|
114
|
-
const manager2 = createResourceLockManager({
|
|
115
|
-
rootDir,
|
|
116
|
-
pollIntervalMs: 5,
|
|
117
|
-
heartbeatIntervalMs: 20,
|
|
118
|
-
staleLockTimeoutMs: 50,
|
|
119
|
-
isProcessActive: () => false,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const lease = await manager2.acquire(key);
|
|
123
|
-
await lease.release();
|
|
124
|
-
|
|
125
|
-
// manager1 was evicted; its release is best-effort.
|
|
126
|
-
await staleLease.release();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('keeps owner metadata valid when heartbeat writes overlap', async () => {
|
|
130
|
-
const manager = createResourceLockManager({
|
|
131
|
-
rootDir,
|
|
132
|
-
pollIntervalMs: 5,
|
|
133
|
-
heartbeatIntervalMs: 10,
|
|
134
|
-
staleLockTimeoutMs: 200,
|
|
135
|
-
});
|
|
136
|
-
const key = 'ios:simulator:iPhone 17 Pro:26.2';
|
|
137
|
-
const actualWriteFile = fs.writeFile.bind(fs);
|
|
138
|
-
const writeFileSpy = vi
|
|
139
|
-
.spyOn(fs, 'writeFile')
|
|
140
|
-
.mockImplementation(async (file, data, options) => {
|
|
141
|
-
// Delay atomic temp-file writes to simulate overlapping heartbeat flushes.
|
|
142
|
-
if (typeof file === 'string' && file.startsWith(rootDir) && file.endsWith('.tmp')) {
|
|
143
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return await actualWriteFile(file, data, options);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const lease = await manager.acquire(key);
|
|
151
|
-
|
|
152
|
-
// Discover the owner file after acquire creates the key directory.
|
|
153
|
-
const [keyDirName] = await fs.readdir(rootDir);
|
|
154
|
-
const ownerFilePath = path.join(rootDir, keyDirName, 'owner.json');
|
|
155
|
-
|
|
156
|
-
const initialOwner = JSON.parse(
|
|
157
|
-
await fs.readFile(ownerFilePath, 'utf8'),
|
|
158
|
-
) as ResourceLockOwner;
|
|
159
|
-
|
|
160
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
161
|
-
|
|
162
|
-
for (let index = 0; index < 5; index += 1) {
|
|
163
|
-
const owner = JSON.parse(
|
|
164
|
-
await fs.readFile(ownerFilePath, 'utf8'),
|
|
165
|
-
) as ResourceLockOwner;
|
|
166
|
-
expect(owner.ticketId).toBe(initialOwner.ticketId);
|
|
167
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
await lease.release();
|
|
171
|
-
} finally {
|
|
172
|
-
writeFileSpy.mockRestore();
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
type ResourceLockOwner = {
|
|
178
|
-
ticketId: string;
|
|
179
|
-
};
|