@react-native-harness/runtime 1.0.0-alpha.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/.babelrc.js +23 -0
- package/LICENSE +20 -0
- package/README.md +7 -0
- package/assets/logo.png +0 -0
- package/assets/moduleSystem.flow.js +1062 -0
- package/dist/bundler/bundle.d.ts +2 -0
- package/dist/bundler/bundle.d.ts.map +1 -0
- package/dist/bundler/bundle.js +16 -0
- package/dist/bundler/dev-server.d.ts +2 -0
- package/dist/bundler/dev-server.d.ts.map +1 -0
- package/dist/bundler/dev-server.js +5 -0
- package/dist/bundler/errors.d.ts +10 -0
- package/dist/bundler/errors.d.ts.map +1 -0
- package/dist/bundler/errors.js +18 -0
- package/dist/bundler/evaluate.d.ts +2 -0
- package/dist/bundler/evaluate.d.ts.map +1 -0
- package/dist/bundler/evaluate.js +18 -0
- package/dist/bundler/index.d.ts +3 -0
- package/dist/bundler/index.d.ts.map +1 -0
- package/dist/bundler/index.js +2 -0
- package/dist/client/factory.d.ts +2 -0
- package/dist/client/factory.d.ts.map +1 -0
- package/dist/client/factory.js +41 -0
- package/dist/client/getDeviceDescriptor.d.ts +8 -0
- package/dist/client/getDeviceDescriptor.d.ts.map +1 -0
- package/dist/client/getDeviceDescriptor.js +20 -0
- package/dist/client/getWSServer.d.ts +2 -0
- package/dist/client/getWSServer.d.ts.map +1 -0
- package/dist/client/getWSServer.js +7 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +1 -0
- package/dist/collector/errors.d.ts +8 -0
- package/dist/collector/errors.d.ts.map +1 -0
- package/dist/collector/errors.js +20 -0
- package/dist/collector/factory.d.ts +3 -0
- package/dist/collector/factory.d.ts.map +1 -0
- package/dist/collector/factory.js +25 -0
- package/dist/collector/functions.d.ts +22 -0
- package/dist/collector/functions.d.ts.map +1 -0
- package/dist/collector/functions.js +271 -0
- package/dist/collector/index.d.ts +5 -0
- package/dist/collector/index.d.ts.map +1 -0
- package/dist/collector/index.js +3 -0
- package/dist/collector/types.d.ts +10 -0
- package/dist/collector/types.d.ts.map +1 -0
- package/dist/collector/types.js +1 -0
- package/dist/collector/validation.d.ts +4 -0
- package/dist/collector/validation.d.ts.map +1 -0
- package/dist/collector/validation.js +15 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/expect/index.d.ts +9 -0
- package/dist/expect/index.d.ts.map +1 -0
- package/dist/expect/index.js +71 -0
- package/dist/expect/setup.d.ts +2 -0
- package/dist/expect/setup.d.ts.map +1 -0
- package/dist/expect/setup.js +5 -0
- package/dist/exports.d.ts +7 -0
- package/dist/exports.d.ts.map +1 -0
- package/dist/exports.js +6 -0
- package/dist/getEntryComponent.d.ts +6 -0
- package/dist/getEntryComponent.d.ts.map +1 -0
- package/dist/getEntryComponent.js +6 -0
- package/dist/globals.d.ts +5 -0
- package/dist/globals.d.ts.map +1 -0
- package/dist/globals.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/initialize.d.ts +2 -0
- package/dist/initialize.d.ts.map +1 -0
- package/dist/initialize.js +16 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/mock.d.ts +15 -0
- package/dist/mock.d.ts.map +1 -0
- package/dist/mock.js +37 -0
- package/dist/mocker/index.d.ts +2 -0
- package/dist/mocker/index.d.ts.map +1 -0
- package/dist/mocker/index.js +1 -0
- package/dist/mocker/registry.d.ts +7 -0
- package/dist/mocker/registry.d.ts.map +1 -0
- package/dist/mocker/registry.js +41 -0
- package/dist/mocker/types.d.ts +6 -0
- package/dist/mocker/types.d.ts.map +1 -0
- package/dist/mocker/types.js +1 -0
- package/dist/module.d.ts +3 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +19 -0
- package/dist/module.web.d.ts +2 -0
- package/dist/module.web.d.ts.map +1 -0
- package/dist/module.web.js +12 -0
- package/dist/rntl/client.d.ts +3 -0
- package/dist/rntl/client.d.ts.map +1 -0
- package/dist/rntl/client.js +8 -0
- package/dist/rntl/describe.d.ts +2 -0
- package/dist/rntl/describe.d.ts.map +1 -0
- package/dist/rntl/describe.js +1 -0
- package/dist/rntl/expect.d.ts +128 -0
- package/dist/rntl/expect.d.ts.map +1 -0
- package/dist/rntl/expect.js +670 -0
- package/dist/rntl/fn.d.ts +2 -0
- package/dist/rntl/fn.d.ts.map +1 -0
- package/dist/rntl/fn.js +1 -0
- package/dist/rntl/mock.d.ts +2 -0
- package/dist/rntl/mock.d.ts.map +1 -0
- package/dist/rntl/mock.js +1 -0
- package/dist/rntl/render.d.ts +4 -0
- package/dist/rntl/render.d.ts.map +1 -0
- package/dist/rntl/render.js +11 -0
- package/dist/rntl/screen.d.ts +45 -0
- package/dist/rntl/screen.d.ts.map +1 -0
- package/dist/rntl/screen.js +31 -0
- package/dist/rntl/spies.d.ts +45 -0
- package/dist/rntl/spies.d.ts.map +1 -0
- package/dist/rntl/spies.js +553 -0
- package/dist/rntl/userEvent.d.ts +22 -0
- package/dist/rntl/userEvent.d.ts.map +1 -0
- package/dist/rntl/userEvent.js +19 -0
- package/dist/runner/errors.d.ts +9 -0
- package/dist/runner/errors.d.ts.map +1 -0
- package/dist/runner/errors.js +23 -0
- package/dist/runner/factory.d.ts +3 -0
- package/dist/runner/factory.d.ts.map +1 -0
- package/dist/runner/factory.js +17 -0
- package/dist/runner/hooks.d.ts +4 -0
- package/dist/runner/hooks.d.ts.map +1 -0
- package/dist/runner/hooks.js +39 -0
- package/dist/runner/index.d.ts +4 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/index.js +2 -0
- package/dist/runner/runSuite.d.ts +4 -0
- package/dist/runner/runSuite.d.ts.map +1 -0
- package/dist/runner/runSuite.js +147 -0
- package/dist/runner/types.d.ts +13 -0
- package/dist/runner/types.d.ts.map +1 -0
- package/dist/runner/types.js +1 -0
- package/dist/runner.d.ts +7 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +201 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +44 -0
- package/dist/spy/index.d.ts +2 -0
- package/dist/spy/index.d.ts.map +1 -0
- package/dist/spy/index.js +2 -0
- package/dist/state.d.ts +25 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +37 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/ui/ReadyScreen.d.ts +2 -0
- package/dist/ui/ReadyScreen.d.ts.map +1 -0
- package/dist/ui/ReadyScreen.js +110 -0
- package/dist/ui/UI.d.ts +13 -0
- package/dist/ui/UI.d.ts.map +1 -0
- package/dist/ui/UI.js +121 -0
- package/dist/ui/WrongEnvironmentScreen.d.ts +2 -0
- package/dist/ui/WrongEnvironmentScreen.d.ts.map +1 -0
- package/dist/ui/WrongEnvironmentScreen.js +87 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/state.d.ts +7 -0
- package/dist/ui/state.d.ts.map +1 -0
- package/dist/ui/state.js +6 -0
- package/dist/utils/dev-server.d.ts +2 -0
- package/dist/utils/dev-server.d.ts.map +1 -0
- package/dist/utils/dev-server.js +5 -0
- package/dist/utils/emitter.d.ts +16 -0
- package/dist/utils/emitter.d.ts.map +1 -0
- package/dist/utils/emitter.js +39 -0
- package/eslint.config.mjs +16 -0
- package/package.json +38 -0
- package/src/__tests__/collector.test.ts +553 -0
- package/src/__tests__/error-handling.test.ts +132 -0
- package/src/__tests__/expect.test.ts +619 -0
- package/src/__tests__/spy.test.ts +538 -0
- package/src/bundler/bundle.ts +19 -0
- package/src/bundler/errors.ts +16 -0
- package/src/bundler/evaluate.ts +25 -0
- package/src/bundler/index.ts +2 -0
- package/src/client/factory.ts +56 -0
- package/src/client/getDeviceDescriptor.ts +30 -0
- package/src/client/getWSServer.ts +9 -0
- package/src/client/index.ts +1 -0
- package/src/collector/errors.ts +27 -0
- package/src/collector/factory.ts +32 -0
- package/src/collector/functions.ts +376 -0
- package/src/collector/index.ts +12 -0
- package/src/collector/types.ts +15 -0
- package/src/collector/validation.ts +21 -0
- package/src/constants.ts +2 -0
- package/src/errors.ts +12 -0
- package/src/expect/index.ts +117 -0
- package/src/expect/setup.ts +10 -0
- package/src/globals.ts +5 -0
- package/src/index.ts +7 -0
- package/src/initialize.ts +22 -0
- package/src/mocker/index.ts +1 -0
- package/src/mocker/metro-require.d.ts +5 -0
- package/src/mocker/registry.ts +58 -0
- package/src/mocker/types.ts +6 -0
- package/src/react-native.d.ts +16 -0
- package/src/runner/errors.ts +31 -0
- package/src/runner/factory.ts +21 -0
- package/src/runner/hooks.ts +51 -0
- package/src/runner/index.ts +7 -0
- package/src/runner/runSuite.ts +201 -0
- package/src/runner/types.ts +19 -0
- package/src/spy/index.ts +2 -0
- package/src/ui/ReadyScreen.tsx +151 -0
- package/src/ui/WrongEnvironmentScreen.tsx +113 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/state.ts +13 -0
- package/src/utils/dev-server.ts +6 -0
- package/src/utils/emitter.ts +64 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +33 -0
- package/tsconfig.spec.json +30 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/types/global.d.ts +2 -0
- package/types/index.d.ts +1 -0
- package/vite.config.ts +27 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type TestErrorCode =
|
|
2
|
+
| 'CONTEXT_NOT_INITIALIZED'
|
|
3
|
+
| 'OUTSIDE_DESCRIBE_BLOCK'
|
|
4
|
+
| 'INVALID_TEST_NAME'
|
|
5
|
+
| 'DUPLICATE_TEST_NAME'
|
|
6
|
+
| 'INVALID_FUNCTION';
|
|
7
|
+
|
|
8
|
+
export class TestError extends Error {
|
|
9
|
+
constructor(
|
|
10
|
+
public code: TestErrorCode,
|
|
11
|
+
public functionName: string,
|
|
12
|
+
public context?: Record<string, unknown>
|
|
13
|
+
) {
|
|
14
|
+
const baseMessages: Record<TestErrorCode, string> = {
|
|
15
|
+
CONTEXT_NOT_INITIALIZED:
|
|
16
|
+
'Test context not initialized. Call collectTests() first.',
|
|
17
|
+
OUTSIDE_DESCRIBE_BLOCK: `${functionName}() must be called within a describe() block`,
|
|
18
|
+
INVALID_TEST_NAME: `${functionName}() requires a non-empty string name`,
|
|
19
|
+
DUPLICATE_TEST_NAME: `Duplicate test name "${context?.name}" in suite "${context?.suiteName}"`,
|
|
20
|
+
INVALID_FUNCTION: `${functionName}() requires a function as the second parameter`,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const message = baseMessages[code] || `Unknown error in ${functionName}()`;
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'TestError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TestCollectorEvents } from '@react-native-harness/bridge';
|
|
2
|
+
import { getEmitter } from '../utils/emitter.js';
|
|
3
|
+
import { collectTests } from './functions.js';
|
|
4
|
+
import { TestCollector } from './types.js';
|
|
5
|
+
|
|
6
|
+
export const getTestCollector = (): TestCollector => {
|
|
7
|
+
const events = getEmitter<TestCollectorEvents>();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
events,
|
|
11
|
+
collect: async (fn, testFilePath) => {
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
events.emit({
|
|
14
|
+
type: 'collection-started',
|
|
15
|
+
file: testFilePath,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const result = await collectTests(fn);
|
|
19
|
+
|
|
20
|
+
events.emit({
|
|
21
|
+
type: 'collection-finished',
|
|
22
|
+
file: testFilePath,
|
|
23
|
+
duration: Date.now() - start,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
28
|
+
dispose: () => {
|
|
29
|
+
events.clearAllListeners();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TestCase,
|
|
3
|
+
TestSuite,
|
|
4
|
+
CollectionResult,
|
|
5
|
+
} from '@react-native-harness/bridge';
|
|
6
|
+
import type { TestFn } from './types.js';
|
|
7
|
+
import { TestError } from './errors.js';
|
|
8
|
+
import { validateTestName, validateTestFunction } from './validation.js';
|
|
9
|
+
|
|
10
|
+
type TestStatus = 'active' | 'skipped' | 'todo';
|
|
11
|
+
|
|
12
|
+
type RawTestCase = {
|
|
13
|
+
name: string;
|
|
14
|
+
fn: TestFn;
|
|
15
|
+
options: {
|
|
16
|
+
only?: boolean;
|
|
17
|
+
skip?: boolean;
|
|
18
|
+
todo?: boolean;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type RawTestSuite = {
|
|
23
|
+
name: string;
|
|
24
|
+
tests: RawTestCase[];
|
|
25
|
+
suites: RawTestSuite[];
|
|
26
|
+
hooks: {
|
|
27
|
+
beforeAll: TestFn[];
|
|
28
|
+
afterAll: TestFn[];
|
|
29
|
+
beforeEach: TestFn[];
|
|
30
|
+
afterEach: TestFn[];
|
|
31
|
+
};
|
|
32
|
+
options: {
|
|
33
|
+
only?: boolean;
|
|
34
|
+
skip?: boolean;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Computation functions for two-phase approach
|
|
39
|
+
const computeTestStatus = (
|
|
40
|
+
test: RawTestCase,
|
|
41
|
+
suiteContext: { hasFocusedTests: boolean }
|
|
42
|
+
): TestStatus => {
|
|
43
|
+
if (test.options.todo) return 'todo';
|
|
44
|
+
if (test.options.skip) return 'skipped';
|
|
45
|
+
if (test.options.only) return 'active';
|
|
46
|
+
if (suiteContext.hasFocusedTests) return 'skipped';
|
|
47
|
+
return 'active';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const computeSuiteStatus = (
|
|
51
|
+
suite: RawTestSuite,
|
|
52
|
+
parentContext: { hasFocusedChildren: boolean }
|
|
53
|
+
): TestStatus => {
|
|
54
|
+
if (suite.options.skip) return 'skipped';
|
|
55
|
+
if (suite.options.only) return 'active';
|
|
56
|
+
if (parentContext.hasFocusedChildren) return 'skipped';
|
|
57
|
+
return 'active';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const convertRawTestCaseToTestCase = (
|
|
61
|
+
rawTest: RawTestCase,
|
|
62
|
+
suiteContext: { hasFocusedTests: boolean }
|
|
63
|
+
): TestCase => {
|
|
64
|
+
return {
|
|
65
|
+
name: rawTest.name,
|
|
66
|
+
fn: rawTest.fn,
|
|
67
|
+
status: computeTestStatus(rawTest, suiteContext),
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const convertRawTestSuiteToTestSuite = (
|
|
72
|
+
rawSuite: RawTestSuite,
|
|
73
|
+
parentContext: { hasFocusedChildren: boolean } = {
|
|
74
|
+
hasFocusedChildren: false,
|
|
75
|
+
},
|
|
76
|
+
parentSuite?: TestSuite
|
|
77
|
+
): TestSuite => {
|
|
78
|
+
// Validate duplicate test names within this suite
|
|
79
|
+
const testNames = new Set<string>();
|
|
80
|
+
for (const test of rawSuite.tests) {
|
|
81
|
+
if (testNames.has(test.name)) {
|
|
82
|
+
throw new TestError('DUPLICATE_TEST_NAME', 'test', {
|
|
83
|
+
name: test.name,
|
|
84
|
+
suiteName: rawSuite.name,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
testNames.add(test.name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check if this suite has focused tests
|
|
91
|
+
const hasFocusedTests = rawSuite.tests.some((test) => test.options.only);
|
|
92
|
+
|
|
93
|
+
// Check if this suite has focused children
|
|
94
|
+
const hasFocusedChildren = rawSuite.suites.some(
|
|
95
|
+
(suite) =>
|
|
96
|
+
suite.options.only || suite.tests.some((test) => test.options.only)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Convert tests
|
|
100
|
+
const tests = rawSuite.tests.map((test) =>
|
|
101
|
+
convertRawTestCaseToTestCase(test, { hasFocusedTests })
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Create the suite first so we can reference it when converting children
|
|
105
|
+
const suite: TestSuite = {
|
|
106
|
+
name: rawSuite.name,
|
|
107
|
+
tests,
|
|
108
|
+
suites: [],
|
|
109
|
+
parent: parentSuite,
|
|
110
|
+
beforeAll: rawSuite.hooks.beforeAll,
|
|
111
|
+
afterAll: rawSuite.hooks.afterAll,
|
|
112
|
+
beforeEach: rawSuite.hooks.beforeEach,
|
|
113
|
+
afterEach: rawSuite.hooks.afterEach,
|
|
114
|
+
status: computeSuiteStatus(rawSuite, parentContext),
|
|
115
|
+
_hasFocused: hasFocusedTests || hasFocusedChildren || rawSuite.options.only,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Convert child suites with this suite as their parent
|
|
119
|
+
suite.suites = rawSuite.suites.map((childSuite) =>
|
|
120
|
+
convertRawTestSuiteToTestSuite(childSuite, { hasFocusedChildren }, suite)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return suite;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type TestContext = {
|
|
127
|
+
rootSuite: RawTestSuite;
|
|
128
|
+
currentSuite: RawTestSuite | null;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let currentContext: TestContext | null = null;
|
|
132
|
+
|
|
133
|
+
const clearState = (): TestContext => {
|
|
134
|
+
const rootSuite = createRawSuite('root');
|
|
135
|
+
return {
|
|
136
|
+
rootSuite,
|
|
137
|
+
currentSuite: rootSuite,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const getCurrentSuite = (): RawTestSuite | null => {
|
|
142
|
+
if (!currentContext) {
|
|
143
|
+
throw new TestError('CONTEXT_NOT_INITIALIZED', 'getCurrentSuite');
|
|
144
|
+
}
|
|
145
|
+
return currentContext.currentSuite;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getRootSuite = (): RawTestSuite => {
|
|
149
|
+
if (!currentContext) {
|
|
150
|
+
throw new TestError('CONTEXT_NOT_INITIALIZED', 'getRootSuite');
|
|
151
|
+
}
|
|
152
|
+
return currentContext.rootSuite;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const setCurrentSuite = (suite: RawTestSuite | null): void => {
|
|
156
|
+
if (!currentContext) {
|
|
157
|
+
throw new TestError('CONTEXT_NOT_INITIALIZED', 'setCurrentSuite');
|
|
158
|
+
}
|
|
159
|
+
currentContext.currentSuite = suite;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const createRawSuite = (
|
|
163
|
+
name: string,
|
|
164
|
+
options: { only?: boolean; skip?: boolean } = {}
|
|
165
|
+
): RawTestSuite => {
|
|
166
|
+
return {
|
|
167
|
+
name,
|
|
168
|
+
tests: [],
|
|
169
|
+
suites: [],
|
|
170
|
+
hooks: {
|
|
171
|
+
beforeAll: [],
|
|
172
|
+
afterAll: [],
|
|
173
|
+
beforeEach: [],
|
|
174
|
+
afterEach: [],
|
|
175
|
+
},
|
|
176
|
+
options,
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const describe = Object.assign(
|
|
181
|
+
(name: string, fn: () => void) => {
|
|
182
|
+
validateTestName(name, 'describe');
|
|
183
|
+
validateTestFunction(fn, 'describe');
|
|
184
|
+
|
|
185
|
+
const suite = createRawSuite(name);
|
|
186
|
+
const previousSuite = getCurrentSuite();
|
|
187
|
+
setCurrentSuite(suite);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
fn();
|
|
191
|
+
} finally {
|
|
192
|
+
setCurrentSuite(previousSuite);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Add the suite to its parent
|
|
196
|
+
if (previousSuite) {
|
|
197
|
+
previousSuite.suites.push(suite);
|
|
198
|
+
} else {
|
|
199
|
+
getRootSuite().suites.push(suite);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
skip: (name: string, fn: () => void) => {
|
|
204
|
+
validateTestName(name, 'describe.skip');
|
|
205
|
+
validateTestFunction(fn, 'describe.skip');
|
|
206
|
+
|
|
207
|
+
const suite = createRawSuite(name, { skip: true });
|
|
208
|
+
const previousSuite = getCurrentSuite();
|
|
209
|
+
setCurrentSuite(suite);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
fn();
|
|
213
|
+
} finally {
|
|
214
|
+
setCurrentSuite(previousSuite);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Add the suite to its parent
|
|
218
|
+
if (previousSuite) {
|
|
219
|
+
previousSuite.suites.push(suite);
|
|
220
|
+
} else {
|
|
221
|
+
getRootSuite().suites.push(suite);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
only: (name: string, fn: () => void) => {
|
|
225
|
+
validateTestName(name, 'describe.only');
|
|
226
|
+
validateTestFunction(fn, 'describe.only');
|
|
227
|
+
|
|
228
|
+
const suite = createRawSuite(name, { only: true });
|
|
229
|
+
const previousSuite = getCurrentSuite();
|
|
230
|
+
setCurrentSuite(suite);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
fn();
|
|
234
|
+
} finally {
|
|
235
|
+
setCurrentSuite(previousSuite);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Add the suite to its parent
|
|
239
|
+
if (previousSuite) {
|
|
240
|
+
previousSuite.suites.push(suite);
|
|
241
|
+
} else {
|
|
242
|
+
getRootSuite().suites.push(suite);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
export const test = Object.assign(
|
|
249
|
+
(name: string, fn: TestFn) => {
|
|
250
|
+
validateTestName(name, 'test');
|
|
251
|
+
validateTestFunction(fn, 'test');
|
|
252
|
+
|
|
253
|
+
const currentSuite = getCurrentSuite();
|
|
254
|
+
if (!currentSuite) {
|
|
255
|
+
throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'test');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Add test with default options
|
|
259
|
+
currentSuite.tests.push({ name, fn, options: {} });
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
skip: (name: string, fn: TestFn) => {
|
|
263
|
+
validateTestName(name, 'test.skip');
|
|
264
|
+
validateTestFunction(fn, 'test.skip');
|
|
265
|
+
|
|
266
|
+
const currentSuite = getCurrentSuite();
|
|
267
|
+
if (!currentSuite) {
|
|
268
|
+
throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'test.skip');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
currentSuite.tests.push({ name, fn, options: { skip: true } });
|
|
272
|
+
},
|
|
273
|
+
only: (name: string, fn: TestFn) => {
|
|
274
|
+
validateTestName(name, 'test.only');
|
|
275
|
+
validateTestFunction(fn, 'test.only');
|
|
276
|
+
|
|
277
|
+
const currentSuite = getCurrentSuite();
|
|
278
|
+
if (!currentSuite) {
|
|
279
|
+
throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'test.only');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
currentSuite.tests.push({ name, fn, options: { only: true } });
|
|
283
|
+
},
|
|
284
|
+
todo: (name: string) => {
|
|
285
|
+
validateTestName(name, 'test.todo');
|
|
286
|
+
|
|
287
|
+
const currentSuite = getCurrentSuite();
|
|
288
|
+
if (!currentSuite) {
|
|
289
|
+
throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'test.todo');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
currentSuite.tests.push({
|
|
293
|
+
name,
|
|
294
|
+
fn: () => {
|
|
295
|
+
// Empty function for todo tests
|
|
296
|
+
},
|
|
297
|
+
options: { todo: true },
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
export const it = test;
|
|
304
|
+
|
|
305
|
+
export function beforeAll(fn: TestFn) {
|
|
306
|
+
validateTestFunction(fn, 'beforeAll');
|
|
307
|
+
|
|
308
|
+
const currentSuite = getCurrentSuite();
|
|
309
|
+
if (!currentSuite) {
|
|
310
|
+
throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'beforeAll');
|
|
311
|
+
}
|
|
312
|
+
currentSuite.hooks.beforeAll.push(fn);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function afterAll(fn: TestFn) {
|
|
316
|
+
validateTestFunction(fn, 'afterAll');
|
|
317
|
+
|
|
318
|
+
const currentSuite = getCurrentSuite();
|
|
319
|
+
if (!currentSuite) {
|
|
320
|
+
throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'afterAll');
|
|
321
|
+
}
|
|
322
|
+
currentSuite.hooks.afterAll.push(fn);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function beforeEach(fn: TestFn) {
|
|
326
|
+
validateTestFunction(fn, 'beforeEach');
|
|
327
|
+
|
|
328
|
+
const currentSuite = getCurrentSuite();
|
|
329
|
+
if (!currentSuite) {
|
|
330
|
+
throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'beforeEach');
|
|
331
|
+
}
|
|
332
|
+
currentSuite.hooks.beforeEach.push(fn);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function afterEach(fn: TestFn) {
|
|
336
|
+
validateTestFunction(fn, 'afterEach');
|
|
337
|
+
|
|
338
|
+
const currentSuite = getCurrentSuite();
|
|
339
|
+
if (!currentSuite) {
|
|
340
|
+
throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'afterEach');
|
|
341
|
+
}
|
|
342
|
+
currentSuite.hooks.afterEach.push(fn);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Recursively counts the total number of tests that will actually be executed.
|
|
347
|
+
* Only counts active tests since skipped and todo tests are not executed.
|
|
348
|
+
*/
|
|
349
|
+
const countTests = (suite: TestSuite): number => {
|
|
350
|
+
let count = suite.tests.filter((test) => test.status === 'active').length;
|
|
351
|
+
|
|
352
|
+
for (const childSuite of suite.suites) {
|
|
353
|
+
count += countTests(childSuite);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return count;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
export const collectTests = (fn: () => void): CollectionResult => {
|
|
360
|
+
currentContext = clearState();
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
fn();
|
|
364
|
+
|
|
365
|
+
// Convert raw structure to final structure using computation phase
|
|
366
|
+
const testSuite = convertRawTestSuiteToTestSuite(getRootSuite());
|
|
367
|
+
const totalTests = countTests(testSuite);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
testSuite,
|
|
371
|
+
totalTests,
|
|
372
|
+
};
|
|
373
|
+
} finally {
|
|
374
|
+
currentContext = null;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
describe,
|
|
3
|
+
test,
|
|
4
|
+
it,
|
|
5
|
+
beforeAll,
|
|
6
|
+
afterAll,
|
|
7
|
+
beforeEach,
|
|
8
|
+
afterEach,
|
|
9
|
+
} from './functions.js';
|
|
10
|
+
export { TestError, type TestErrorCode } from './errors.js';
|
|
11
|
+
export type { TestCollector, TestCollectorEventsEmitter } from './types.js';
|
|
12
|
+
export { getTestCollector } from './factory.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EventEmitter } from '../utils/emitter.js';
|
|
2
|
+
import {
|
|
3
|
+
TestCollectorEvents,
|
|
4
|
+
CollectionResult,
|
|
5
|
+
} from '@react-native-harness/bridge';
|
|
6
|
+
|
|
7
|
+
export type TestFn = () => void | Promise<void>;
|
|
8
|
+
|
|
9
|
+
export type TestCollectorEventsEmitter = EventEmitter<TestCollectorEvents>;
|
|
10
|
+
|
|
11
|
+
export type TestCollector = {
|
|
12
|
+
events: TestCollectorEventsEmitter;
|
|
13
|
+
collect: (fn: () => void, testFilePath: string) => Promise<CollectionResult>;
|
|
14
|
+
dispose: () => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TestError } from './errors.js';
|
|
2
|
+
import { TestFn } from './types.js';
|
|
3
|
+
|
|
4
|
+
export const validateTestName = (name: string, functionName: string): void => {
|
|
5
|
+
if (!name || typeof name !== 'string' || name.trim() === '') {
|
|
6
|
+
throw new TestError('INVALID_TEST_NAME', functionName, {
|
|
7
|
+
name,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const validateTestFunction = (
|
|
13
|
+
fn: TestFn,
|
|
14
|
+
functionName: string
|
|
15
|
+
): void => {
|
|
16
|
+
if (typeof fn !== 'function') {
|
|
17
|
+
throw new TestError('INVALID_FUNCTION', functionName, {
|
|
18
|
+
functionType: typeof fn,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
};
|
package/src/constants.ts
ADDED
package/src/errors.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class EnvironmentError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
public readonly context: string,
|
|
4
|
+
public readonly details?: string
|
|
5
|
+
) {
|
|
6
|
+
const message = details
|
|
7
|
+
? `Environment error in ${context}: ${details}`
|
|
8
|
+
: `Environment error: ${context}`;
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'EnvironmentError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Assertion, ExpectStatic, MatcherState } from '@vitest/expect';
|
|
2
|
+
import {
|
|
3
|
+
addCustomEqualityTesters,
|
|
4
|
+
ASYMMETRIC_MATCHERS_OBJECT,
|
|
5
|
+
customMatchers,
|
|
6
|
+
getState,
|
|
7
|
+
GLOBAL_EXPECT,
|
|
8
|
+
setState,
|
|
9
|
+
} from '@vitest/expect';
|
|
10
|
+
import * as chai from 'chai';
|
|
11
|
+
|
|
12
|
+
// Setup additional matchers
|
|
13
|
+
import './setup.js';
|
|
14
|
+
|
|
15
|
+
export function createExpect(): ExpectStatic {
|
|
16
|
+
const expect = ((value: any, message?: string): Assertion => {
|
|
17
|
+
const { assertionCalls } = getState(expect);
|
|
18
|
+
setState({ assertionCalls: assertionCalls + 1 }, expect);
|
|
19
|
+
return chai.expect(value, message) as unknown as Assertion;
|
|
20
|
+
}) as ExpectStatic;
|
|
21
|
+
Object.assign(expect, chai.expect);
|
|
22
|
+
Object.assign(expect, (globalThis as any)[ASYMMETRIC_MATCHERS_OBJECT]);
|
|
23
|
+
|
|
24
|
+
expect.getState = () => getState<MatcherState>(expect);
|
|
25
|
+
expect.setState = (state) => setState(state as Partial<MatcherState>, expect);
|
|
26
|
+
|
|
27
|
+
// @ts-expect-error global is not typed
|
|
28
|
+
const globalState = getState(globalThis[GLOBAL_EXPECT]) || {};
|
|
29
|
+
|
|
30
|
+
setState<MatcherState>(
|
|
31
|
+
{
|
|
32
|
+
// this should also add "snapshotState" that is added conditionally
|
|
33
|
+
...globalState,
|
|
34
|
+
assertionCalls: 0,
|
|
35
|
+
isExpectingAssertions: false,
|
|
36
|
+
isExpectingAssertionsError: null,
|
|
37
|
+
expectedAssertionsNumber: null,
|
|
38
|
+
expectedAssertionsNumberErrorGen: null,
|
|
39
|
+
},
|
|
40
|
+
expect
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// @ts-expect-error untyped
|
|
44
|
+
expect.extend = (matchers) => chai.expect.extend(expect, matchers);
|
|
45
|
+
// @ts-expect-error untyped
|
|
46
|
+
expect.addEqualityTesters = (customTesters) =>
|
|
47
|
+
addCustomEqualityTesters(customTesters);
|
|
48
|
+
|
|
49
|
+
// @ts-expect-error untyped
|
|
50
|
+
expect.soft = (...args) => {
|
|
51
|
+
// @ts-expect-error private soft access
|
|
52
|
+
return expect(...args).withContext({ soft: true }) as Assertion;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// @ts-expect-error untyped
|
|
56
|
+
expect.unreachable = (message?: string) => {
|
|
57
|
+
chai.assert.fail(
|
|
58
|
+
`expected${message ? ` "${message}" ` : ' '}not to be reached`
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function assertions(expected: number) {
|
|
63
|
+
const errorGen = () =>
|
|
64
|
+
new Error(
|
|
65
|
+
`expected number of assertions to be ${expected}, but got ${
|
|
66
|
+
expect.getState().assertionCalls
|
|
67
|
+
}`
|
|
68
|
+
);
|
|
69
|
+
if (Error.captureStackTrace) {
|
|
70
|
+
Error.captureStackTrace(errorGen(), assertions);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expect.setState({
|
|
74
|
+
expectedAssertionsNumber: expected,
|
|
75
|
+
expectedAssertionsNumberErrorGen: errorGen,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasAssertions() {
|
|
80
|
+
const error = new Error('expected any number of assertion, but got none');
|
|
81
|
+
if (Error.captureStackTrace) {
|
|
82
|
+
Error.captureStackTrace(error, hasAssertions);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
expect.setState({
|
|
86
|
+
isExpectingAssertions: true,
|
|
87
|
+
isExpectingAssertionsError: error,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
chai.util.addMethod(expect, 'assertions', assertions);
|
|
92
|
+
chai.util.addMethod(expect, 'hasAssertions', hasAssertions);
|
|
93
|
+
|
|
94
|
+
expect.extend(customMatchers);
|
|
95
|
+
|
|
96
|
+
return expect;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const globalExpect: ExpectStatic = createExpect();
|
|
100
|
+
|
|
101
|
+
Object.defineProperty(globalThis, GLOBAL_EXPECT, {
|
|
102
|
+
value: globalExpect,
|
|
103
|
+
writable: true,
|
|
104
|
+
configurable: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export { assert, should } from 'chai';
|
|
108
|
+
export { chai, globalExpect as expect };
|
|
109
|
+
|
|
110
|
+
export type {
|
|
111
|
+
Assertion,
|
|
112
|
+
AsymmetricMatchersContaining,
|
|
113
|
+
DeeplyAllowMatchers,
|
|
114
|
+
ExpectStatic,
|
|
115
|
+
JestAssertion,
|
|
116
|
+
Matchers,
|
|
117
|
+
} from '@vitest/expect';
|
package/src/globals.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getDeviceDescriptor } from './client/getDeviceDescriptor.js';
|
|
2
|
+
import { getClient } from './client/index.js';
|
|
3
|
+
|
|
4
|
+
// Polyfill for EventTarget
|
|
5
|
+
const Shim = require('event-target-shim');
|
|
6
|
+
globalThis.Event = Shim.Event;
|
|
7
|
+
globalThis.EventTarget = Shim.EventTarget;
|
|
8
|
+
|
|
9
|
+
// Turn off LogBox
|
|
10
|
+
const { LogBox } = require('react-native');
|
|
11
|
+
LogBox.ignoreAllLogs(true);
|
|
12
|
+
|
|
13
|
+
// Turn off HMR
|
|
14
|
+
const HMRClient = require('react-native/Libraries/Utilities/HMRClient');
|
|
15
|
+
HMRClient.setup = () => {
|
|
16
|
+
// No setup = no HMR
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Initialize the client
|
|
20
|
+
void getClient().then((client) =>
|
|
21
|
+
client.rpc.reportReady(getDeviceDescriptor())
|
|
22
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { mock, requireActual, clearMocks } from './registry.js';
|