@react-native-harness/jest 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-args.d.ts +3 -4
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +3 -8
- package/dist/diagnostics/index.d.ts +4 -1
- package/dist/diagnostics/index.d.ts.map +1 -1
- package/dist/diagnostics/index.js +2 -1
- package/dist/diagnostics/report.d.ts +81 -1
- package/dist/diagnostics/report.d.ts.map +1 -1
- package/dist/diagnostics/report.js +225 -3
- package/dist/diagnostics/telemetry.d.ts +42 -0
- package/dist/diagnostics/telemetry.d.ts.map +1 -0
- package/dist/diagnostics/telemetry.js +120 -0
- package/dist/errors.d.ts +8 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +29 -3
- package/dist/execute-run.d.ts.map +1 -1
- package/dist/execute-run.js +73 -21
- package/dist/format-harness-error.d.ts.map +1 -1
- package/dist/format-harness-error.js +3 -0
- package/dist/harness-session.d.ts +8 -1
- package/dist/harness-session.d.ts.map +1 -1
- package/dist/harness-session.js +110 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/jest-preset.d.ts +3 -0
- package/dist/jest-preset.d.ts.map +1 -1
- package/dist/jest-preset.js +18 -0
- package/dist/relative-path.d.ts +8 -0
- package/dist/relative-path.d.ts.map +1 -0
- package/dist/relative-path.js +8 -0
- package/dist/resource-lock.d.ts.map +1 -1
- package/dist/resource-lock.js +47 -27
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +4 -4
- package/dist/watch-session.d.ts +6 -0
- package/dist/watch-session.d.ts.map +1 -0
- package/dist/watch-session.js +37 -0
- package/package.json +8 -8
- package/src/cli-args.ts +5 -13
- package/src/diagnostics/index.ts +12 -1
- package/src/diagnostics/report.ts +326 -5
- package/src/diagnostics/telemetry.ts +186 -0
- package/src/errors.ts +42 -5
- package/src/execute-run.ts +93 -23
- package/src/format-harness-error.ts +5 -0
- package/src/harness-session.ts +143 -22
- package/src/index.ts +8 -7
- package/src/jest-preset.ts +18 -0
- package/src/relative-path.ts +10 -0
- package/src/resource-lock.ts +64 -33
- package/src/run.ts +4 -4
- package/src/watch-session.ts +65 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHarnessSession } from './harness-session.js';
|
|
2
|
+
import { getWatchSession } from './watch-session.js';
|
|
2
3
|
import { executeRun } from './execute-run.js';
|
|
3
4
|
import { HarnessError } from '@react-native-harness/tools';
|
|
4
5
|
import { getErrorMessage } from './logs.js';
|
|
@@ -6,7 +7,6 @@ export default class JestHarness {
|
|
|
6
7
|
isSerial = true;
|
|
7
8
|
supportsEventEmitters = true;
|
|
8
9
|
#globalConfig;
|
|
9
|
-
#session = null;
|
|
10
10
|
#listeners = new Map();
|
|
11
11
|
constructor(globalConfig) {
|
|
12
12
|
this.#globalConfig = globalConfig;
|
|
@@ -27,11 +27,12 @@ export default class JestHarness {
|
|
|
27
27
|
throw new Error('Parallel test running is not supported');
|
|
28
28
|
}
|
|
29
29
|
const isWatchMode = this.#globalConfig.watch || this.#globalConfig.watchAll;
|
|
30
|
+
let session = null;
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await executeRun(
|
|
32
|
+
session = isWatchMode
|
|
33
|
+
? await getWatchSession(this.#globalConfig)
|
|
34
|
+
: await createHarnessSession(this.#globalConfig);
|
|
35
|
+
await executeRun(session, tests, watcher, this.#emit, this.#globalConfig);
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
37
38
|
if (error instanceof HarnessError) {
|
|
@@ -41,8 +42,7 @@ export default class JestHarness {
|
|
|
41
42
|
}
|
|
42
43
|
finally {
|
|
43
44
|
if (!isWatchMode) {
|
|
44
|
-
await
|
|
45
|
-
this.#session = null;
|
|
45
|
+
await session?.dispose();
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
package/dist/jest-preset.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jest-preset.d.ts","sourceRoot":"","sources":["../src/jest-preset.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"jest-preset.d.ts","sourceRoot":"","sources":["../src/jest-preset.ts"],"names":[],"mappings":";;;;;;AAAA,wBAoBE"}
|
package/dist/jest-preset.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
runner: '@react-native-harness/jest',
|
|
3
|
+
// Tests run serially on the device, so Jest only uses its workers to build
|
|
4
|
+
// the haste map. On a cold cache or after any file change that starts a pool
|
|
5
|
+
// of `availableParallelism() - 1` processes, which roughly doubles Jest's
|
|
6
|
+
// CPU time without making the run faster.
|
|
7
|
+
maxWorkers: 1,
|
|
8
|
+
// Build and cache output never contains tests, but it can hold tens of
|
|
9
|
+
// thousands of files. Jest checks these patterns before descending into a
|
|
10
|
+
// directory, so its crawl skips them entirely. Jest appends a project's own
|
|
11
|
+
// patterns to the preset's.
|
|
12
|
+
modulePathIgnorePatterns: [
|
|
13
|
+
'/\\.harness(/|$)',
|
|
14
|
+
'/ios/(Pods|build)(/|$)',
|
|
15
|
+
'/android/(build|\\.gradle|app/build|app/\\.cxx)(/|$)',
|
|
16
|
+
],
|
|
17
|
+
// With the build output skipped, Jest's own crawl is cheaper than querying
|
|
18
|
+
// watchman, which costs two CLI process spawns per run and still lists every
|
|
19
|
+
// file. Metro keeps using watchman.
|
|
20
|
+
watchman: false,
|
|
3
21
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns `filePath` relative to `rootDir`, always using `/` as the separator.
|
|
3
|
+
*
|
|
4
|
+
* These paths leave the host (they are sent to the device and used in Metro
|
|
5
|
+
* bundle URLs and plugin hooks), so they must not depend on the host OS.
|
|
6
|
+
*/
|
|
7
|
+
export declare const toRelativePosixPath: (rootDir: string, filePath: string) => string;
|
|
8
|
+
//# sourceMappingURL=relative-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relative-path.d.ts","sourceRoot":"","sources":["../src/relative-path.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,MACZ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Returns `filePath` relative to `rootDir`, always using `/` as the separator.
|
|
4
|
+
*
|
|
5
|
+
* These paths leave the host (they are sent to the device and used in Metro
|
|
6
|
+
* bundle URLs and plugin hooks), so they must not depend on the host OS.
|
|
7
|
+
*/
|
|
8
|
+
export const toRelativePosixPath = (rootDir, filePath) => path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-lock.d.ts","sourceRoot":"","sources":["../src/resource-lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAiBzE,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,CACP,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,KACjC,OAAO,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C,CAAC;
|
|
1
|
+
{"version":3,"file":"resource-lock.d.ts","sourceRoot":"","sources":["../src/resource-lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAiBzE,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,CACP,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,KACjC,OAAO,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C,CAAC;AAsSF,eAAO,MAAM,yBAAyB,GACpC,UAAS,0BAA+B,KACvC,mBA6KF,CAAC"}
|
package/dist/resource-lock.js
CHANGED
|
@@ -35,6 +35,28 @@ const isMissingFileError = (error) => {
|
|
|
35
35
|
typeof error.code === 'string' &&
|
|
36
36
|
error.code === 'ENOENT');
|
|
37
37
|
};
|
|
38
|
+
// On Windows, a file being deleted by another process can briefly fail
|
|
39
|
+
// reads and deletes with EPERM/EBUSY instead of ENOENT.
|
|
40
|
+
const isTransientFsError = (error) => {
|
|
41
|
+
return (error instanceof Error &&
|
|
42
|
+
'code' in error &&
|
|
43
|
+
(error.code === 'EPERM' || error.code === 'EBUSY'));
|
|
44
|
+
};
|
|
45
|
+
const TRANSIENT_FS_RETRIES = 5;
|
|
46
|
+
const TRANSIENT_FS_RETRY_DELAY_MS = 20;
|
|
47
|
+
const retryTransientFsError = async (operation) => {
|
|
48
|
+
for (let attempt = 1;; attempt += 1) {
|
|
49
|
+
try {
|
|
50
|
+
return await operation();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (!isTransientFsError(error) || attempt >= TRANSIENT_FS_RETRIES) {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, TRANSIENT_FS_RETRY_DELAY_MS));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
38
60
|
const isExclusiveCreateError = (error) => {
|
|
39
61
|
return (error instanceof Error &&
|
|
40
62
|
'code' in error &&
|
|
@@ -46,7 +68,7 @@ const ensureLockDirectories = async (paths) => {
|
|
|
46
68
|
};
|
|
47
69
|
const readJsonFile = async (filePath) => {
|
|
48
70
|
try {
|
|
49
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
71
|
+
const content = await retryTransientFsError(() => fs.readFile(filePath, 'utf8'));
|
|
50
72
|
return JSON.parse(content);
|
|
51
73
|
}
|
|
52
74
|
catch (error) {
|
|
@@ -69,7 +91,7 @@ const writeJsonFileAtomic = async (filePath, value) => {
|
|
|
69
91
|
};
|
|
70
92
|
const removeFileIfPresent = async (filePath) => {
|
|
71
93
|
try {
|
|
72
|
-
await fs.rm(filePath, { force: true });
|
|
94
|
+
await retryTransientFsError(() => fs.rm(filePath, { force: true }));
|
|
73
95
|
}
|
|
74
96
|
catch (error) {
|
|
75
97
|
if (!isMissingFileError(error)) {
|
|
@@ -200,7 +222,7 @@ export const createResourceLockManager = (options = {}) => {
|
|
|
200
222
|
await fs.writeFile(ticketPath, JSON.stringify(metadata), 'utf8');
|
|
201
223
|
scopedLogger.debug('queued ticket %s for key %s', ticketId, key);
|
|
202
224
|
let heartbeatTimer = null;
|
|
203
|
-
let heartbeatInFlight =
|
|
225
|
+
let heartbeatInFlight = null;
|
|
204
226
|
let released = false;
|
|
205
227
|
let didNotifyWait = false;
|
|
206
228
|
const waitStartedAt = Date.now();
|
|
@@ -213,6 +235,9 @@ export const createResourceLockManager = (options = {}) => {
|
|
|
213
235
|
clearInterval(heartbeatTimer);
|
|
214
236
|
heartbeatTimer = null;
|
|
215
237
|
}
|
|
238
|
+
// Let an in-flight refresh finish so it cannot recreate owner.json
|
|
239
|
+
// after we remove it.
|
|
240
|
+
await heartbeatInFlight;
|
|
216
241
|
const owner = await readJsonFile(paths.ownerFilePath);
|
|
217
242
|
if (owner?.ticketId === ticketId) {
|
|
218
243
|
await removeFileIfPresent(paths.ownerFilePath);
|
|
@@ -221,40 +246,35 @@ export const createResourceLockManager = (options = {}) => {
|
|
|
221
246
|
scopedLogger.debug('released ticket %s for key %s', ticketId, key);
|
|
222
247
|
};
|
|
223
248
|
const startHeartbeat = () => {
|
|
224
|
-
|
|
225
|
-
if (heartbeatInFlight) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
heartbeatInFlight = true;
|
|
249
|
+
const refreshHeartbeat = async () => {
|
|
229
250
|
try {
|
|
230
251
|
const nextHeartbeatAt = Date.now();
|
|
231
252
|
const owner = await readJsonFile(paths.ownerFilePath);
|
|
232
253
|
if (released || owner?.ticketId !== ticketId) {
|
|
233
254
|
return;
|
|
234
255
|
}
|
|
235
|
-
|
|
256
|
+
await writeJsonFileAtomic(paths.ownerFilePath, {
|
|
236
257
|
...owner,
|
|
237
258
|
heartbeatAt: nextHeartbeatAt,
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
try {
|
|
243
|
-
await writeJsonFileAtomic(paths.ownerFilePath, nextMetadata);
|
|
244
|
-
scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
// A failed refresh is not fatal by design: the lock goes stale
|
|
248
|
-
// and another holder reclaims it. Swallow it so a transient
|
|
249
|
-
// write error (e.g. the owner file racing a concurrent release,
|
|
250
|
-
// or an EPERM on Windows when the directory is being torn down)
|
|
251
|
-
// never surfaces as an unhandled rejection from this interval.
|
|
252
|
-
scopedLogger.debug('heartbeat refresh for ticket %s failed: %s', ticketId, error instanceof Error ? error.message : String(error));
|
|
253
|
-
}
|
|
259
|
+
});
|
|
260
|
+
scopedLogger.debug('refreshed heartbeat for ticket %s', ticketId);
|
|
254
261
|
}
|
|
255
|
-
|
|
256
|
-
|
|
262
|
+
catch (error) {
|
|
263
|
+
// A failed refresh is not fatal by design: the lock goes stale
|
|
264
|
+
// and another holder reclaims it. Swallow it so a transient
|
|
265
|
+
// error (e.g. the owner file racing a concurrent release, or an
|
|
266
|
+
// EPERM on Windows while a file is being deleted) never surfaces
|
|
267
|
+
// as an unhandled rejection from this interval.
|
|
268
|
+
scopedLogger.debug('heartbeat refresh for ticket %s failed: %s', ticketId, error instanceof Error ? error.message : String(error));
|
|
257
269
|
}
|
|
270
|
+
};
|
|
271
|
+
heartbeatTimer = setInterval(() => {
|
|
272
|
+
if (heartbeatInFlight) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
heartbeatInFlight = refreshHeartbeat().finally(() => {
|
|
276
|
+
heartbeatInFlight = null;
|
|
277
|
+
});
|
|
258
278
|
}, heartbeatIntervalMs);
|
|
259
279
|
heartbeatTimer.unref?.();
|
|
260
280
|
};
|
package/dist/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EACV,eAAe,IAAI,sBAAsB,EAE1C,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAoE3D,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,aAAa,EAAE,UAAU,CAAC,aAAa,CAAC;IACxC,YAAY,EAAE,UAAU,CAAC,YAAY,CAAC;CACvC,KAAK,OAAO,CAAC;IACZ,UAAU,EAAE,cAAc,CAAC;IAC3B,aAAa,EAAE,sBAAsB,CAAC;IACtC,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,kBA0FhC,CAAC"}
|
package/dist/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { formatResultsErrors } from 'jest-message-util';
|
|
3
2
|
import { toTestResult } from './toTestResult.js';
|
|
4
3
|
import { formatHarnessErrorMessage } from './format-harness-error.js';
|
|
4
|
+
import { toRelativePosixPath } from './relative-path.js';
|
|
5
5
|
// Helper function to flatten nested test suites into a flat array of tests with hierarchy
|
|
6
6
|
const flattenTests = (suiteResult, ancestorTitles = []) => {
|
|
7
7
|
const tests = [];
|
|
@@ -55,10 +55,10 @@ const calculateStats = (tests) => {
|
|
|
55
55
|
};
|
|
56
56
|
export const runHarnessTestFile = async ({ testPath, globalConfig, projectConfig, session, }) => {
|
|
57
57
|
const start = Date.now();
|
|
58
|
-
const relativeTestPath =
|
|
58
|
+
const relativeTestPath = toRelativePosixPath(globalConfig.rootDir, testPath);
|
|
59
59
|
// Extract setup files from Jest config and convert to relative paths
|
|
60
|
-
const setupFiles = projectConfig.setupFiles?.map((setupFile) =>
|
|
61
|
-
const setupFilesAfterEnv = projectConfig.setupFilesAfterEnv?.map((setupFile) =>
|
|
60
|
+
const setupFiles = projectConfig.setupFiles?.map((setupFile) => toRelativePosixPath(globalConfig.rootDir, setupFile));
|
|
61
|
+
const setupFilesAfterEnv = projectConfig.setupFilesAfterEnv?.map((setupFile) => toRelativePosixPath(globalConfig.rootDir, setupFile));
|
|
62
62
|
const testTimeout = session.config.testTimeout ??
|
|
63
63
|
projectConfig
|
|
64
64
|
.testTimeout ??
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Config } from 'jest-runner';
|
|
2
|
+
import { type HarnessSession } from './harness-session.js';
|
|
3
|
+
export declare const getWatchSession: (globalConfig: Config.GlobalConfig) => Promise<HarnessSession>;
|
|
4
|
+
/** @internal Test-only: forget the module-level session without disposing. */
|
|
5
|
+
export declare const resetWatchSessionForTests: () => void;
|
|
6
|
+
//# sourceMappingURL=watch-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-session.d.ts","sourceRoot":"","sources":["../src/watch-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAqB9B,eAAO,MAAM,eAAe,GAC1B,cAAc,MAAM,CAAC,YAAY,KAChC,OAAO,CAAC,cAAc,CAgCxB,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,QAAO,IAE5C,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHarnessSession, } from './harness-session.js';
|
|
2
|
+
let current = null;
|
|
3
|
+
// Everything createHarnessSession reads from the Jest config.
|
|
4
|
+
const getSessionKey = (globalConfig) => JSON.stringify([globalConfig.rootDir, Boolean(globalConfig.collectCoverage)]);
|
|
5
|
+
export const getWatchSession = (globalConfig) => {
|
|
6
|
+
const key = getSessionKey(globalConfig);
|
|
7
|
+
if (current?.key === key) {
|
|
8
|
+
return current.promise;
|
|
9
|
+
}
|
|
10
|
+
const stale = current;
|
|
11
|
+
// The entry is installed synchronously so concurrent callers share it, even
|
|
12
|
+
// while a stale session is still being disposed.
|
|
13
|
+
const entry = {
|
|
14
|
+
key,
|
|
15
|
+
promise: (async () => {
|
|
16
|
+
if (stale) {
|
|
17
|
+
// A failed disposal must not fail the run.
|
|
18
|
+
await stale.promise
|
|
19
|
+
.then((session) => session.dispose())
|
|
20
|
+
.catch(() => undefined);
|
|
21
|
+
}
|
|
22
|
+
return createHarnessSession(globalConfig);
|
|
23
|
+
})(),
|
|
24
|
+
};
|
|
25
|
+
current = entry;
|
|
26
|
+
// Never cache a failed creation; the next run retries.
|
|
27
|
+
entry.promise.catch(() => {
|
|
28
|
+
if (current === entry) {
|
|
29
|
+
current = null;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return entry.promise;
|
|
33
|
+
};
|
|
34
|
+
/** @internal Test-only: forget the module-level session without disposing. */
|
|
35
|
+
export const resetWatchSessionForTests = () => {
|
|
36
|
+
current = null;
|
|
37
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/jest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"jest-util": "^30.2.0",
|
|
40
40
|
"tslib": "^2.3.0",
|
|
41
41
|
"yargs": "^17.7.2",
|
|
42
|
-
"@react-native-harness/
|
|
43
|
-
"@react-native-harness/
|
|
44
|
-
"@react-native-harness/
|
|
45
|
-
"@react-native-harness/
|
|
46
|
-
"@react-native-harness/
|
|
47
|
-
"@react-native-harness/
|
|
48
|
-
"@react-native-harness/
|
|
42
|
+
"@react-native-harness/config": "1.7.0",
|
|
43
|
+
"@react-native-harness/tools": "1.7.0",
|
|
44
|
+
"@react-native-harness/platforms": "1.7.0",
|
|
45
|
+
"@react-native-harness/bundler-metro": "1.7.0",
|
|
46
|
+
"@react-native-harness/plugins": "1.7.0",
|
|
47
|
+
"@react-native-harness/cache": "1.7.0",
|
|
48
|
+
"@react-native-harness/bridge": "1.7.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/yargs": "^17.0.32",
|
package/src/cli-args.ts
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { harnessCliOptions } from '@react-native-harness/config';
|
|
2
|
+
import yargs, { type InferredOptionTypes } from 'yargs';
|
|
2
3
|
import { hideBin } from 'yargs/helpers';
|
|
3
4
|
|
|
4
|
-
export type HarnessCliArgs =
|
|
5
|
-
harnessRunner?: string;
|
|
6
|
-
metroPort?: number;
|
|
7
|
-
};
|
|
5
|
+
export type HarnessCliArgs = InferredOptionTypes<typeof harnessCliOptions>;
|
|
8
6
|
|
|
9
7
|
export const getAdditionalCliArgs = (): HarnessCliArgs => {
|
|
10
8
|
const argv = yargs(hideBin(process.argv))
|
|
11
|
-
.
|
|
12
|
-
type: 'string',
|
|
13
|
-
description: 'Specify which Harness runner to use',
|
|
14
|
-
})
|
|
15
|
-
.option('metroPort', {
|
|
16
|
-
type: 'number',
|
|
17
|
-
description: 'Override the Metro bundler port',
|
|
18
|
-
})
|
|
9
|
+
.options(harnessCliOptions)
|
|
19
10
|
.strict(false)
|
|
20
11
|
.help(false)
|
|
21
12
|
.version(false)
|
|
22
13
|
.exitProcess(false)
|
|
14
|
+
.fail(false)
|
|
23
15
|
.parseSync();
|
|
24
16
|
|
|
25
17
|
return {
|
package/src/diagnostics/index.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
export { observeMetroEvents, observeBridgeEvents } from './derivers.js';
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
writeTraceFile,
|
|
4
|
+
appendToTraceFile,
|
|
5
|
+
printSummary,
|
|
6
|
+
createDiagnosticsAppender,
|
|
7
|
+
summarizeRunKpis,
|
|
8
|
+
writeSummaryFile,
|
|
9
|
+
appendStepSummary,
|
|
10
|
+
} from './report.js';
|
|
11
|
+
export type { DiagnosticsAppender, RunKpis } from './report.js';
|
|
12
|
+
export { startSessionTelemetry } from './telemetry.js';
|
|
13
|
+
export type { SessionTelemetry } from './telemetry.js';
|