@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,157 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { AppBridgeDisconnectedError } from '@react-native-harness/bridge/server';
|
|
4
|
-
import { NativeCrashError, PlatformReadyTimeoutError } from '../errors.js';
|
|
5
|
-
|
|
6
|
-
describe('PlatformReadyTimeoutError', () => {
|
|
7
|
-
it('includes the configured timeout and config hint', () => {
|
|
8
|
-
expect(new PlatformReadyTimeoutError(300000).message).toBe(
|
|
9
|
-
'The platform did not become ready within 300000ms. Increase "platformReadyTimeout" if your device, simulator, or emulator needs more time to start.'
|
|
10
|
-
);
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe('NativeCrashError', () => {
|
|
15
|
-
it('reports the extracted crash log path when available', () => {
|
|
16
|
-
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
17
|
-
phase: 'execution',
|
|
18
|
-
artifactPath: path.join(
|
|
19
|
-
process.cwd(),
|
|
20
|
-
'.harness',
|
|
21
|
-
'crash-reports',
|
|
22
|
-
'crash.ips'
|
|
23
|
-
),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
expect(error.message).toContain(
|
|
27
|
-
`Harness extracted the crash log: ${path.join(
|
|
28
|
-
'.harness',
|
|
29
|
-
'crash-reports',
|
|
30
|
-
'crash.ips'
|
|
31
|
-
)}`
|
|
32
|
-
);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('lists enrichment artifact paths when available', () => {
|
|
36
|
-
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
37
|
-
phase: 'execution',
|
|
38
|
-
artifactPath: path.join(
|
|
39
|
-
process.cwd(),
|
|
40
|
-
'.harness',
|
|
41
|
-
'crash-reports',
|
|
42
|
-
'logcat.txt'
|
|
43
|
-
),
|
|
44
|
-
enrichmentArtifacts: [
|
|
45
|
-
{
|
|
46
|
-
artifactType: 'dropbox-native-crash',
|
|
47
|
-
artifactPath: path.join(
|
|
48
|
-
process.cwd(),
|
|
49
|
-
'.harness',
|
|
50
|
-
'crash-reports',
|
|
51
|
-
'dropbox-native-crash.txt'
|
|
52
|
-
),
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
expect(error.message).toContain('Additional crash artifacts:');
|
|
58
|
-
expect(error.message).toContain(
|
|
59
|
-
path.join('.harness', 'crash-reports', 'dropbox-native-crash.txt')
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('reports crash log extraction failure when no artifact was pulled', () => {
|
|
64
|
-
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
65
|
-
phase: 'execution',
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
expect(error.message).toContain("Harness couldn't extract the crash log.");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('formats the extracted stack trace in the error message', () => {
|
|
72
|
-
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
73
|
-
phase: 'execution',
|
|
74
|
-
processName: 'HarnessPlayground',
|
|
75
|
-
pid: 1234,
|
|
76
|
-
signal: 'SIGTRAP',
|
|
77
|
-
exceptionType: 'EXC_BREAKPOINT',
|
|
78
|
-
stackTrace: [
|
|
79
|
-
'0 AppDelegate.crashIfRequested() (AppDelegate.swift:31)',
|
|
80
|
-
'1 AppDelegate.application(_:didFinishLaunchingWithOptions:) (AppDelegate.swift:56)',
|
|
81
|
-
],
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
expect(error.message).toContain(
|
|
85
|
-
' 0 AppDelegate.crashIfRequested() (AppDelegate.swift:31)'
|
|
86
|
-
);
|
|
87
|
-
expect(error.message).toContain(
|
|
88
|
-
' 1 AppDelegate.application(_:didFinishLaunchingWithOptions:) (AppDelegate.swift:56)'
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('omits single-line iOS summaries from the rendered error message', () => {
|
|
93
|
-
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
94
|
-
phase: 'startup',
|
|
95
|
-
artifactType: 'ios-crash-report',
|
|
96
|
-
summary:
|
|
97
|
-
'2026-03-12 13:46:18.154 Df HarnessPlayground[18007:65e716] [com.apple.dt.xctest:Default] notify_get_state check indicated test daemon not ready.',
|
|
98
|
-
processName: 'HarnessPlayground',
|
|
99
|
-
pid: 18007,
|
|
100
|
-
signal: 'SIGABRT',
|
|
101
|
-
exceptionType: 'EXC_CRASH',
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
expect(error.message).not.toContain(
|
|
105
|
-
'notify_get_state check indicated test daemon not ready'
|
|
106
|
-
);
|
|
107
|
-
expect(error.message).toContain('Signal: SIGABRT');
|
|
108
|
-
expect(error.message).toContain('Exception: EXC_CRASH');
|
|
109
|
-
expect(error.message).toContain('Process: HarnessPlayground (pid 18007)');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('does not duplicate stack frames when the summary already contains a crash block', () => {
|
|
113
|
-
const frame =
|
|
114
|
-
'03-13 07:59:44.943 20373 20373 E AndroidRuntime: \tat com.harnessplayground.MainActivity.onCreate(MainActivity.kt:38)';
|
|
115
|
-
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
116
|
-
phase: 'startup',
|
|
117
|
-
summary: [
|
|
118
|
-
'--------- beginning of crash',
|
|
119
|
-
'03-13 07:59:44.943 20373 20373 E AndroidRuntime: FATAL EXCEPTION: main',
|
|
120
|
-
frame,
|
|
121
|
-
].join('\n'),
|
|
122
|
-
stackTrace: [frame],
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(
|
|
126
|
-
error.message.match(/MainActivity\.onCreate\(MainActivity\.kt:38\)/g)
|
|
127
|
-
).toHaveLength(1);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('collapses the native crash stack header so jest does not reprint multiline messages', () => {
|
|
131
|
-
const error = new NativeCrashError('/tmp/crash.harness.ts', {
|
|
132
|
-
phase: 'startup',
|
|
133
|
-
summary: ['line one', 'line two'].join('\n'),
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
expect(error.stack).toBe(
|
|
137
|
-
'NativeCrashError: The native app crashed while preparing to run this test file.'
|
|
138
|
-
);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('AppBridgeDisconnectedError', () => {
|
|
143
|
-
it('explains likely causes without exposing an internal stack', () => {
|
|
144
|
-
const error = new AppBridgeDisconnectedError('app-disconnected');
|
|
145
|
-
|
|
146
|
-
expect(error.message).toBe(
|
|
147
|
-
'The app bridge disconnected during test execution. This can happen if the app was killed, crashed, reloaded, or restarted while the test file was running.'
|
|
148
|
-
);
|
|
149
|
-
expect(error.stack).toBe(`AppBridgeDisconnectedError: ${error.message}`);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('uses a reconnection-specific message when a newer app connection replaces the old one', () => {
|
|
153
|
-
expect(new AppBridgeDisconnectedError('app-replaced').message).toBe(
|
|
154
|
-
'The app bridge was replaced by a newer app connection. This can happen when the app reloads, restarts, or reconnects while a test file is still running.'
|
|
155
|
-
);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1,526 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
-
import type { Config, Test, TestWatcher } from 'jest-runner';
|
|
3
|
-
import type { TestResult as JestTestResult } from '@jest/test-result';
|
|
4
|
-
import type {
|
|
5
|
-
TestRunnerTestFinishedEvent,
|
|
6
|
-
TestRunnerTestStartedEvent,
|
|
7
|
-
TestSuiteResult,
|
|
8
|
-
} from '@react-native-harness/bridge';
|
|
9
|
-
import { NativeCrashError, StartupStallError } from '../errors.js';
|
|
10
|
-
import {
|
|
11
|
-
AppBridgeDisconnectedError,
|
|
12
|
-
DeviceNotRespondingError,
|
|
13
|
-
} from '@react-native-harness/bridge/server';
|
|
14
|
-
import type { HarnessSession } from '../harness-session.js';
|
|
15
|
-
import { executeRun } from '../execute-run.js';
|
|
16
|
-
|
|
17
|
-
type EmitEvent = Parameters<typeof executeRun>[3];
|
|
18
|
-
type RecordedEmitEvent = {
|
|
19
|
-
emitEvent: EmitEvent;
|
|
20
|
-
calls: Array<unknown[]>;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const resolveUndefined = async () => undefined;
|
|
24
|
-
|
|
25
|
-
// Mock the file-runner so we control what jestResult/harnessResult each test returns
|
|
26
|
-
// without needing a real Jest config or Metro bundler.
|
|
27
|
-
const mockRunHarnessTestFile = vi.hoisted(() => vi.fn());
|
|
28
|
-
vi.mock('../run.js', () => ({
|
|
29
|
-
runHarnessTestFile: mockRunHarnessTestFile,
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Helpers
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
const makeJestResult = (
|
|
37
|
-
overrides: Partial<JestTestResult> = {},
|
|
38
|
-
): JestTestResult =>
|
|
39
|
-
({
|
|
40
|
-
testFilePath: '/test/example.ts',
|
|
41
|
-
testResults: [],
|
|
42
|
-
failureMessage: null,
|
|
43
|
-
numFailingTests: 0,
|
|
44
|
-
numPassingTests: 1,
|
|
45
|
-
numPendingTests: 0,
|
|
46
|
-
numTodoTests: 0,
|
|
47
|
-
leaks: false,
|
|
48
|
-
openHandles: [],
|
|
49
|
-
skipped: false,
|
|
50
|
-
snapshot: {
|
|
51
|
-
added: 0, fileDeleted: false, matched: 0, unchecked: 0,
|
|
52
|
-
uncheckedKeys: [], unmatched: 0, updated: 0,
|
|
53
|
-
},
|
|
54
|
-
perfStats: { start: 0, end: 1, runtime: 1, slow: false,
|
|
55
|
-
loadTestEnvironmentStart: 0, loadTestEnvironmentEnd: 0,
|
|
56
|
-
setupAfterEnvStart: 0, setupAfterEnvEnd: 0,
|
|
57
|
-
setupFilesStart: 0, setupFilesEnd: 0 },
|
|
58
|
-
coverage: undefined,
|
|
59
|
-
...overrides,
|
|
60
|
-
} as JestTestResult);
|
|
61
|
-
|
|
62
|
-
const makeHarnessResult = (status: 'passed' | 'failed' = 'passed'): TestSuiteResult => ({
|
|
63
|
-
name: 'suite',
|
|
64
|
-
tests: [],
|
|
65
|
-
suites: [],
|
|
66
|
-
status,
|
|
67
|
-
duration: 50,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const makeFileRunResult = (overrides: Partial<ReturnType<typeof mockRunHarnessTestFile>> = {}) => ({
|
|
71
|
-
jestResult: makeJestResult(),
|
|
72
|
-
harnessResult: makeHarnessResult(),
|
|
73
|
-
relativeTestPath: 'example.ts',
|
|
74
|
-
duration: 50,
|
|
75
|
-
...overrides,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const makeTest = (filePath = '/test/example.ts'): Test => ({
|
|
79
|
-
path: filePath,
|
|
80
|
-
context: { config: {} as Config.ProjectConfig },
|
|
81
|
-
} as Test);
|
|
82
|
-
|
|
83
|
-
const makeWatcher = (interrupted = false): TestWatcher =>
|
|
84
|
-
({ isInterrupted: () => interrupted }) as TestWatcher;
|
|
85
|
-
|
|
86
|
-
const makeGlobalConfig = (overrides: Partial<Config.GlobalConfig> = {}): Config.GlobalConfig =>
|
|
87
|
-
({
|
|
88
|
-
rootDir: '/project',
|
|
89
|
-
watch: false,
|
|
90
|
-
watchAll: false,
|
|
91
|
-
collectCoverage: false,
|
|
92
|
-
...overrides,
|
|
93
|
-
} as Config.GlobalConfig);
|
|
94
|
-
|
|
95
|
-
const makeSession = (overrides: Partial<HarnessSession> = {}): HarnessSession => ({
|
|
96
|
-
config: {
|
|
97
|
-
metroPort: 8081,
|
|
98
|
-
host: undefined,
|
|
99
|
-
resetEnvironmentBetweenTestFiles: false,
|
|
100
|
-
detectNativeCrashes: true,
|
|
101
|
-
runners: [
|
|
102
|
-
{ platformId: 'android', name: 'android' },
|
|
103
|
-
{ platformId: 'ios', name: 'ios' },
|
|
104
|
-
],
|
|
105
|
-
} as HarnessSession['config'],
|
|
106
|
-
context: {
|
|
107
|
-
platform: {
|
|
108
|
-
platformId: 'android',
|
|
109
|
-
name: 'android',
|
|
110
|
-
runner: '/virtual/android-runner.js',
|
|
111
|
-
cli: '/virtual/android-cli.js',
|
|
112
|
-
config: {},
|
|
113
|
-
},
|
|
114
|
-
} as HarnessSession['context'],
|
|
115
|
-
ensureAppReady: vi.fn(resolveUndefined),
|
|
116
|
-
runTestFile: vi.fn(async () => makeHarnessResult()),
|
|
117
|
-
restartApp: vi.fn(resolveUndefined),
|
|
118
|
-
resetCrashState: vi.fn(),
|
|
119
|
-
flushClientLogs: vi.fn(() => []),
|
|
120
|
-
callHook: vi.fn(resolveUndefined),
|
|
121
|
-
onTestRunnerEvent: vi.fn(() => () => undefined),
|
|
122
|
-
setRunState: vi.fn(),
|
|
123
|
-
dispose: vi.fn(resolveUndefined),
|
|
124
|
-
...overrides,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const makeEmitEvent = (): RecordedEmitEvent => {
|
|
128
|
-
const calls: Array<unknown[]> = [];
|
|
129
|
-
const emitEvent: EmitEvent = (async (...eventData) => {
|
|
130
|
-
calls.push(eventData);
|
|
131
|
-
}) as EmitEvent;
|
|
132
|
-
|
|
133
|
-
return { emitEvent, calls };
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Tests
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
beforeEach(() => {
|
|
141
|
-
vi.clearAllMocks();
|
|
142
|
-
mockRunHarnessTestFile.mockResolvedValue(makeFileRunResult());
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('executeRun', () => {
|
|
146
|
-
describe('lifecycle hooks', () => {
|
|
147
|
-
it('emits run:started then run:finished in order', async () => {
|
|
148
|
-
const hookNames: string[] = [];
|
|
149
|
-
const session = makeSession({
|
|
150
|
-
callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
154
|
-
|
|
155
|
-
expect(hookNames[0]).toBe('run:started');
|
|
156
|
-
expect(hookNames[hookNames.length - 1]).toBe('run:finished');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('emits test-file:started and test-file:finished around each test', async () => {
|
|
160
|
-
const hookNames: string[] = [];
|
|
161
|
-
const session = makeSession({
|
|
162
|
-
callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
166
|
-
|
|
167
|
-
const fileHooks = hookNames.filter((n) =>
|
|
168
|
-
n === 'test-file:started' || n === 'test-file:finished',
|
|
169
|
-
);
|
|
170
|
-
expect(fileHooks).toEqual([
|
|
171
|
-
'test-file:started', 'test-file:finished',
|
|
172
|
-
'test-file:started', 'test-file:finished',
|
|
173
|
-
]);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('always emits run:finished even when a test throws', async () => {
|
|
177
|
-
const hookNames: string[] = [];
|
|
178
|
-
const session = makeSession({
|
|
179
|
-
callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
|
|
180
|
-
ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
184
|
-
|
|
185
|
-
expect(hookNames).toContain('run:finished');
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe('happy path', () => {
|
|
190
|
-
it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
|
|
191
|
-
const order: string[] = [];
|
|
192
|
-
const session = makeSession({
|
|
193
|
-
ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
|
|
194
|
-
});
|
|
195
|
-
const emitEvent: EmitEvent = (async (eventName, ..._eventData) => {
|
|
196
|
-
if (eventName === 'test-file-start') {
|
|
197
|
-
order.push('test-file-start');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (eventName === 'test-file-success') {
|
|
201
|
-
order.push('test-file-success');
|
|
202
|
-
}
|
|
203
|
-
}) as EmitEvent;
|
|
204
|
-
|
|
205
|
-
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
206
|
-
order.push('runTestFile');
|
|
207
|
-
return makeFileRunResult();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
211
|
-
|
|
212
|
-
expect(order).toEqual([
|
|
213
|
-
'test-file-start',
|
|
214
|
-
'ensureAppReady',
|
|
215
|
-
'runTestFile',
|
|
216
|
-
'test-file-success',
|
|
217
|
-
]);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('accumulates passing test counts in run:finished summary', async () => {
|
|
221
|
-
let finishedPayload: unknown;
|
|
222
|
-
const session = makeSession({
|
|
223
|
-
callHook: vi.fn(async (name, payload) => {
|
|
224
|
-
if (name === 'run:finished') finishedPayload = payload;
|
|
225
|
-
}),
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
mockRunHarnessTestFile.mockResolvedValue(
|
|
229
|
-
makeFileRunResult({ jestResult: makeJestResult({ numPassingTests: 3 }) }),
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
233
|
-
|
|
234
|
-
const payload = finishedPayload as { summary: { passed: number }; status: string };
|
|
235
|
-
expect(payload.summary.passed).toBe(6);
|
|
236
|
-
expect(payload.status).toBe('passed');
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('attaches buffered client logs to the Jest result', async () => {
|
|
240
|
-
const clientLogs = [{ message: 'Loaded screen', origin: '', type: 'warn' }] satisfies NonNullable<JestTestResult['console']>;
|
|
241
|
-
const session = makeSession({
|
|
242
|
-
flushClientLogs: vi.fn()
|
|
243
|
-
.mockReturnValueOnce([])
|
|
244
|
-
.mockReturnValueOnce(clientLogs),
|
|
245
|
-
});
|
|
246
|
-
const { emitEvent, calls } = makeEmitEvent();
|
|
247
|
-
|
|
248
|
-
await executeRun(
|
|
249
|
-
session,
|
|
250
|
-
[makeTest()],
|
|
251
|
-
makeWatcher(),
|
|
252
|
-
emitEvent,
|
|
253
|
-
makeGlobalConfig(),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
expect(session.flushClientLogs).toHaveBeenCalledTimes(2);
|
|
257
|
-
expect(calls).toContainEqual([
|
|
258
|
-
'test-file-success',
|
|
259
|
-
expect.anything(),
|
|
260
|
-
expect.objectContaining({ console: clientLogs }),
|
|
261
|
-
]);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('emits test-case events before file success', async () => {
|
|
265
|
-
let testRunnerListener:
|
|
266
|
-
| ((event: TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent) => void)
|
|
267
|
-
| undefined;
|
|
268
|
-
const { emitEvent, calls: emittedEvents } = makeEmitEvent();
|
|
269
|
-
const session = makeSession({
|
|
270
|
-
onTestRunnerEvent: vi.fn((listener) => {
|
|
271
|
-
testRunnerListener = listener as typeof testRunnerListener;
|
|
272
|
-
return () => undefined;
|
|
273
|
-
}),
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
mockRunHarnessTestFile.mockImplementation(async () => {
|
|
277
|
-
testRunnerListener?.({
|
|
278
|
-
type: 'test-started',
|
|
279
|
-
file: 'example.ts',
|
|
280
|
-
suite: 'suite',
|
|
281
|
-
name: 'works',
|
|
282
|
-
ancestorTitles: ['suite'],
|
|
283
|
-
fullName: 'suite works',
|
|
284
|
-
startedAt: 10,
|
|
285
|
-
declarationMode: 'only',
|
|
286
|
-
});
|
|
287
|
-
testRunnerListener?.({
|
|
288
|
-
type: 'test-finished',
|
|
289
|
-
file: 'example.ts',
|
|
290
|
-
suite: 'suite',
|
|
291
|
-
name: 'works',
|
|
292
|
-
ancestorTitles: ['suite'],
|
|
293
|
-
fullName: 'suite works',
|
|
294
|
-
startedAt: 10,
|
|
295
|
-
declarationMode: 'only',
|
|
296
|
-
duration: 5,
|
|
297
|
-
status: 'passed',
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
return makeFileRunResult();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
304
|
-
|
|
305
|
-
expect(emittedEvents).toEqual([
|
|
306
|
-
['test-file-start', expect.anything()],
|
|
307
|
-
[
|
|
308
|
-
'test-case-start',
|
|
309
|
-
'example.ts',
|
|
310
|
-
expect.objectContaining({
|
|
311
|
-
ancestorTitles: ['suite'],
|
|
312
|
-
fullName: 'suite works',
|
|
313
|
-
mode: 'only',
|
|
314
|
-
title: 'works',
|
|
315
|
-
startedAt: 10,
|
|
316
|
-
}),
|
|
317
|
-
],
|
|
318
|
-
[
|
|
319
|
-
'test-case-result',
|
|
320
|
-
'example.ts',
|
|
321
|
-
expect.objectContaining({
|
|
322
|
-
ancestorTitles: ['suite'],
|
|
323
|
-
fullName: 'suite works',
|
|
324
|
-
numPassingAsserts: 1,
|
|
325
|
-
startedAt: 10,
|
|
326
|
-
status: 'passed',
|
|
327
|
-
title: 'works',
|
|
328
|
-
}),
|
|
329
|
-
],
|
|
330
|
-
['test-file-success', expect.anything(), expect.anything()],
|
|
331
|
-
]);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
describe('runtime failures', () => {
|
|
336
|
-
it('passes StartupStallError to onFailure with an empty stack', async () => {
|
|
337
|
-
const { emitEvent, calls } = makeEmitEvent();
|
|
338
|
-
const session = makeSession({
|
|
339
|
-
ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1500, 3)),
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
343
|
-
|
|
344
|
-
expect(calls).toContainEqual([
|
|
345
|
-
'test-file-failure',
|
|
346
|
-
expect.objectContaining({ path: '/test/example.ts' }),
|
|
347
|
-
expect.objectContaining({ message: expect.stringContaining('1500'), stack: '' }),
|
|
348
|
-
]);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('passes DeviceNotRespondingError to onFailure with an empty stack', async () => {
|
|
352
|
-
const { emitEvent, calls } = makeEmitEvent();
|
|
353
|
-
const session = makeSession({
|
|
354
|
-
ensureAppReady: vi.fn().mockRejectedValue(
|
|
355
|
-
new DeviceNotRespondingError('runTests', []),
|
|
356
|
-
),
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
360
|
-
|
|
361
|
-
expect(calls).toContainEqual([
|
|
362
|
-
'test-file-failure',
|
|
363
|
-
expect.anything(),
|
|
364
|
-
expect.objectContaining({ stack: '' }),
|
|
365
|
-
]);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('passes AppBridgeDisconnectedError to onFailure with an empty stack', async () => {
|
|
369
|
-
const { emitEvent, calls } = makeEmitEvent();
|
|
370
|
-
const session = makeSession({
|
|
371
|
-
ensureAppReady: vi.fn().mockRejectedValue(
|
|
372
|
-
new AppBridgeDisconnectedError('app-disconnected'),
|
|
373
|
-
),
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
|
|
377
|
-
|
|
378
|
-
expect(calls).toContainEqual([
|
|
379
|
-
'test-file-failure',
|
|
380
|
-
expect.anything(),
|
|
381
|
-
expect.objectContaining({
|
|
382
|
-
message: expect.stringContaining('The app bridge disconnected during test execution.'),
|
|
383
|
-
stack: '',
|
|
384
|
-
}),
|
|
385
|
-
]);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('calls resetCrashState after a NativeCrashError', async () => {
|
|
389
|
-
const session = makeSession({
|
|
390
|
-
ensureAppReady: vi.fn().mockRejectedValue(
|
|
391
|
-
new NativeCrashError('example.ts', { phase: 'execution', source: 'polling' }),
|
|
392
|
-
),
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
396
|
-
|
|
397
|
-
expect(session.resetCrashState).toHaveBeenCalled();
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it('counts runtime failures in the summary', async () => {
|
|
401
|
-
let finishedPayload: unknown;
|
|
402
|
-
const session = makeSession({
|
|
403
|
-
callHook: vi.fn(async (name, payload) => {
|
|
404
|
-
if (name === 'run:finished') finishedPayload = payload;
|
|
405
|
-
}),
|
|
406
|
-
ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1000, 2)),
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
|
|
410
|
-
|
|
411
|
-
const payload = finishedPayload as { summary: { failed: number }; status: string };
|
|
412
|
-
expect(payload.summary.failed).toBe(1);
|
|
413
|
-
expect(payload.status).toBe('failed');
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
describe('watcher interrupt', () => {
|
|
418
|
-
it('stops processing tests and still emits run:finished', async () => {
|
|
419
|
-
const hookNames: string[] = [];
|
|
420
|
-
const session = makeSession({
|
|
421
|
-
callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
|
|
422
|
-
});
|
|
423
|
-
const { emitEvent, calls } = makeEmitEvent();
|
|
424
|
-
|
|
425
|
-
await executeRun(
|
|
426
|
-
session,
|
|
427
|
-
[makeTest('/a.ts'), makeTest('/b.ts')],
|
|
428
|
-
makeWatcher(true /* interrupted */),
|
|
429
|
-
emitEvent,
|
|
430
|
-
makeGlobalConfig(),
|
|
431
|
-
);
|
|
432
|
-
|
|
433
|
-
// No test was started; watcher was already interrupted.
|
|
434
|
-
expect(calls).not.toContainEqual(['test-file-start', expect.anything()]);
|
|
435
|
-
expect(hookNames).toContain('run:finished');
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
describe('resetEnvironmentBetweenTestFiles', () => {
|
|
440
|
-
it('calls restartApp before the second test but not the first', async () => {
|
|
441
|
-
const session = makeSession({
|
|
442
|
-
config: {
|
|
443
|
-
metroPort: 8081,
|
|
444
|
-
resetEnvironmentBetweenTestFiles: true,
|
|
445
|
-
detectNativeCrashes: false,
|
|
446
|
-
runners: [
|
|
447
|
-
{ platformId: 'android', name: 'android' },
|
|
448
|
-
{ platformId: 'ios', name: 'ios' },
|
|
449
|
-
],
|
|
450
|
-
} as HarnessSession['config'],
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
await executeRun(
|
|
454
|
-
session,
|
|
455
|
-
[makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')],
|
|
456
|
-
makeWatcher(),
|
|
457
|
-
makeEmitEvent().emitEvent,
|
|
458
|
-
makeGlobalConfig(),
|
|
459
|
-
);
|
|
460
|
-
|
|
461
|
-
// restartApp should be called for tests 2 and 3, not test 1.
|
|
462
|
-
expect(session.restartApp).toHaveBeenCalledTimes(2);
|
|
463
|
-
});
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
describe('platform-specific test files', () => {
|
|
467
|
-
it('skips files for other platforms without running them on the device', async () => {
|
|
468
|
-
const session = makeSession();
|
|
469
|
-
const { emitEvent, calls } = makeEmitEvent();
|
|
470
|
-
|
|
471
|
-
await executeRun(
|
|
472
|
-
session,
|
|
473
|
-
[
|
|
474
|
-
makeTest('/project/smoke.harness.ts'),
|
|
475
|
-
makeTest('/project/kotlin.android.harness.ts'),
|
|
476
|
-
makeTest('/project/swift.ios.harness.ts'),
|
|
477
|
-
],
|
|
478
|
-
makeWatcher(),
|
|
479
|
-
emitEvent,
|
|
480
|
-
makeGlobalConfig(),
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(2);
|
|
484
|
-
expect(mockRunHarnessTestFile).toHaveBeenCalledWith(
|
|
485
|
-
expect.objectContaining({ testPath: '/project/smoke.harness.ts' }),
|
|
486
|
-
);
|
|
487
|
-
expect(mockRunHarnessTestFile).toHaveBeenCalledWith(
|
|
488
|
-
expect.objectContaining({ testPath: '/project/kotlin.android.harness.ts' }),
|
|
489
|
-
);
|
|
490
|
-
expect(session.ensureAppReady).toHaveBeenCalledTimes(2);
|
|
491
|
-
expect(calls).toContainEqual([
|
|
492
|
-
'test-file-success',
|
|
493
|
-
expect.objectContaining({ path: '/project/swift.ios.harness.ts' }),
|
|
494
|
-
expect.objectContaining({ skipped: true }),
|
|
495
|
-
]);
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
it('does not restart the app for skipped platform-specific files', async () => {
|
|
499
|
-
const session = makeSession({
|
|
500
|
-
config: {
|
|
501
|
-
metroPort: 8081,
|
|
502
|
-
resetEnvironmentBetweenTestFiles: true,
|
|
503
|
-
detectNativeCrashes: false,
|
|
504
|
-
runners: [
|
|
505
|
-
{ platformId: 'android', name: 'android' },
|
|
506
|
-
{ platformId: 'ios', name: 'ios' },
|
|
507
|
-
],
|
|
508
|
-
} as HarnessSession['config'],
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
await executeRun(
|
|
512
|
-
session,
|
|
513
|
-
[
|
|
514
|
-
makeTest('/project/swift.ios.harness.ts'),
|
|
515
|
-
makeTest('/project/kotlin.android.harness.ts'),
|
|
516
|
-
],
|
|
517
|
-
makeWatcher(),
|
|
518
|
-
vi.fn(),
|
|
519
|
-
makeGlobalConfig(),
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
expect(session.restartApp).not.toHaveBeenCalled();
|
|
523
|
-
expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
});
|