@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,58 @@
|
|
|
1
|
+
import { ModuleFactory, ModuleId, Require } from './types.js';
|
|
2
|
+
|
|
3
|
+
const mockRegistry = new Map<number, ModuleFactory>();
|
|
4
|
+
const mockCache = new Map<number, unknown>();
|
|
5
|
+
|
|
6
|
+
const originalRequire = global.__r;
|
|
7
|
+
|
|
8
|
+
export const mock = (moduleId: ModuleId, factory: ModuleFactory): void => {
|
|
9
|
+
mockCache.delete(moduleId);
|
|
10
|
+
mockRegistry.set(moduleId, factory);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const clearMocks = (): void => {
|
|
14
|
+
mockRegistry.clear();
|
|
15
|
+
mockCache.clear();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getMockRegistry = (): Map<number, ModuleFactory> => {
|
|
19
|
+
return mockRegistry;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getMockImplementation = (moduleId: number): unknown | null => {
|
|
23
|
+
if (mockCache.has(moduleId)) {
|
|
24
|
+
return mockCache.get(moduleId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const factory = mockRegistry.get(moduleId);
|
|
28
|
+
if (!factory) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const implementation = factory();
|
|
33
|
+
mockCache.set(moduleId, implementation);
|
|
34
|
+
return implementation;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
export const requireActual = <T = any>(moduleId: string): T =>
|
|
39
|
+
// babel plugin will transform 'moduleId' to a number
|
|
40
|
+
originalRequire(moduleId as unknown as ModuleId) as T;
|
|
41
|
+
|
|
42
|
+
const mockRequire = (moduleId: string) => {
|
|
43
|
+
// babel plugin will transform 'moduleId' to a number
|
|
44
|
+
const mockedModule = getMockImplementation(moduleId as unknown as ModuleId);
|
|
45
|
+
|
|
46
|
+
if (mockedModule) {
|
|
47
|
+
return mockedModule;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return originalRequire(moduleId as unknown as ModuleId);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
Object.setPrototypeOf(mockRequire, Object.getPrototypeOf(originalRequire));
|
|
54
|
+
Object.defineProperties(
|
|
55
|
+
mockRequire,
|
|
56
|
+
Object.getOwnPropertyDescriptors(originalRequire)
|
|
57
|
+
);
|
|
58
|
+
global.__r = mockRequire as unknown as Require;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare module 'react-native/Libraries/Core/Devtools/getDevServer' {
|
|
2
|
+
export type DevServerInfo = {
|
|
3
|
+
url: string;
|
|
4
|
+
fullBundleUrl?: string;
|
|
5
|
+
bundleLoadedFromServer: boolean;
|
|
6
|
+
};
|
|
7
|
+
export default function getDevServer(): DevServerInfo;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
var __r:
|
|
12
|
+
| {
|
|
13
|
+
(moduleId: number): unknown;
|
|
14
|
+
}
|
|
15
|
+
| undefined;
|
|
16
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SerializedError } from '@react-native-harness/bridge';
|
|
2
|
+
|
|
3
|
+
export class TestExecutionError extends Error {
|
|
4
|
+
file: string;
|
|
5
|
+
suite: string;
|
|
6
|
+
test: string;
|
|
7
|
+
|
|
8
|
+
constructor(error: unknown, file: string, suite: string, test: string) {
|
|
9
|
+
super('Test execution error');
|
|
10
|
+
this.name = 'TestExecutionError';
|
|
11
|
+
this.file = file;
|
|
12
|
+
this.suite = suite;
|
|
13
|
+
this.test = test;
|
|
14
|
+
this.cause = error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toSerializedJSON(): SerializedError {
|
|
18
|
+
const causeName =
|
|
19
|
+
this.cause instanceof Error ? this.cause.name : 'Unknown name';
|
|
20
|
+
const causeMessage =
|
|
21
|
+
this.cause instanceof Error ? this.cause.message : 'Unknown message';
|
|
22
|
+
const causeStack =
|
|
23
|
+
this.cause instanceof Error ? this.cause.stack : undefined;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: causeName,
|
|
27
|
+
message: causeMessage,
|
|
28
|
+
stack: causeStack,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TestRunnerEvents } from '@react-native-harness/bridge';
|
|
2
|
+
import { getEmitter } from '../utils/emitter.js';
|
|
3
|
+
import { runSuite } from './runSuite.js';
|
|
4
|
+
import { TestRunner } from './types.js';
|
|
5
|
+
|
|
6
|
+
export const getTestRunner = (): TestRunner => {
|
|
7
|
+
const events = getEmitter<TestRunnerEvents>();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
events,
|
|
11
|
+
run: async (testSuite, testFilePath) => {
|
|
12
|
+
return runSuite(testSuite, {
|
|
13
|
+
events,
|
|
14
|
+
testFilePath,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
dispose: () => {
|
|
18
|
+
events.clearAllListeners();
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { TestSuite } from '@react-native-harness/bridge';
|
|
2
|
+
|
|
3
|
+
export type HookType = 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll';
|
|
4
|
+
|
|
5
|
+
const collectInheritedHooks = (
|
|
6
|
+
suite: TestSuite,
|
|
7
|
+
hookType: HookType
|
|
8
|
+
): (() => void | Promise<void>)[] => {
|
|
9
|
+
const hooks: (() => void | Promise<void>)[] = [];
|
|
10
|
+
const suiteChain: TestSuite[] = [];
|
|
11
|
+
|
|
12
|
+
// Collect all suites from current to root
|
|
13
|
+
let currentSuite: TestSuite | undefined = suite;
|
|
14
|
+
while (currentSuite) {
|
|
15
|
+
suiteChain.push(currentSuite);
|
|
16
|
+
currentSuite = currentSuite.parent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (hookType === 'beforeEach' || hookType === 'beforeAll') {
|
|
20
|
+
// For beforeEach/beforeAll: run parent hooks first (reverse the chain)
|
|
21
|
+
for (let i = suiteChain.length - 1; i >= 0; i--) {
|
|
22
|
+
if (hookType === 'beforeEach') {
|
|
23
|
+
hooks.push(...suiteChain[i].beforeEach);
|
|
24
|
+
} else {
|
|
25
|
+
hooks.push(...suiteChain[i].beforeAll);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
// For afterEach/afterAll: run child hooks first (use chain as-is)
|
|
30
|
+
for (const suiteInChain of suiteChain) {
|
|
31
|
+
if (hookType === 'afterEach') {
|
|
32
|
+
hooks.push(...suiteInChain.afterEach);
|
|
33
|
+
} else {
|
|
34
|
+
hooks.push(...suiteInChain.afterAll);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return hooks;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const runHooks = async (
|
|
43
|
+
suite: TestSuite,
|
|
44
|
+
hookType: HookType
|
|
45
|
+
): Promise<void> => {
|
|
46
|
+
const hooks = collectInheritedHooks(suite, hookType);
|
|
47
|
+
|
|
48
|
+
for (const hook of hooks) {
|
|
49
|
+
await hook();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TestCase,
|
|
3
|
+
TestResult,
|
|
4
|
+
TestSuite,
|
|
5
|
+
TestSuiteResult,
|
|
6
|
+
} from '@react-native-harness/bridge';
|
|
7
|
+
import { runHooks } from './hooks.js';
|
|
8
|
+
import { TestExecutionError } from './errors.js';
|
|
9
|
+
import { TestRunnerContext } from './types.js';
|
|
10
|
+
|
|
11
|
+
const runTest = async (
|
|
12
|
+
test: TestCase,
|
|
13
|
+
suite: TestSuite,
|
|
14
|
+
context: TestRunnerContext
|
|
15
|
+
): Promise<TestResult> => {
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
|
|
18
|
+
// Emit test-started event
|
|
19
|
+
context.events.emit({
|
|
20
|
+
type: 'test-started',
|
|
21
|
+
name: test.name,
|
|
22
|
+
suite: suite.name,
|
|
23
|
+
file: context.testFilePath,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (test.status === 'skipped') {
|
|
28
|
+
const result = {
|
|
29
|
+
name: test.name,
|
|
30
|
+
status: 'skipped' as const,
|
|
31
|
+
duration: 0,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Emit test-finished event
|
|
35
|
+
context.events.emit({
|
|
36
|
+
type: 'test-finished',
|
|
37
|
+
name: test.name,
|
|
38
|
+
suite: suite.name,
|
|
39
|
+
file: context.testFilePath,
|
|
40
|
+
duration: 0,
|
|
41
|
+
status: 'skipped',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (test.status === 'todo') {
|
|
48
|
+
console.log(`- ${test.name} (todo)`);
|
|
49
|
+
const result = {
|
|
50
|
+
name: test.name,
|
|
51
|
+
status: 'todo' as const,
|
|
52
|
+
duration: 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Emit test-finished event
|
|
56
|
+
context.events.emit({
|
|
57
|
+
type: 'test-finished',
|
|
58
|
+
name: test.name,
|
|
59
|
+
suite: suite.name,
|
|
60
|
+
file: context.testFilePath,
|
|
61
|
+
duration: 0,
|
|
62
|
+
status: 'todo',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Run all beforeEach hooks from the current suite and its parents
|
|
69
|
+
await runHooks(suite, 'beforeEach');
|
|
70
|
+
|
|
71
|
+
// Run the actual test
|
|
72
|
+
await test.fn();
|
|
73
|
+
|
|
74
|
+
// Run all afterEach hooks from the current suite and its parents
|
|
75
|
+
await runHooks(suite, 'afterEach');
|
|
76
|
+
|
|
77
|
+
const duration = Date.now() - startTime;
|
|
78
|
+
|
|
79
|
+
const result = {
|
|
80
|
+
name: test.name,
|
|
81
|
+
status: 'passed' as const,
|
|
82
|
+
duration,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Emit test-finished event
|
|
86
|
+
context.events.emit({
|
|
87
|
+
type: 'test-finished',
|
|
88
|
+
file: context.testFilePath,
|
|
89
|
+
suite: suite.name,
|
|
90
|
+
name: test.name,
|
|
91
|
+
duration,
|
|
92
|
+
status: 'passed',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const testError = new TestExecutionError(
|
|
98
|
+
error,
|
|
99
|
+
context.testFilePath,
|
|
100
|
+
suite.name,
|
|
101
|
+
test.name
|
|
102
|
+
);
|
|
103
|
+
const duration = Date.now() - startTime;
|
|
104
|
+
|
|
105
|
+
const result = {
|
|
106
|
+
name: test.name,
|
|
107
|
+
status: 'failed' as const,
|
|
108
|
+
error: testError.toSerializedJSON(),
|
|
109
|
+
duration,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Emit test-finished event
|
|
113
|
+
context.events.emit({
|
|
114
|
+
type: 'test-finished',
|
|
115
|
+
file: context.testFilePath,
|
|
116
|
+
suite: suite.name,
|
|
117
|
+
name: test.name,
|
|
118
|
+
duration,
|
|
119
|
+
error: testError.toSerializedJSON(),
|
|
120
|
+
status: 'failed',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const runSuite = async (
|
|
128
|
+
suite: TestSuite,
|
|
129
|
+
context: TestRunnerContext
|
|
130
|
+
): Promise<TestSuiteResult> => {
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
|
|
133
|
+
// Emit suite-started event
|
|
134
|
+
context.events.emit({
|
|
135
|
+
type: 'suite-started',
|
|
136
|
+
name: suite.name,
|
|
137
|
+
file: context.testFilePath,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const testResults: TestResult[] = [];
|
|
141
|
+
const suiteResults: TestSuiteResult[] = [];
|
|
142
|
+
|
|
143
|
+
// Run beforeAll hooks
|
|
144
|
+
await runHooks(suite, 'beforeAll');
|
|
145
|
+
|
|
146
|
+
// Run all tests in the current suite
|
|
147
|
+
for (const test of suite.tests) {
|
|
148
|
+
const result = await runTest(test, suite, context);
|
|
149
|
+
testResults.push(result);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Run all child suites
|
|
153
|
+
for (const childSuite of suite.suites) {
|
|
154
|
+
const result = await runSuite(childSuite, context);
|
|
155
|
+
suiteResults.push(result);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Run afterAll hooks
|
|
159
|
+
await runHooks(suite, 'afterAll');
|
|
160
|
+
|
|
161
|
+
const duration = Date.now() - startTime;
|
|
162
|
+
|
|
163
|
+
// Determine overall suite status
|
|
164
|
+
let status: 'passed' | 'failed' | 'skipped' | 'todo' = 'passed';
|
|
165
|
+
|
|
166
|
+
// Check if any tests or child suites failed
|
|
167
|
+
const hasFailedTests = testResults.some(
|
|
168
|
+
(result) => result.status === 'failed'
|
|
169
|
+
);
|
|
170
|
+
const hasFailedSuites = suiteResults.some(
|
|
171
|
+
(result) => result.status === 'failed'
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (hasFailedTests || hasFailedSuites) {
|
|
175
|
+
status = 'failed';
|
|
176
|
+
} else if (
|
|
177
|
+
(testResults.every((result) => result.status === 'skipped') &&
|
|
178
|
+
suiteResults.every((result) => result.status === 'skipped') &&
|
|
179
|
+
testResults.length > 0) ||
|
|
180
|
+
suiteResults.length > 0
|
|
181
|
+
) {
|
|
182
|
+
status = 'skipped';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Emit suite-finished event
|
|
186
|
+
context.events.emit({
|
|
187
|
+
type: 'suite-finished',
|
|
188
|
+
file: context.testFilePath,
|
|
189
|
+
name: suite.name,
|
|
190
|
+
duration,
|
|
191
|
+
status,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
name: suite.name,
|
|
196
|
+
tests: testResults,
|
|
197
|
+
suites: suiteResults,
|
|
198
|
+
status,
|
|
199
|
+
duration,
|
|
200
|
+
};
|
|
201
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EventEmitter } from '../utils/emitter.js';
|
|
2
|
+
import type {
|
|
3
|
+
TestRunnerEvents,
|
|
4
|
+
TestSuite,
|
|
5
|
+
TestSuiteResult,
|
|
6
|
+
} from '@react-native-harness/bridge';
|
|
7
|
+
|
|
8
|
+
export type TestRunnerEventsEmitter = EventEmitter<TestRunnerEvents>;
|
|
9
|
+
|
|
10
|
+
export type TestRunnerContext = {
|
|
11
|
+
events: TestRunnerEventsEmitter;
|
|
12
|
+
testFilePath: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type TestRunner = {
|
|
16
|
+
events: TestRunnerEventsEmitter;
|
|
17
|
+
run: (testSuite: TestSuite, testFilePath: string) => Promise<TestSuiteResult>;
|
|
18
|
+
dispose: () => void;
|
|
19
|
+
};
|
package/src/spy/index.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
View,
|
|
3
|
+
Text,
|
|
4
|
+
Image,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
StatusBar,
|
|
8
|
+
Platform,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { LOGO_IMAGE } from '../constants.js';
|
|
11
|
+
import { useRunnerStatus } from './state.js';
|
|
12
|
+
|
|
13
|
+
require('../initialize.js');
|
|
14
|
+
|
|
15
|
+
export const ReadyScreen = () => {
|
|
16
|
+
const status = useRunnerStatus();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View style={styles.container}>
|
|
20
|
+
<View style={styles.contentContainer}>
|
|
21
|
+
<View style={styles.logoContainer}>
|
|
22
|
+
<Image style={styles.logo} source={LOGO_IMAGE} />
|
|
23
|
+
</View>
|
|
24
|
+
<Text style={styles.title}>React Native Harness</Text>
|
|
25
|
+
|
|
26
|
+
{status === 'idle' ? (
|
|
27
|
+
<View style={styles.statusIndicator}>
|
|
28
|
+
<View style={styles.statusDot} />
|
|
29
|
+
<Text style={styles.statusText}>Idle</Text>
|
|
30
|
+
</View>
|
|
31
|
+
) : status === 'loading' ? (
|
|
32
|
+
<View style={styles.loadingContainer}>
|
|
33
|
+
<ActivityIndicator
|
|
34
|
+
size="small"
|
|
35
|
+
color="#3b82f6"
|
|
36
|
+
style={styles.loadingSpinner}
|
|
37
|
+
/>
|
|
38
|
+
<Text style={styles.loadingText}>Loading...</Text>
|
|
39
|
+
</View>
|
|
40
|
+
) : (
|
|
41
|
+
<View style={styles.statusIndicator}>
|
|
42
|
+
<View style={styles.statusDot} />
|
|
43
|
+
<Text style={styles.statusText}>Running...</Text>
|
|
44
|
+
</View>
|
|
45
|
+
)}
|
|
46
|
+
</View>
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const styles = StyleSheet.create({
|
|
52
|
+
container: {
|
|
53
|
+
flex: 1,
|
|
54
|
+
justifyContent: 'center',
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
backgroundColor: '#0a1628',
|
|
57
|
+
position: 'relative',
|
|
58
|
+
},
|
|
59
|
+
safeArea: {
|
|
60
|
+
flex: 1,
|
|
61
|
+
backgroundColor: 'transparent',
|
|
62
|
+
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
|
|
63
|
+
},
|
|
64
|
+
contentContainer: {
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
padding: 24,
|
|
67
|
+
borderRadius: 24,
|
|
68
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
69
|
+
borderWidth: 1,
|
|
70
|
+
borderColor: 'rgba(59, 130, 246, 0.2)',
|
|
71
|
+
shadowColor: '#000',
|
|
72
|
+
shadowOffset: {
|
|
73
|
+
width: 0,
|
|
74
|
+
height: 20,
|
|
75
|
+
},
|
|
76
|
+
shadowOpacity: 0.3,
|
|
77
|
+
shadowRadius: 30,
|
|
78
|
+
maxWidth: 350,
|
|
79
|
+
},
|
|
80
|
+
logoContainer: {
|
|
81
|
+
marginBottom: 12,
|
|
82
|
+
},
|
|
83
|
+
logo: {
|
|
84
|
+
width: 128,
|
|
85
|
+
height: 128,
|
|
86
|
+
},
|
|
87
|
+
title: {
|
|
88
|
+
fontSize: 28,
|
|
89
|
+
fontWeight: '700',
|
|
90
|
+
color: '#38bdf8',
|
|
91
|
+
textAlign: 'center',
|
|
92
|
+
letterSpacing: 1,
|
|
93
|
+
marginBottom: 8,
|
|
94
|
+
},
|
|
95
|
+
subtitle: {
|
|
96
|
+
fontSize: 16,
|
|
97
|
+
fontWeight: '400',
|
|
98
|
+
color: 'rgba(255, 255, 255, 0.7)',
|
|
99
|
+
textAlign: 'center',
|
|
100
|
+
letterSpacing: 0.5,
|
|
101
|
+
marginBottom: 24,
|
|
102
|
+
},
|
|
103
|
+
statusIndicator: {
|
|
104
|
+
flexDirection: 'row',
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
backgroundColor: 'rgba(34, 211, 238, 0.15)',
|
|
107
|
+
paddingHorizontal: 16,
|
|
108
|
+
paddingVertical: 8,
|
|
109
|
+
height: 36,
|
|
110
|
+
borderRadius: 20,
|
|
111
|
+
borderWidth: 1,
|
|
112
|
+
borderColor: 'rgba(34, 211, 238, 0.4)',
|
|
113
|
+
},
|
|
114
|
+
statusDot: {
|
|
115
|
+
width: 8,
|
|
116
|
+
height: 8,
|
|
117
|
+
borderRadius: 4,
|
|
118
|
+
backgroundColor: '#22d3ee',
|
|
119
|
+
marginRight: 8,
|
|
120
|
+
shadowColor: '#22d3ee',
|
|
121
|
+
shadowOffset: { width: 0, height: 0 },
|
|
122
|
+
shadowOpacity: 0.8,
|
|
123
|
+
shadowRadius: 4,
|
|
124
|
+
},
|
|
125
|
+
statusText: {
|
|
126
|
+
fontSize: 14,
|
|
127
|
+
fontWeight: '500',
|
|
128
|
+
color: '#22d3ee',
|
|
129
|
+
letterSpacing: 0.5,
|
|
130
|
+
},
|
|
131
|
+
loadingContainer: {
|
|
132
|
+
flexDirection: 'row',
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
backgroundColor: 'rgba(59, 130, 246, 0.15)',
|
|
135
|
+
paddingHorizontal: 16,
|
|
136
|
+
paddingVertical: 8,
|
|
137
|
+
height: 36,
|
|
138
|
+
borderRadius: 20,
|
|
139
|
+
borderWidth: 1,
|
|
140
|
+
borderColor: 'rgba(59, 130, 246, 0.4)',
|
|
141
|
+
},
|
|
142
|
+
loadingSpinner: {
|
|
143
|
+
marginRight: 8,
|
|
144
|
+
},
|
|
145
|
+
loadingText: {
|
|
146
|
+
fontSize: 14,
|
|
147
|
+
fontWeight: '500',
|
|
148
|
+
color: '#3b82f6',
|
|
149
|
+
letterSpacing: 0.5,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
View,
|
|
3
|
+
Text,
|
|
4
|
+
Image,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Platform,
|
|
7
|
+
StatusBar,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { LOGO_IMAGE } from '../constants.js';
|
|
10
|
+
|
|
11
|
+
export const WrongEnvironmentScreen = () => {
|
|
12
|
+
return (
|
|
13
|
+
<View style={styles.container}>
|
|
14
|
+
<View style={styles.contentContainer}>
|
|
15
|
+
<View style={styles.logoContainer}>
|
|
16
|
+
<Image style={styles.logo} source={LOGO_IMAGE} />
|
|
17
|
+
</View>
|
|
18
|
+
<Text style={styles.title}>React Native Harness</Text>
|
|
19
|
+
|
|
20
|
+
<View style={styles.errorIndicator}>
|
|
21
|
+
<View style={styles.errorDot} />
|
|
22
|
+
<Text style={styles.errorText}>Environment Error</Text>
|
|
23
|
+
</View>
|
|
24
|
+
<Text style={styles.submessage}>
|
|
25
|
+
Please double-check that you followed the installation documentation
|
|
26
|
+
carefully.
|
|
27
|
+
</Text>
|
|
28
|
+
</View>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const styles = StyleSheet.create({
|
|
34
|
+
container: {
|
|
35
|
+
flex: 1,
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
backgroundColor: '#0a1628',
|
|
39
|
+
position: 'relative',
|
|
40
|
+
},
|
|
41
|
+
safeArea: {
|
|
42
|
+
flex: 1,
|
|
43
|
+
backgroundColor: 'transparent',
|
|
44
|
+
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
|
|
45
|
+
},
|
|
46
|
+
contentContainer: {
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
padding: 24,
|
|
49
|
+
borderRadius: 24,
|
|
50
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
51
|
+
borderWidth: 1,
|
|
52
|
+
borderColor: 'rgba(239, 68, 68, 0.2)',
|
|
53
|
+
shadowColor: '#000',
|
|
54
|
+
shadowOffset: {
|
|
55
|
+
width: 0,
|
|
56
|
+
height: 20,
|
|
57
|
+
},
|
|
58
|
+
shadowOpacity: 0.3,
|
|
59
|
+
shadowRadius: 30,
|
|
60
|
+
maxWidth: 350,
|
|
61
|
+
},
|
|
62
|
+
logoContainer: {
|
|
63
|
+
marginBottom: 12,
|
|
64
|
+
},
|
|
65
|
+
logo: {
|
|
66
|
+
width: 128,
|
|
67
|
+
height: 128,
|
|
68
|
+
},
|
|
69
|
+
title: {
|
|
70
|
+
fontSize: 28,
|
|
71
|
+
fontWeight: '700',
|
|
72
|
+
color: '#38bdf8',
|
|
73
|
+
textAlign: 'center',
|
|
74
|
+
letterSpacing: 1,
|
|
75
|
+
marginBottom: 8,
|
|
76
|
+
},
|
|
77
|
+
errorIndicator: {
|
|
78
|
+
flexDirection: 'row',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
backgroundColor: 'rgba(239, 68, 68, 0.15)',
|
|
81
|
+
paddingHorizontal: 16,
|
|
82
|
+
paddingVertical: 8,
|
|
83
|
+
height: 36,
|
|
84
|
+
borderRadius: 20,
|
|
85
|
+
borderWidth: 1,
|
|
86
|
+
borderColor: 'rgba(239, 68, 68, 0.4)',
|
|
87
|
+
marginBottom: 24,
|
|
88
|
+
},
|
|
89
|
+
errorDot: {
|
|
90
|
+
width: 8,
|
|
91
|
+
height: 8,
|
|
92
|
+
borderRadius: 4,
|
|
93
|
+
backgroundColor: '#ef4444',
|
|
94
|
+
marginRight: 8,
|
|
95
|
+
shadowColor: '#ef4444',
|
|
96
|
+
shadowOffset: { width: 0, height: 0 },
|
|
97
|
+
shadowOpacity: 0.8,
|
|
98
|
+
shadowRadius: 4,
|
|
99
|
+
},
|
|
100
|
+
errorText: {
|
|
101
|
+
fontSize: 14,
|
|
102
|
+
fontWeight: '500',
|
|
103
|
+
color: '#ef4444',
|
|
104
|
+
letterSpacing: 0.5,
|
|
105
|
+
},
|
|
106
|
+
submessage: {
|
|
107
|
+
fontSize: 14,
|
|
108
|
+
fontWeight: '400',
|
|
109
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
110
|
+
textAlign: 'center',
|
|
111
|
+
letterSpacing: 0.5,
|
|
112
|
+
},
|
|
113
|
+
});
|
package/src/ui/index.ts
ADDED
package/src/ui/state.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { create, useStore } from 'zustand/react';
|
|
2
|
+
|
|
3
|
+
export type RunnerState = {
|
|
4
|
+
status: 'loading' | 'idle' | 'running';
|
|
5
|
+
setStatus: (status: 'loading' | 'idle' | 'running') => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const store = create<RunnerState>((set) => ({
|
|
9
|
+
status: 'loading',
|
|
10
|
+
setStatus: (status) => set({ status }),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
export const useRunnerStatus = () => useStore(store, (state) => state.status);
|