@react-native-harness/runtime 1.0.0-alpha.2 → 1.0.0-alpha.21
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/README.md +23 -4
- package/assets/harness-module-system.js +73 -0
- package/dist/bundler/bundle.d.ts.map +1 -1
- package/dist/bundler/bundle.js +7 -2
- package/dist/bundler/errors.d.ts +5 -0
- package/dist/bundler/errors.d.ts.map +1 -1
- package/dist/bundler/errors.js +11 -0
- package/dist/bundler/evaluate.d.ts.map +1 -1
- package/dist/bundler/evaluate.js +7 -7
- package/dist/bundler/factory.d.ts +3 -0
- package/dist/bundler/factory.d.ts.map +1 -0
- package/dist/bundler/factory.js +36 -0
- package/dist/bundler/index.d.ts +2 -1
- package/dist/bundler/index.d.ts.map +1 -1
- package/dist/bundler/index.js +1 -1
- package/dist/bundler/types.d.ts +7 -0
- package/dist/bundler/types.d.ts.map +1 -0
- package/dist/bundler/types.js +1 -0
- package/dist/client/factory.d.ts.map +1 -1
- package/dist/client/factory.js +34 -6
- package/dist/client/getDeviceDescriptor.d.ts +1 -1
- package/dist/client/getDeviceDescriptor.d.ts.map +1 -1
- package/dist/client/getDeviceDescriptor.js +18 -6
- package/dist/client/getWSServer.d.ts.map +1 -1
- package/dist/client/getWSServer.js +2 -1
- package/dist/client/setup-files.d.ts +12 -0
- package/dist/client/setup-files.d.ts.map +1 -0
- package/dist/client/setup-files.js +60 -0
- package/dist/collector/functions.d.ts +1 -1
- package/dist/collector/functions.d.ts.map +1 -1
- package/dist/collector/functions.js +10 -2
- package/dist/collector/types.d.ts +1 -1
- package/dist/collector/types.d.ts.map +1 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -1
- package/dist/disableHMRWhenReady.d.ts +2 -0
- package/dist/disableHMRWhenReady.d.ts.map +1 -0
- package/dist/disableHMRWhenReady.js +20 -0
- package/dist/entry-point.d.ts +2 -0
- package/dist/entry-point.d.ts.map +1 -0
- package/dist/entry-point.js +4 -0
- package/dist/expect/index.d.ts.map +1 -1
- package/dist/expect/index.js +2 -0
- package/dist/expect/setup.js +2 -0
- package/dist/filtering/index.d.ts +2 -0
- package/dist/filtering/index.d.ts.map +1 -0
- package/dist/filtering/index.js +1 -0
- package/dist/filtering/testNameFilter.d.ts +12 -0
- package/dist/filtering/testNameFilter.d.ts.map +1 -0
- package/dist/filtering/testNameFilter.js +56 -0
- package/dist/globals.d.ts +6 -2
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js +7 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/initialize.js +14 -6
- package/dist/jest-mock.d.ts +2 -0
- package/dist/jest-mock.d.ts.map +1 -0
- package/dist/jest-mock.js +25 -0
- package/dist/mocker/index.d.ts +1 -1
- package/dist/mocker/index.d.ts.map +1 -1
- package/dist/mocker/index.js +1 -1
- package/dist/mocker/registry.d.ts +2 -3
- package/dist/mocker/registry.d.ts.map +1 -1
- package/dist/mocker/registry.js +25 -16
- package/dist/namespace.d.ts +18 -0
- package/dist/namespace.d.ts.map +1 -0
- package/dist/namespace.js +19 -0
- package/dist/polyfills.d.ts +11 -0
- package/dist/polyfills.d.ts.map +1 -0
- package/dist/polyfills.js +13 -0
- package/dist/render/ErrorBoundary.d.ts +17 -0
- package/dist/render/ErrorBoundary.d.ts.map +1 -0
- package/dist/render/ErrorBoundary.js +73 -0
- package/dist/render/TestComponentOverlay.d.ts +3 -0
- package/dist/render/TestComponentOverlay.d.ts.map +1 -0
- package/dist/render/TestComponentOverlay.js +36 -0
- package/dist/render/cleanup.d.ts +2 -0
- package/dist/render/cleanup.d.ts.map +1 -0
- package/dist/render/cleanup.js +6 -0
- package/dist/render/index.d.ts +6 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +66 -0
- package/dist/render/setup.d.ts +2 -0
- package/dist/render/setup.d.ts.map +1 -0
- package/dist/render/setup.js +7 -0
- package/dist/render/types.d.ts +12 -0
- package/dist/render/types.d.ts.map +1 -0
- package/dist/render/types.js +1 -0
- package/dist/runner/errors.d.ts +4 -2
- package/dist/runner/errors.d.ts.map +1 -1
- package/dist/runner/errors.js +21 -3
- package/dist/runner/factory.d.ts.map +1 -1
- package/dist/runner/factory.js +6 -1
- package/dist/runner/runSuite.d.ts.map +1 -1
- package/dist/runner/runSuite.js +59 -7
- package/dist/symbolicate.d.ts +3 -0
- package/dist/symbolicate.d.ts.map +1 -0
- package/dist/symbolicate.js +19 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/ui/ReadyScreen.d.ts.map +1 -1
- package/dist/ui/ReadyScreen.js +3 -10
- package/dist/ui/WrongEnvironmentScreen.d.ts.map +1 -1
- package/dist/ui/WrongEnvironmentScreen.js +2 -10
- package/dist/ui/state.d.ts +14 -1
- package/dist/ui/state.d.ts.map +1 -1
- package/dist/ui/state.js +22 -0
- package/dist/utils/emitter.d.ts.map +1 -1
- package/dist/waitFor.d.ts +21 -0
- package/dist/waitFor.d.ts.map +1 -0
- package/dist/waitFor.js +137 -0
- package/eslint.config.mjs +1 -7
- package/package.json +22 -14
- package/src/__tests__/collector.test.ts +55 -55
- package/src/__tests__/error-handling.test.ts +34 -34
- package/src/__tests__/expect.test.ts +13 -5
- package/src/__tests__/initialize.test.ts +24 -0
- package/src/bundler/bundle.ts +9 -2
- package/src/bundler/errors.ts +11 -0
- package/src/bundler/evaluate.ts +9 -9
- package/src/bundler/factory.ts +43 -0
- package/src/bundler/index.ts +2 -1
- package/src/bundler/types.ts +7 -0
- package/src/client/factory.ts +53 -11
- package/src/client/getDeviceDescriptor.ts +29 -8
- package/src/client/getWSServer.ts +2 -1
- package/src/client/setup-files.ts +81 -0
- package/src/collector/functions.ts +18 -2
- package/src/collector/types.ts +4 -1
- package/src/constants.ts +0 -1
- package/src/disableHMRWhenReady.ts +27 -0
- package/src/entry-point.ts +8 -0
- package/src/expect/index.ts +8 -2
- package/src/expect/setup.ts +3 -0
- package/src/filtering/index.ts +4 -0
- package/src/filtering/testNameFilter.ts +82 -0
- package/src/globals.ts +15 -2
- package/src/index.ts +4 -0
- package/src/initialize.ts +21 -8
- package/src/jest-mock.ts +32 -0
- package/src/mocker/index.ts +6 -1
- package/src/mocker/metro-require.d.ts +2 -0
- package/src/mocker/registry.ts +29 -18
- package/src/namespace.ts +41 -0
- package/src/polyfills.ts +14 -0
- package/src/react-native.d.ts +35 -6
- package/src/render/ErrorBoundary.tsx +108 -0
- package/src/render/TestComponentOverlay.tsx +47 -0
- package/src/render/cleanup.ts +7 -0
- package/src/render/index.ts +96 -0
- package/src/render/setup.ts +8 -0
- package/src/render/types.ts +11 -0
- package/src/runner/errors.ts +35 -5
- package/src/runner/factory.ts +8 -1
- package/src/runner/runSuite.ts +70 -9
- package/src/symbolicate.ts +24 -0
- package/src/ui/ReadyScreen.tsx +2 -12
- package/src/ui/WrongEnvironmentScreen.tsx +1 -19
- package/src/ui/state.ts +39 -0
- package/src/utils/emitter.ts +1 -0
- package/src/waitFor.ts +199 -0
- package/tsconfig.spec.json +7 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/assets/logo.png +0 -0
- package/assets/moduleSystem.flow.js +0 -1062
- package/types/global.d.ts +0 -2
- package/types/index.d.ts +0 -1
|
@@ -72,7 +72,9 @@ describe('expect - Truthiness', () => {
|
|
|
72
72
|
expect('hello').toBeTruthy();
|
|
73
73
|
expect({}).toBeTruthy();
|
|
74
74
|
expect([]).toBeTruthy();
|
|
75
|
-
expect(function () {
|
|
75
|
+
expect(function () {
|
|
76
|
+
// noop
|
|
77
|
+
}).toBeTruthy();
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
test('should fail for falsy values', () => {
|
|
@@ -330,7 +332,9 @@ describe('expect - Type Checking', () => {
|
|
|
330
332
|
expect(true).toBeTypeOf('boolean');
|
|
331
333
|
expect(undefined).toBeTypeOf('undefined');
|
|
332
334
|
expect(Symbol('test')).toBeTypeOf('symbol');
|
|
333
|
-
expect(() => {
|
|
335
|
+
expect(() => {
|
|
336
|
+
// noop
|
|
337
|
+
}).toBeTypeOf('function');
|
|
334
338
|
expect({}).toBeTypeOf('object');
|
|
335
339
|
});
|
|
336
340
|
|
|
@@ -406,7 +410,11 @@ describe('expect - Exceptions', () => {
|
|
|
406
410
|
});
|
|
407
411
|
|
|
408
412
|
test('should fail for functions that do not throw', () => {
|
|
409
|
-
expect(() =>
|
|
413
|
+
expect(() =>
|
|
414
|
+
expect(() => {
|
|
415
|
+
// noop
|
|
416
|
+
}).toThrow()
|
|
417
|
+
).toThrow();
|
|
410
418
|
});
|
|
411
419
|
|
|
412
420
|
test('should fail for wrong error message', () => {
|
|
@@ -558,10 +566,10 @@ describe('expect - Custom Messages', () => {
|
|
|
558
566
|
|
|
559
567
|
describe('expect - Edge Cases', () => {
|
|
560
568
|
test('should handle circular references', () => {
|
|
561
|
-
const circular:
|
|
569
|
+
const circular: Record<string, unknown> = { a: 1 };
|
|
562
570
|
circular.self = circular;
|
|
563
571
|
|
|
564
|
-
const circular2:
|
|
572
|
+
const circular2: Record<string, unknown> = { a: 1 };
|
|
565
573
|
circular2.self = circular2;
|
|
566
574
|
|
|
567
575
|
expect(circular).toEqual(circular2);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { disableHMRWhenReady } from '../disableHMRWhenReady.js';
|
|
4
|
+
|
|
5
|
+
describe('initialize', () => {
|
|
6
|
+
it('retries HMRClient.disable until setup is ready', async () => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
|
|
9
|
+
const disable = vi
|
|
10
|
+
.fn()
|
|
11
|
+
.mockImplementationOnce(() => {
|
|
12
|
+
throw new Error('Expected HMRClient.setup() call at startup.');
|
|
13
|
+
})
|
|
14
|
+
.mockImplementationOnce(() => {
|
|
15
|
+
// ok
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const promise = disableHMRWhenReady(disable, 50);
|
|
19
|
+
await vi.runAllTimersAsync();
|
|
20
|
+
await promise;
|
|
21
|
+
|
|
22
|
+
expect(disable).toHaveBeenCalledTimes(2);
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/bundler/bundle.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import { getDevServerUrl } from '../utils/dev-server.js';
|
|
3
|
+
import { BundlingFailedError } from './errors.js';
|
|
3
4
|
|
|
4
5
|
const getModuleUrl = (fileName: string): string => {
|
|
5
6
|
const devServerUrl = getDevServerUrl();
|
|
@@ -9,11 +10,17 @@ const getModuleUrl = (fileName: string): string => {
|
|
|
9
10
|
platform: Platform.OS,
|
|
10
11
|
});
|
|
11
12
|
|
|
12
|
-
return `${devServerUrl}
|
|
13
|
+
return `${devServerUrl}${bundleName}?${urlSearchParams.toString()}`;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export const fetchModule = async (fileName: string): Promise<string> => {
|
|
16
17
|
const url = getModuleUrl(fileName);
|
|
17
18
|
const response = await fetch(url);
|
|
18
|
-
|
|
19
|
+
const text = await response.text();
|
|
20
|
+
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new BundlingFailedError(fileName, text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return text;
|
|
19
26
|
};
|
package/src/bundler/errors.ts
CHANGED
|
@@ -14,3 +14,14 @@ export class MalformedModuleError extends Error {
|
|
|
14
14
|
this.name = 'MalformedModuleError';
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
export class BundlingFailedError extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
public readonly modulePath: string,
|
|
21
|
+
public readonly reason: string
|
|
22
|
+
) {
|
|
23
|
+
const reasonMessage = JSON.parse(reason).message ?? reason;
|
|
24
|
+
super(`Bundling of ${modulePath} failed with error:\n\n${reasonMessage}\n`);
|
|
25
|
+
this.name = 'BundlingFailedError';
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/bundler/evaluate.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { MalformedModuleError } from './errors.js';
|
|
2
|
-
import { EnvironmentError } from '../errors.js';
|
|
3
2
|
|
|
4
3
|
export const evaluateModule = (moduleJs: string, modulePath: string): void => {
|
|
5
|
-
const
|
|
4
|
+
const __rMatches = Array.from(moduleJs.matchAll(/__r\((\d+)\)/g));
|
|
6
5
|
|
|
7
|
-
if (
|
|
6
|
+
if (__rMatches.length === 0) {
|
|
8
7
|
throw new MalformedModuleError(modulePath, 'No __r function found');
|
|
9
8
|
}
|
|
10
9
|
|
|
10
|
+
// Get the last match as there may be many require calls
|
|
11
|
+
const __rMatch = __rMatches[__rMatches.length - 1];
|
|
11
12
|
const __rParam = __rMatch[1];
|
|
12
13
|
|
|
13
14
|
if (!__rParam) {
|
|
14
15
|
throw new MalformedModuleError(modulePath, 'No __r parameter found');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
eval(moduleJs);
|
|
18
|
+
const moduleId = Number(__rParam);
|
|
19
19
|
|
|
20
|
-
if
|
|
21
|
-
|
|
22
|
-
}
|
|
20
|
+
// This is important as if module was already initialized, it would not be re-initialized
|
|
21
|
+
global.__resetModule(moduleId);
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
// eslint-disable-next-line no-eval
|
|
24
|
+
eval(moduleJs);
|
|
25
25
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BundlerEvents } from '@react-native-harness/bridge';
|
|
2
|
+
import { getEmitter } from '../utils/emitter.js';
|
|
3
|
+
import { Bundler } from './types.js';
|
|
4
|
+
import { fetchModule } from './bundle.js';
|
|
5
|
+
import { BundlingFailedError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
export const getBundler = (): Bundler => {
|
|
8
|
+
const events = getEmitter<BundlerEvents>();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
events,
|
|
12
|
+
getModule: async (filePath) => {
|
|
13
|
+
const bundlingStartTime = Date.now();
|
|
14
|
+
events.emit({
|
|
15
|
+
type: 'module-bundling-started',
|
|
16
|
+
file: filePath,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const moduleJs = await fetchModule(filePath);
|
|
21
|
+
events.emit({
|
|
22
|
+
type: 'module-bundling-finished',
|
|
23
|
+
file: filePath,
|
|
24
|
+
duration: Date.now() - bundlingStartTime,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return moduleJs;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
events.emit({
|
|
30
|
+
type: 'module-bundling-failed',
|
|
31
|
+
file: filePath,
|
|
32
|
+
duration: Date.now() - bundlingStartTime,
|
|
33
|
+
error:
|
|
34
|
+
error instanceof BundlingFailedError
|
|
35
|
+
? error.reason
|
|
36
|
+
: 'Unknown error',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
package/src/bundler/index.ts
CHANGED
package/src/client/factory.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
TestRunnerEvents,
|
|
3
3
|
TestCollectorEvents,
|
|
4
|
+
BundlerEvents,
|
|
5
|
+
TestExecutionOptions,
|
|
4
6
|
} from '@react-native-harness/bridge';
|
|
5
7
|
import { getBridgeClient } from '@react-native-harness/bridge/client';
|
|
6
8
|
import { store } from '../ui/state.js';
|
|
@@ -8,7 +10,10 @@ import { getTestRunner, TestRunner } from '../runner/index.js';
|
|
|
8
10
|
import { getTestCollector, TestCollector } from '../collector/index.js';
|
|
9
11
|
import { combineEventEmitters, EventEmitter } from '../utils/emitter.js';
|
|
10
12
|
import { getWSServer } from './getWSServer.js';
|
|
11
|
-
import {
|
|
13
|
+
import { getBundler, evaluateModule, Bundler } from '../bundler/index.js';
|
|
14
|
+
import { markTestsAsSkippedByName } from '../filtering/index.js';
|
|
15
|
+
import { setup } from '../render/setup.js';
|
|
16
|
+
import { runSetupFiles } from './setup-files.js';
|
|
12
17
|
|
|
13
18
|
export const getClient = async () => {
|
|
14
19
|
const client = await getBridgeClient(getWSServer(), {
|
|
@@ -17,7 +22,10 @@ export const getClient = async () => {
|
|
|
17
22
|
},
|
|
18
23
|
});
|
|
19
24
|
|
|
20
|
-
client.rpc.$functions.runTests = async (
|
|
25
|
+
client.rpc.$functions.runTests = async (
|
|
26
|
+
path: string,
|
|
27
|
+
options: TestExecutionOptions = {}
|
|
28
|
+
) => {
|
|
21
29
|
if (store.getState().status === 'running') {
|
|
22
30
|
throw new Error('Already running tests');
|
|
23
31
|
}
|
|
@@ -26,24 +34,58 @@ export const getClient = async () => {
|
|
|
26
34
|
|
|
27
35
|
let collector: TestCollector | null = null;
|
|
28
36
|
let runner: TestRunner | null = null;
|
|
29
|
-
let events: EventEmitter<
|
|
30
|
-
|
|
37
|
+
let events: EventEmitter<
|
|
38
|
+
TestRunnerEvents | TestCollectorEvents | BundlerEvents
|
|
39
|
+
> | null = null;
|
|
40
|
+
let bundler: Bundler | null = null;
|
|
31
41
|
|
|
32
42
|
try {
|
|
33
43
|
collector = getTestCollector();
|
|
34
44
|
runner = getTestRunner();
|
|
35
|
-
|
|
45
|
+
bundler = getBundler();
|
|
46
|
+
events = combineEventEmitters(
|
|
47
|
+
collector.events,
|
|
48
|
+
runner.events,
|
|
49
|
+
bundler.events
|
|
50
|
+
);
|
|
36
51
|
|
|
37
52
|
events.addListener((event) => {
|
|
38
53
|
client.rpc.emitEvent(event.type, event);
|
|
39
54
|
});
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
await runSetupFiles({
|
|
57
|
+
setupFiles: options.setupFiles ?? [],
|
|
58
|
+
setupFilesAfterEnv: [],
|
|
59
|
+
events: events as EventEmitter<BundlerEvents>,
|
|
60
|
+
bundler: bundler as Bundler,
|
|
61
|
+
evaluateModule,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const moduleJs = await bundler.getModule(path);
|
|
65
|
+
const collectionResult = await collector.collect(async () => {
|
|
66
|
+
await runSetupFiles({
|
|
67
|
+
setupFiles: [],
|
|
68
|
+
setupFilesAfterEnv: options.setupFilesAfterEnv ?? [],
|
|
69
|
+
events: events as EventEmitter<BundlerEvents>,
|
|
70
|
+
bundler: bundler as Bundler,
|
|
71
|
+
evaluateModule,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Setup automatic cleanup for rendered components
|
|
75
|
+
setup();
|
|
76
|
+
evaluateModule(moduleJs, path);
|
|
77
|
+
}, path);
|
|
78
|
+
|
|
79
|
+
// Apply test name pattern by marking non-matching tests as skipped
|
|
80
|
+
const processedTestSuite = options.testNamePattern
|
|
81
|
+
? markTestsAsSkippedByName(
|
|
82
|
+
collectionResult.testSuite,
|
|
83
|
+
options.testNamePattern
|
|
84
|
+
)
|
|
85
|
+
: collectionResult.testSuite;
|
|
86
|
+
|
|
87
|
+
const result = await runner.run(processedTestSuite, path);
|
|
88
|
+
return result;
|
|
47
89
|
} finally {
|
|
48
90
|
collector?.dispose();
|
|
49
91
|
runner?.dispose();
|
|
@@ -1,28 +1,49 @@
|
|
|
1
|
-
import { Platform } from 'react-native';
|
|
1
|
+
import { Platform, PlatformConstants, PlatformStatic } from 'react-native';
|
|
2
|
+
|
|
3
|
+
interface PlatformKeplerStatic extends PlatformStatic {
|
|
4
|
+
constants: PlatformConstants;
|
|
5
|
+
OS: 'kepler';
|
|
6
|
+
Version: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const getPlatform = (): Platform | PlatformKeplerStatic => {
|
|
10
|
+
return Platform as Platform | PlatformKeplerStatic;
|
|
11
|
+
};
|
|
2
12
|
|
|
3
13
|
export type DeviceDescriptor = {
|
|
4
|
-
platform: 'ios' | 'android';
|
|
14
|
+
platform: 'ios' | 'android' | 'vega';
|
|
5
15
|
manufacturer: string;
|
|
6
16
|
model: string;
|
|
7
17
|
osVersion: string;
|
|
8
18
|
};
|
|
9
19
|
|
|
10
20
|
export const getDeviceDescriptor = (): DeviceDescriptor => {
|
|
11
|
-
|
|
21
|
+
const platform = getPlatform();
|
|
22
|
+
|
|
23
|
+
if (platform.OS === 'ios') {
|
|
12
24
|
return {
|
|
13
25
|
platform: 'ios',
|
|
14
26
|
manufacturer: 'Apple',
|
|
15
27
|
model: 'Unknown',
|
|
16
|
-
osVersion:
|
|
28
|
+
osVersion: platform.constants.osVersion,
|
|
17
29
|
};
|
|
18
30
|
}
|
|
19
31
|
|
|
20
|
-
if (
|
|
32
|
+
if (platform.OS === 'android') {
|
|
21
33
|
return {
|
|
22
34
|
platform: 'android',
|
|
23
|
-
manufacturer:
|
|
24
|
-
model:
|
|
25
|
-
osVersion:
|
|
35
|
+
manufacturer: platform.constants.Manufacturer,
|
|
36
|
+
model: platform.constants.Model,
|
|
37
|
+
osVersion: platform.constants.Release,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (platform.OS === 'kepler') {
|
|
42
|
+
return {
|
|
43
|
+
platform: 'vega',
|
|
44
|
+
manufacturer: '',
|
|
45
|
+
model: '',
|
|
46
|
+
osVersion: '',
|
|
26
47
|
};
|
|
27
48
|
}
|
|
28
49
|
|
|
@@ -4,6 +4,7 @@ import { WS_SERVER_PORT } from '../constants.js';
|
|
|
4
4
|
export const getWSServer = (): string => {
|
|
5
5
|
const devServerUrl = getDevServerUrl();
|
|
6
6
|
const hostname = devServerUrl.split('://')[1].split(':')[0];
|
|
7
|
+
const port = global.RN_HARNESS?.webSocketPort || WS_SERVER_PORT;
|
|
7
8
|
|
|
8
|
-
return `ws://${hostname}:${
|
|
9
|
+
return `ws://${hostname}:${port}`;
|
|
9
10
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { EventEmitter } from '../utils/emitter.js';
|
|
2
|
+
import { Bundler } from '../bundler/index.js';
|
|
3
|
+
import { BundlerEvents } from '@react-native-harness/bridge';
|
|
4
|
+
|
|
5
|
+
export type RunSetupFilesOptions = {
|
|
6
|
+
setupFiles: string[];
|
|
7
|
+
setupFilesAfterEnv: string[];
|
|
8
|
+
events: EventEmitter<BundlerEvents>;
|
|
9
|
+
bundler: Bundler;
|
|
10
|
+
evaluateModule: (moduleJs: string, filePath: string) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const runSetupFiles = async ({
|
|
14
|
+
setupFiles,
|
|
15
|
+
setupFilesAfterEnv,
|
|
16
|
+
events,
|
|
17
|
+
bundler,
|
|
18
|
+
evaluateModule,
|
|
19
|
+
}: RunSetupFilesOptions) => {
|
|
20
|
+
for (const setupFile of setupFiles) {
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
events.emit({
|
|
23
|
+
type: 'setup-file-bundling-started',
|
|
24
|
+
file: setupFile,
|
|
25
|
+
setupType: 'setupFiles',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const setupModuleJs = await bundler.getModule(setupFile);
|
|
30
|
+
events.emit({
|
|
31
|
+
type: 'setup-file-bundling-finished',
|
|
32
|
+
file: setupFile,
|
|
33
|
+
setupType: 'setupFiles',
|
|
34
|
+
duration: Date.now() - startTime,
|
|
35
|
+
});
|
|
36
|
+
evaluateModule(setupModuleJs, setupFile);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const errorMessage =
|
|
39
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
40
|
+
events.emit({
|
|
41
|
+
type: 'setup-file-bundling-failed',
|
|
42
|
+
file: setupFile,
|
|
43
|
+
setupType: 'setupFiles',
|
|
44
|
+
duration: Date.now() - startTime,
|
|
45
|
+
error: errorMessage,
|
|
46
|
+
});
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const setupFile of setupFilesAfterEnv) {
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
events.emit({
|
|
54
|
+
type: 'setup-file-bundling-started',
|
|
55
|
+
file: setupFile,
|
|
56
|
+
setupType: 'setupFilesAfterEnv',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const setupModuleJs = await bundler.getModule(setupFile);
|
|
61
|
+
events.emit({
|
|
62
|
+
type: 'setup-file-bundling-finished',
|
|
63
|
+
file: setupFile,
|
|
64
|
+
setupType: 'setupFilesAfterEnv',
|
|
65
|
+
duration: Date.now() - startTime,
|
|
66
|
+
});
|
|
67
|
+
evaluateModule(setupModuleJs, setupFile);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const errorMessage =
|
|
70
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
71
|
+
events.emit({
|
|
72
|
+
type: 'setup-file-bundling-failed',
|
|
73
|
+
file: setupFile,
|
|
74
|
+
setupType: 'setupFilesAfterEnv',
|
|
75
|
+
duration: Date.now() - startTime,
|
|
76
|
+
error: errorMessage,
|
|
77
|
+
});
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
@@ -53,7 +53,21 @@ const computeSuiteStatus = (
|
|
|
53
53
|
): TestStatus => {
|
|
54
54
|
if (suite.options.skip) return 'skipped';
|
|
55
55
|
if (suite.options.only) return 'active';
|
|
56
|
+
|
|
57
|
+
// Check if this suite has any focused content (tests or child suites)
|
|
58
|
+
const hasFocusedTests = suite.tests.some((test) => test.options.only);
|
|
59
|
+
const hasFocusedChildren = suite.suites.some(
|
|
60
|
+
(childSuite) =>
|
|
61
|
+
childSuite.options.only ||
|
|
62
|
+
childSuite.tests.some((test) => test.options.only)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// If this suite has focused content, it should be active
|
|
66
|
+
if (hasFocusedTests || hasFocusedChildren) return 'active';
|
|
67
|
+
|
|
68
|
+
// If parent has focused children and this suite has no focused content, skip it
|
|
56
69
|
if (parentContext.hasFocusedChildren) return 'skipped';
|
|
70
|
+
|
|
57
71
|
return 'active';
|
|
58
72
|
};
|
|
59
73
|
|
|
@@ -356,11 +370,13 @@ const countTests = (suite: TestSuite): number => {
|
|
|
356
370
|
return count;
|
|
357
371
|
};
|
|
358
372
|
|
|
359
|
-
export const collectTests =
|
|
373
|
+
export const collectTests = async (
|
|
374
|
+
fn: () => void | Promise<void>
|
|
375
|
+
): Promise<CollectionResult> => {
|
|
360
376
|
currentContext = clearState();
|
|
361
377
|
|
|
362
378
|
try {
|
|
363
|
-
fn();
|
|
379
|
+
await fn();
|
|
364
380
|
|
|
365
381
|
// Convert raw structure to final structure using computation phase
|
|
366
382
|
const testSuite = convertRawTestSuiteToTestSuite(getRootSuite());
|
package/src/collector/types.ts
CHANGED
|
@@ -10,6 +10,9 @@ export type TestCollectorEventsEmitter = EventEmitter<TestCollectorEvents>;
|
|
|
10
10
|
|
|
11
11
|
export type TestCollector = {
|
|
12
12
|
events: TestCollectorEventsEmitter;
|
|
13
|
-
collect: (
|
|
13
|
+
collect: (
|
|
14
|
+
fn: () => void | Promise<void>,
|
|
15
|
+
testFilePath: string
|
|
16
|
+
) => Promise<CollectionResult>;
|
|
14
17
|
dispose: () => void;
|
|
15
18
|
};
|
package/src/constants.ts
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function disableHMRWhenReady(
|
|
2
|
+
disable: () => void,
|
|
3
|
+
retriesLeft: number,
|
|
4
|
+
retryDelay = 10
|
|
5
|
+
) {
|
|
6
|
+
return new Promise<void>((resolve, reject) => {
|
|
7
|
+
function attempt(remaining: number) {
|
|
8
|
+
try {
|
|
9
|
+
disable();
|
|
10
|
+
resolve();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (
|
|
13
|
+
remaining > 0 &&
|
|
14
|
+
error instanceof Error &&
|
|
15
|
+
error.message.includes('Expected HMRClient.setup() call at startup.')
|
|
16
|
+
) {
|
|
17
|
+
setTimeout(() => attempt(remaining - 1), retryDelay);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
reject(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
attempt(retriesLeft);
|
|
26
|
+
});
|
|
27
|
+
}
|
package/src/expect/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This is adapted version of https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/integrations/chai/index.ts
|
|
2
|
+
// Credits to Vitest team for the original implementation.
|
|
3
|
+
|
|
1
4
|
import type { Assertion, ExpectStatic, MatcherState } from '@vitest/expect';
|
|
2
5
|
import {
|
|
3
6
|
addCustomEqualityTesters,
|
|
@@ -13,13 +16,16 @@ import * as chai from 'chai';
|
|
|
13
16
|
import './setup.js';
|
|
14
17
|
|
|
15
18
|
export function createExpect(): ExpectStatic {
|
|
16
|
-
const expect = ((value:
|
|
19
|
+
const expect = ((value: unknown, message?: string): Assertion => {
|
|
17
20
|
const { assertionCalls } = getState(expect);
|
|
18
21
|
setState({ assertionCalls: assertionCalls + 1 }, expect);
|
|
19
22
|
return chai.expect(value, message) as unknown as Assertion;
|
|
20
23
|
}) as ExpectStatic;
|
|
21
24
|
Object.assign(expect, chai.expect);
|
|
22
|
-
Object.assign(
|
|
25
|
+
Object.assign(
|
|
26
|
+
expect,
|
|
27
|
+
globalThis[ASYMMETRIC_MATCHERS_OBJECT as unknown as keyof typeof globalThis]
|
|
28
|
+
);
|
|
23
29
|
|
|
24
30
|
expect.getState = () => getState<MatcherState>(expect);
|
|
25
31
|
expect.setState = (state) => setState(state as Partial<MatcherState>, expect);
|
package/src/expect/setup.ts
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { TestSuite } from '@react-native-harness/bridge';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filters tests by name pattern, matching against test names and suite+test combinations
|
|
5
|
+
* @deprecated Use markTestsAsSkippedByName instead - this function will be removed in a future version
|
|
6
|
+
*/
|
|
7
|
+
export const filterTestsByName = (
|
|
8
|
+
suite: TestSuite,
|
|
9
|
+
testNamePattern: string
|
|
10
|
+
): TestSuite => {
|
|
11
|
+
const regex = new RegExp(testNamePattern);
|
|
12
|
+
return filterSuiteRecursively(suite, regex);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Marks tests as skipped based on name pattern, keeping all tests in the structure
|
|
17
|
+
* but setting non-matching tests to 'skipped' status
|
|
18
|
+
*/
|
|
19
|
+
export const markTestsAsSkippedByName = (
|
|
20
|
+
suite: TestSuite,
|
|
21
|
+
testNamePattern: string
|
|
22
|
+
): TestSuite => {
|
|
23
|
+
const regex = new RegExp(testNamePattern);
|
|
24
|
+
return markTestsRecursively(suite, regex);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const markTestsRecursively = (suite: TestSuite, regex: RegExp): TestSuite => {
|
|
28
|
+
// Mark tests in current suite - skip tests that don't match the pattern
|
|
29
|
+
const updatedTests = suite.tests.map((test) => {
|
|
30
|
+
const matches =
|
|
31
|
+
regex.test(test.name) || regex.test(`${suite.name} ${test.name}`);
|
|
32
|
+
|
|
33
|
+
// If test doesn't match pattern and is currently active, mark it as skipped
|
|
34
|
+
if (!matches && test.status === 'active') {
|
|
35
|
+
return {
|
|
36
|
+
...test,
|
|
37
|
+
status: 'skipped' as const,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Keep original status for matching tests or already skipped/todo tests
|
|
42
|
+
return test;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Recursively process child suites
|
|
46
|
+
const updatedChildSuites = suite.suites.map((childSuite) =>
|
|
47
|
+
markTestsRecursively(childSuite, regex)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...suite,
|
|
52
|
+
tests: updatedTests,
|
|
53
|
+
suites: updatedChildSuites,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const filterSuiteRecursively = (suite: TestSuite, regex: RegExp): TestSuite => {
|
|
58
|
+
// Filter tests in current suite - match against test name or "suite test" combination
|
|
59
|
+
const filteredTests = suite.tests.filter(
|
|
60
|
+
(test) => regex.test(test.name) || regex.test(`${suite.name} ${test.name}`)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Recursively filter child suites
|
|
64
|
+
const filteredChildSuites = suite.suites
|
|
65
|
+
.map((childSuite) => filterSuiteRecursively(childSuite, regex))
|
|
66
|
+
.filter((childSuite) => hasAnyActiveTests(childSuite));
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...suite,
|
|
70
|
+
tests: filteredTests,
|
|
71
|
+
suites: filteredChildSuites,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const hasAnyActiveTests = (suite: TestSuite): boolean => {
|
|
76
|
+
const hasDirectTests = suite.tests.some((test) => test.status === 'active');
|
|
77
|
+
const hasChildTests = suite.suites.some((childSuite) =>
|
|
78
|
+
hasAnyActiveTests(childSuite)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return hasDirectTests || hasChildTests;
|
|
82
|
+
};
|