@react-native-harness/runtime 1.2.0-rc.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/factory.d.ts +2 -1
- package/dist/client/factory.d.ts.map +1 -1
- package/dist/client/factory.js +53 -59
- package/dist/client/store.d.ts +3 -3
- package/dist/client/store.d.ts.map +1 -1
- package/dist/client/store.js +7 -7
- package/dist/collector/functions.d.ts +3 -3
- package/dist/collector/functions.d.ts.map +1 -1
- package/dist/collector/types.d.ts +3 -2
- package/dist/collector/types.d.ts.map +1 -1
- package/dist/collector/validation.d.ts +2 -2
- package/dist/collector/validation.d.ts.map +1 -1
- package/dist/device/index.d.ts +12 -0
- package/dist/device/index.d.ts.map +1 -0
- package/dist/device/index.js +62 -0
- package/dist/expect/matchers/toMatchImageSnapshot.d.ts +1 -1
- package/dist/expect/matchers/toMatchImageSnapshot.d.ts.map +1 -1
- package/dist/expect/matchers/toMatchImageSnapshot.js +4 -12
- package/dist/hmr.d.ts +2 -0
- package/dist/hmr.d.ts.map +1 -0
- package/dist/hmr.js +5 -0
- package/dist/initialize.js +14 -5
- package/dist/jsx/jsx-dev-runtime.d.ts +2 -1
- package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -1
- package/dist/jsx/jsx-dev-runtime.js +16 -7
- package/dist/logbox.d.ts +4 -0
- package/dist/logbox.d.ts.map +1 -0
- package/dist/logbox.js +18 -0
- package/dist/runner/hooks.d.ts +2 -1
- package/dist/runner/hooks.d.ts.map +1 -1
- package/dist/runner/hooks.js +27 -17
- package/dist/runner/runSuite.d.ts.map +1 -1
- package/dist/runner/runSuite.js +56 -6
- package/dist/runner/test-context.d.ts +16 -0
- package/dist/runner/test-context.d.ts.map +1 -0
- package/dist/runner/test-context.js +57 -0
- package/dist/runner/types.d.ts +2 -1
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/test-utils/react-native-url-polyfill.d.ts +9 -0
- package/dist/test-utils/react-native-url-polyfill.d.ts.map +1 -0
- package/dist/test-utils/react-native-url-polyfill.js +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/waitFor.d.ts.map +1 -1
- package/dist/waitFor.js +5 -3
- package/out-tsc/vitest/src/__tests__/device.test.d.ts +2 -0
- package/out-tsc/vitest/src/__tests__/device.test.d.ts.map +1 -0
- package/out-tsc/vitest/src/__tests__/logbox.test.d.ts +2 -0
- package/out-tsc/vitest/src/__tests__/logbox.test.d.ts.map +1 -0
- package/out-tsc/vitest/src/__tests__/runner-context.test.d.ts +2 -0
- package/out-tsc/vitest/src/__tests__/runner-context.test.d.ts.map +1 -0
- package/out-tsc/vitest/src/client/factory.d.ts +2 -1
- package/out-tsc/vitest/src/client/factory.d.ts.map +1 -1
- package/out-tsc/vitest/src/client/store.d.ts +3 -3
- package/out-tsc/vitest/src/client/store.d.ts.map +1 -1
- package/out-tsc/vitest/src/collector/functions.d.ts +3 -3
- package/out-tsc/vitest/src/collector/functions.d.ts.map +1 -1
- package/out-tsc/vitest/src/collector/types.d.ts +3 -2
- package/out-tsc/vitest/src/collector/types.d.ts.map +1 -1
- package/out-tsc/vitest/src/collector/validation.d.ts +2 -2
- package/out-tsc/vitest/src/collector/validation.d.ts.map +1 -1
- package/out-tsc/vitest/src/device/index.d.ts +12 -0
- package/out-tsc/vitest/src/device/index.d.ts.map +1 -0
- package/out-tsc/vitest/src/expect/matchers/toMatchImageSnapshot.d.ts +1 -1
- package/out-tsc/vitest/src/expect/matchers/toMatchImageSnapshot.d.ts.map +1 -1
- package/out-tsc/vitest/src/hmr.d.ts +2 -0
- package/out-tsc/vitest/src/hmr.d.ts.map +1 -0
- package/out-tsc/vitest/src/jsx/jsx-dev-runtime.d.ts +2 -1
- package/out-tsc/vitest/src/jsx/jsx-dev-runtime.d.ts.map +1 -1
- package/out-tsc/vitest/src/logbox.d.ts +4 -0
- package/out-tsc/vitest/src/logbox.d.ts.map +1 -0
- package/out-tsc/vitest/src/runner/hooks.d.ts +2 -1
- package/out-tsc/vitest/src/runner/hooks.d.ts.map +1 -1
- package/out-tsc/vitest/src/runner/runSuite.d.ts.map +1 -1
- package/out-tsc/vitest/src/runner/test-context.d.ts +16 -0
- package/out-tsc/vitest/src/runner/test-context.d.ts.map +1 -0
- package/out-tsc/vitest/src/runner/types.d.ts +2 -1
- package/out-tsc/vitest/src/runner/types.d.ts.map +1 -1
- package/out-tsc/vitest/src/test-utils/react-native-url-polyfill.d.ts +9 -0
- package/out-tsc/vitest/src/test-utils/react-native-url-polyfill.d.ts.map +1 -0
- package/out-tsc/vitest/src/waitFor.d.ts.map +1 -1
- package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -1
- package/out-tsc/vitest/vite.config.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/runner-context.test.ts +483 -0
- package/src/client/factory.ts +63 -74
- package/src/client/store.ts +8 -8
- package/src/collector/functions.ts +5 -4
- package/src/collector/types.ts +4 -1
- package/src/collector/validation.ts +2 -2
- package/src/expect/matchers/toMatchImageSnapshot.ts +9 -23
- package/src/initialize.ts +14 -5
- package/src/jsx/jsx-dev-runtime.ts +34 -15
- package/src/runner/hooks.ts +43 -19
- package/src/runner/runSuite.ts +75 -9
- package/src/runner/test-context.ts +84 -0
- package/src/runner/types.ts +3 -0
- package/src/test-utils/react-native-url-polyfill.ts +1 -0
- package/src/waitFor.ts +8 -6
- package/vite.config.ts +4 -0
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
TestCase,
|
|
3
3
|
TestSuite,
|
|
4
4
|
CollectionResult,
|
|
5
|
+
SuiteHookFn,
|
|
5
6
|
} from '@react-native-harness/bridge';
|
|
6
7
|
import type { TestFn } from './types.js';
|
|
7
8
|
import { TestError } from './errors.js';
|
|
@@ -24,8 +25,8 @@ type RawTestSuite = {
|
|
|
24
25
|
tests: RawTestCase[];
|
|
25
26
|
suites: RawTestSuite[];
|
|
26
27
|
hooks: {
|
|
27
|
-
beforeAll:
|
|
28
|
-
afterAll:
|
|
28
|
+
beforeAll: SuiteHookFn[];
|
|
29
|
+
afterAll: SuiteHookFn[];
|
|
29
30
|
beforeEach: TestFn[];
|
|
30
31
|
afterEach: TestFn[];
|
|
31
32
|
};
|
|
@@ -316,7 +317,7 @@ export const test = Object.assign(
|
|
|
316
317
|
|
|
317
318
|
export const it = test;
|
|
318
319
|
|
|
319
|
-
export function beforeAll(fn:
|
|
320
|
+
export function beforeAll(fn: SuiteHookFn) {
|
|
320
321
|
validateTestFunction(fn, 'beforeAll');
|
|
321
322
|
|
|
322
323
|
const currentSuite = getCurrentSuite();
|
|
@@ -326,7 +327,7 @@ export function beforeAll(fn: TestFn) {
|
|
|
326
327
|
currentSuite.hooks.beforeAll.push(fn);
|
|
327
328
|
}
|
|
328
329
|
|
|
329
|
-
export function afterAll(fn:
|
|
330
|
+
export function afterAll(fn: SuiteHookFn) {
|
|
330
331
|
validateTestFunction(fn, 'afterAll');
|
|
331
332
|
|
|
332
333
|
const currentSuite = getCurrentSuite();
|
package/src/collector/types.ts
CHANGED
|
@@ -2,9 +2,12 @@ import { EventEmitter } from '../utils/emitter.js';
|
|
|
2
2
|
import {
|
|
3
3
|
TestCollectorEvents,
|
|
4
4
|
CollectionResult,
|
|
5
|
+
type HarnessTestContext,
|
|
5
6
|
} from '@react-native-harness/bridge';
|
|
6
7
|
|
|
7
|
-
export type TestFn = () => void | Promise<void>;
|
|
8
|
+
export type TestFn = (context: HarnessTestContext) => void | Promise<void>;
|
|
9
|
+
|
|
10
|
+
export type SuiteHookFn = () => void | Promise<void>;
|
|
8
11
|
|
|
9
12
|
export type TestCollectorEventsEmitter = EventEmitter<TestCollectorEvents>;
|
|
10
13
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TestError } from './errors.js';
|
|
2
|
-
import { TestFn } from './types.js';
|
|
2
|
+
import { TestFn, SuiteHookFn } from './types.js';
|
|
3
3
|
|
|
4
4
|
export const validateTestName = (name: string, functionName: string): void => {
|
|
5
5
|
if (!name || typeof name !== 'string' || name.trim() === '') {
|
|
@@ -10,7 +10,7 @@ export const validateTestName = (name: string, functionName: string): void => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export const validateTestFunction = (
|
|
13
|
-
fn: TestFn,
|
|
13
|
+
fn: TestFn | SuiteHookFn,
|
|
14
14
|
functionName: string
|
|
15
15
|
): void => {
|
|
16
16
|
if (typeof fn !== 'function') {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getHandle } from '../../client/store.js';
|
|
2
2
|
import type { MatcherState } from '@vitest/expect';
|
|
3
|
-
import {
|
|
4
|
-
type ImageSnapshotOptions,
|
|
5
|
-
generateTransferId,
|
|
6
|
-
} from '@react-native-harness/bridge';
|
|
3
|
+
import type { ImageSnapshotOptions } from '@react-native-harness/bridge';
|
|
7
4
|
import { getHarnessContext } from '../../runner/index.js';
|
|
8
5
|
|
|
9
6
|
type ScreenshotResult = {
|
|
@@ -17,30 +14,19 @@ export async function toMatchImageSnapshot(
|
|
|
17
14
|
received: ScreenshotResult,
|
|
18
15
|
options: ImageSnapshotOptions
|
|
19
16
|
): Promise<{ pass: boolean; message: () => string }> {
|
|
20
|
-
const
|
|
17
|
+
const handle = getHandle();
|
|
21
18
|
const context = getHarnessContext();
|
|
22
19
|
|
|
23
|
-
const
|
|
24
|
-
|
|
20
|
+
const screenshotFile = await handle.transferScreenshot(received.data, {
|
|
21
|
+
width: received.width,
|
|
22
|
+
height: received.height,
|
|
23
|
+
});
|
|
25
24
|
|
|
26
|
-
const
|
|
27
|
-
{
|
|
28
|
-
type: 'binary',
|
|
29
|
-
transferId,
|
|
30
|
-
size: received.data.length,
|
|
31
|
-
mimeType: 'image/png',
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
width: received.width,
|
|
35
|
-
height: received.height,
|
|
36
|
-
}
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const result = await client.rpc['test.matchImageSnapshot'](
|
|
25
|
+
const result = await handle.matchImageSnapshot(
|
|
40
26
|
screenshotFile,
|
|
41
27
|
context.testFilePath,
|
|
42
28
|
options,
|
|
43
|
-
context.runner
|
|
29
|
+
context.runner,
|
|
44
30
|
);
|
|
45
31
|
|
|
46
32
|
return {
|
package/src/initialize.ts
CHANGED
|
@@ -3,10 +3,19 @@ import { getClient } from './client/index.js';
|
|
|
3
3
|
import { disableHMRWhenReady } from './disableHMRWhenReady.js';
|
|
4
4
|
import { setupJestMock } from './jest-mock.js';
|
|
5
5
|
|
|
6
|
-
// Polyfill for EventTarget
|
|
6
|
+
// Polyfill for EventTarget on runtimes that don't ship one (RN's JSC).
|
|
7
|
+
// Do NOT overwrite when a native ctor already exists (RN Web / browsers):
|
|
8
|
+
// Safari's EventTarget.dispatchEvent() does an internal brand check and
|
|
9
|
+
// rejects polyfill instances with a TypeError, which breaks any
|
|
10
|
+
// DOM-event-driven flow in the page — most visibly DRM (FairPlay) via
|
|
11
|
+
// libraries that re-dispatch synthetic `encrypted` events.
|
|
7
12
|
const Shim = require('event-target-shim');
|
|
8
|
-
globalThis.Event
|
|
9
|
-
globalThis.
|
|
13
|
+
if (typeof globalThis.Event !== 'function') {
|
|
14
|
+
globalThis.Event = Shim.Event;
|
|
15
|
+
}
|
|
16
|
+
if (typeof globalThis.EventTarget !== 'function') {
|
|
17
|
+
globalThis.EventTarget = Shim.EventTarget;
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
// Setup jest mock to warn users about using Jest APIs
|
|
12
21
|
setupJestMock();
|
|
@@ -25,10 +34,10 @@ setTimeout(() => {
|
|
|
25
34
|
void (async () => {
|
|
26
35
|
try {
|
|
27
36
|
await disableHMRWhenReady(() => HMRClient.disable(), 50);
|
|
28
|
-
const
|
|
37
|
+
const handle = await getClient();
|
|
29
38
|
|
|
30
39
|
const deviceDescriptor = getDeviceDescriptor();
|
|
31
|
-
|
|
40
|
+
handle.reportReady(deviceDescriptor);
|
|
32
41
|
} catch (error) {
|
|
33
42
|
console.error('Failed to initialize React Native Harness', error);
|
|
34
43
|
}
|
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
import * as ReactJSXRuntimeDev from 'react/jsx-dev-runtime';
|
|
2
2
|
|
|
3
|
+
type NamedElementType = {
|
|
4
|
+
displayName?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const isNamedElementType = (value: unknown): value is NamedElementType =>
|
|
9
|
+
(typeof value === 'function' ||
|
|
10
|
+
(typeof value === 'object' && value !== null)) &&
|
|
11
|
+
('displayName' in value || 'name' in value);
|
|
12
|
+
|
|
13
|
+
const isPropsObject = (value: unknown): value is Record<string, unknown> =>
|
|
14
|
+
typeof value === 'object' && value !== null;
|
|
15
|
+
|
|
3
16
|
export const Fragment = ReactJSXRuntimeDev.Fragment;
|
|
4
17
|
|
|
5
|
-
export function jsxDEV(
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
13
|
-
if (
|
|
14
|
-
type &&
|
|
15
|
-
(type.displayName === 'View' || type.name === 'View') &&
|
|
16
|
-
props &&
|
|
18
|
+
export function jsxDEV(...args: Parameters<typeof ReactJSXRuntimeDev.jsxDEV>) {
|
|
19
|
+
const [type, props, key, isStaticChildren, source, self] = args;
|
|
20
|
+
const isViewType =
|
|
21
|
+
isNamedElementType(type) &&
|
|
22
|
+
(type.displayName === 'View' || type.name === 'View');
|
|
23
|
+
const nextProps =
|
|
24
|
+
isViewType &&
|
|
25
|
+
isPropsObject(props) &&
|
|
17
26
|
props.collapsable === undefined
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
? { ...props, collapsable: true }
|
|
28
|
+
: props;
|
|
29
|
+
|
|
30
|
+
if (isViewType && isPropsObject(props) && props.collapsable === undefined) {
|
|
31
|
+
return ReactJSXRuntimeDev.jsxDEV(
|
|
32
|
+
type,
|
|
33
|
+
nextProps,
|
|
34
|
+
key,
|
|
35
|
+
isStaticChildren,
|
|
36
|
+
source,
|
|
37
|
+
self
|
|
38
|
+
);
|
|
20
39
|
}
|
|
21
40
|
return ReactJSXRuntimeDev.jsxDEV(
|
|
22
41
|
type,
|
|
23
|
-
|
|
42
|
+
nextProps,
|
|
24
43
|
key,
|
|
25
44
|
isStaticChildren,
|
|
26
45
|
source,
|
package/src/runner/hooks.ts
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
import type { TestSuite } from '@react-native-harness/bridge';
|
|
1
|
+
import type { SuiteHookFn, TestFn, TestSuite } from '@react-native-harness/bridge';
|
|
2
|
+
import type { ActiveTestContext } from './types.js';
|
|
2
3
|
|
|
3
4
|
export type HookType = 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll';
|
|
4
5
|
|
|
5
6
|
const collectInheritedHooks = (
|
|
6
7
|
suite: TestSuite,
|
|
7
|
-
hookType:
|
|
8
|
-
):
|
|
9
|
-
const hooks:
|
|
8
|
+
hookType: 'beforeEach' | 'afterEach'
|
|
9
|
+
): TestFn[] => {
|
|
10
|
+
const hooks: TestFn[] = [];
|
|
11
|
+
const suiteChain: TestSuite[] = [];
|
|
12
|
+
|
|
13
|
+
let current: TestSuite | undefined = suite;
|
|
14
|
+
while (current) {
|
|
15
|
+
suiteChain.unshift(current);
|
|
16
|
+
current = current.parent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const currentSuite of suiteChain) {
|
|
20
|
+
hooks.push(...currentSuite[hookType]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return hooks;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const collectSuiteHooks = (
|
|
27
|
+
suite: TestSuite,
|
|
28
|
+
hookType: 'beforeAll' | 'afterAll'
|
|
29
|
+
): SuiteHookFn[] => {
|
|
30
|
+
const hooks: SuiteHookFn[] = [];
|
|
10
31
|
const suiteChain: TestSuite[] = [];
|
|
11
32
|
|
|
12
33
|
// Collect all suites from current to root
|
|
@@ -16,23 +37,15 @@ const collectInheritedHooks = (
|
|
|
16
37
|
currentSuite = currentSuite.parent;
|
|
17
38
|
}
|
|
18
39
|
|
|
19
|
-
if (hookType === '
|
|
20
|
-
//
|
|
40
|
+
if (hookType === 'beforeAll') {
|
|
41
|
+
// Run parent suite hooks before child suite hooks.
|
|
21
42
|
for (let i = suiteChain.length - 1; i >= 0; i--) {
|
|
22
|
-
|
|
23
|
-
hooks.push(...suiteChain[i].beforeEach);
|
|
24
|
-
} else {
|
|
25
|
-
hooks.push(...suiteChain[i].beforeAll);
|
|
26
|
-
}
|
|
43
|
+
hooks.push(...suiteChain[i].beforeAll);
|
|
27
44
|
}
|
|
28
45
|
} else {
|
|
29
|
-
//
|
|
46
|
+
// Run child suite hooks before parent suite hooks.
|
|
30
47
|
for (const suiteInChain of suiteChain) {
|
|
31
|
-
|
|
32
|
-
hooks.push(...suiteInChain.afterEach);
|
|
33
|
-
} else {
|
|
34
|
-
hooks.push(...suiteInChain.afterAll);
|
|
35
|
-
}
|
|
48
|
+
hooks.push(...suiteInChain.afterAll);
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
51
|
|
|
@@ -41,11 +54,22 @@ const collectInheritedHooks = (
|
|
|
41
54
|
|
|
42
55
|
export const runHooks = async (
|
|
43
56
|
suite: TestSuite,
|
|
44
|
-
hookType: HookType
|
|
57
|
+
hookType: HookType,
|
|
58
|
+
context?: ActiveTestContext,
|
|
45
59
|
): Promise<void> => {
|
|
60
|
+
if (hookType === 'beforeAll' || hookType === 'afterAll') {
|
|
61
|
+
const hooks = collectSuiteHooks(suite, hookType);
|
|
62
|
+
|
|
63
|
+
for (const hook of hooks) {
|
|
64
|
+
await hook();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
const hooks = collectInheritedHooks(suite, hookType);
|
|
47
71
|
|
|
48
72
|
for (const hook of hooks) {
|
|
49
|
-
await hook();
|
|
73
|
+
await hook(context as ActiveTestContext);
|
|
50
74
|
}
|
|
51
75
|
};
|
package/src/runner/runSuite.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
HarnessTaskContext,
|
|
2
3
|
TestCase,
|
|
3
4
|
TestResult,
|
|
4
5
|
TestSuite,
|
|
@@ -11,7 +12,14 @@ import {
|
|
|
11
12
|
import { flushExpectTestState } from '../expect/errors.js';
|
|
12
13
|
import { runHooks } from './hooks.js';
|
|
13
14
|
import { getTestExecutionError } from './errors.js';
|
|
14
|
-
import { TestRunnerContext } from './types.js';
|
|
15
|
+
import { ActiveTestContext, TestRunnerContext } from './types.js';
|
|
16
|
+
import {
|
|
17
|
+
createTestContext,
|
|
18
|
+
createTestLifecycleState,
|
|
19
|
+
isSkipTestError,
|
|
20
|
+
runOnTestFailed,
|
|
21
|
+
runOnTestFinished,
|
|
22
|
+
} from './test-context.js';
|
|
15
23
|
|
|
16
24
|
declare global {
|
|
17
25
|
var HARNESS_TEST_PATH: string;
|
|
@@ -23,6 +31,27 @@ const runTest = async (
|
|
|
23
31
|
context: TestRunnerContext,
|
|
24
32
|
): Promise<TestResult> => {
|
|
25
33
|
const startTime = Date.now();
|
|
34
|
+
const task: HarnessTaskContext = {
|
|
35
|
+
name: test.name,
|
|
36
|
+
type: 'test',
|
|
37
|
+
mode:
|
|
38
|
+
test.status === 'active'
|
|
39
|
+
? 'run'
|
|
40
|
+
: test.status === 'skipped'
|
|
41
|
+
? 'skip'
|
|
42
|
+
: 'todo',
|
|
43
|
+
file: {
|
|
44
|
+
name: context.testFilePath,
|
|
45
|
+
},
|
|
46
|
+
suite: {
|
|
47
|
+
name: suite.name,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const lifecycleState = createTestLifecycleState();
|
|
51
|
+
const activeTestContext: ActiveTestContext = createTestContext(
|
|
52
|
+
task,
|
|
53
|
+
lifecycleState,
|
|
54
|
+
);
|
|
26
55
|
|
|
27
56
|
// Emit test-started event
|
|
28
57
|
context.events.emit({
|
|
@@ -78,16 +107,50 @@ const runTest = async (
|
|
|
78
107
|
setCurrentExpectTestState(expectTestState);
|
|
79
108
|
|
|
80
109
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
let didSkip = false;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Run all beforeEach hooks from the current suite and its parents
|
|
114
|
+
await runHooks(suite, 'beforeEach', activeTestContext);
|
|
115
|
+
|
|
116
|
+
// Run the actual test
|
|
117
|
+
await test.fn(activeTestContext);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (!isSkipTestError(error)) {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
didSkip = true;
|
|
124
|
+
} finally {
|
|
125
|
+
// Run all afterEach hooks from the current suite and its parents
|
|
126
|
+
await runHooks(suite, 'afterEach', activeTestContext);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (didSkip) {
|
|
130
|
+
const duration = Date.now() - startTime;
|
|
131
|
+
|
|
132
|
+
await runOnTestFinished(lifecycleState);
|
|
133
|
+
|
|
134
|
+
const result = {
|
|
135
|
+
name: test.name,
|
|
136
|
+
status: 'skipped' as const,
|
|
137
|
+
duration,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
context.events.emit({
|
|
141
|
+
type: 'test-finished',
|
|
142
|
+
file: context.testFilePath,
|
|
143
|
+
suite: suite.name,
|
|
144
|
+
name: test.name,
|
|
145
|
+
duration,
|
|
146
|
+
status: 'skipped',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
89
151
|
|
|
90
152
|
await flushExpectTestState(expectTestState);
|
|
153
|
+
await runOnTestFinished(lifecycleState);
|
|
91
154
|
} finally {
|
|
92
155
|
setCurrentExpectTestState(undefined);
|
|
93
156
|
}
|
|
@@ -112,6 +175,9 @@ const runTest = async (
|
|
|
112
175
|
|
|
113
176
|
return result;
|
|
114
177
|
} catch (error) {
|
|
178
|
+
await runOnTestFailed(lifecycleState);
|
|
179
|
+
await runOnTestFinished(lifecycleState);
|
|
180
|
+
|
|
115
181
|
const testError = await getTestExecutionError(
|
|
116
182
|
error,
|
|
117
183
|
context.testFilePath,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { HarnessTaskContext } from '@react-native-harness/bridge';
|
|
2
|
+
import type { ActiveTestContext } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type TestLifecycleState = {
|
|
5
|
+
onTestFailed: Array<() => void | Promise<void>>;
|
|
6
|
+
onTestFinished: Array<() => void | Promise<void>>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export class SkipTestError extends Error {
|
|
10
|
+
note?: string;
|
|
11
|
+
|
|
12
|
+
constructor(note?: string) {
|
|
13
|
+
super(note ?? 'Test skipped');
|
|
14
|
+
this.name = 'SkipTestError';
|
|
15
|
+
this.note = note;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const isSkipTestError = (error: unknown): error is SkipTestError => {
|
|
20
|
+
return error instanceof SkipTestError;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const createSkip = () => {
|
|
24
|
+
function skip(noteOrCondition?: boolean | string, note?: string): void {
|
|
25
|
+
if (typeof noteOrCondition === 'boolean') {
|
|
26
|
+
if (!noteOrCondition) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new SkipTestError(note);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new SkipTestError(noteOrCondition);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return skip as ActiveTestContext['skip'];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const createOnTestFinished = (state: TestLifecycleState) => {
|
|
40
|
+
return (fn: () => void | Promise<void>): void => {
|
|
41
|
+
state.onTestFinished.push(fn);
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createOnTestFailed = (state: TestLifecycleState) => {
|
|
46
|
+
return (fn: () => void | Promise<void>): void => {
|
|
47
|
+
state.onTestFailed.push(fn);
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const createTestLifecycleState = (): TestLifecycleState => {
|
|
52
|
+
return {
|
|
53
|
+
onTestFailed: [],
|
|
54
|
+
onTestFinished: [],
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const runOnTestFailed = async (
|
|
59
|
+
state: TestLifecycleState,
|
|
60
|
+
): Promise<void> => {
|
|
61
|
+
for (let i = state.onTestFailed.length - 1; i >= 0; i--) {
|
|
62
|
+
await state.onTestFailed[i]();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const runOnTestFinished = async (
|
|
67
|
+
state: TestLifecycleState,
|
|
68
|
+
): Promise<void> => {
|
|
69
|
+
for (let i = state.onTestFinished.length - 1; i >= 0; i--) {
|
|
70
|
+
await state.onTestFinished[i]();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const createTestContext = (
|
|
75
|
+
task: HarnessTaskContext,
|
|
76
|
+
state: TestLifecycleState,
|
|
77
|
+
): ActiveTestContext => {
|
|
78
|
+
return {
|
|
79
|
+
task,
|
|
80
|
+
onTestFailed: createOnTestFailed(state),
|
|
81
|
+
onTestFinished: createOnTestFinished(state),
|
|
82
|
+
skip: createSkip(),
|
|
83
|
+
};
|
|
84
|
+
};
|
package/src/runner/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from '../utils/emitter.js';
|
|
2
2
|
import type {
|
|
3
|
+
HarnessTestContext,
|
|
3
4
|
TestRunnerEvents,
|
|
4
5
|
TestSuite,
|
|
5
6
|
TestSuiteResult,
|
|
@@ -12,6 +13,8 @@ export type TestRunnerContext = {
|
|
|
12
13
|
testFilePath: string;
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
export type ActiveTestContext = HarnessTestContext;
|
|
17
|
+
|
|
15
18
|
export type RunTestsOptions = {
|
|
16
19
|
testSuite: TestSuite;
|
|
17
20
|
testFilePath: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const URL = globalThis.URL;
|
package/src/waitFor.ts
CHANGED
|
@@ -114,6 +114,12 @@ export interface WaitUntilOptions
|
|
|
114
114
|
|
|
115
115
|
type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T;
|
|
116
116
|
|
|
117
|
+
const isPromiseLike = <T>(value: T | PromiseLike<T>): value is PromiseLike<T> =>
|
|
118
|
+
value !== null &&
|
|
119
|
+
typeof value === 'object' &&
|
|
120
|
+
'then' in value &&
|
|
121
|
+
typeof value.then === 'function';
|
|
122
|
+
|
|
117
123
|
export function waitUntil<T>(
|
|
118
124
|
callback: WaitUntilCallback<T>,
|
|
119
125
|
options: number | WaitUntilOptions = {}
|
|
@@ -164,12 +170,8 @@ export function waitUntil<T>(
|
|
|
164
170
|
}
|
|
165
171
|
try {
|
|
166
172
|
const result = callback();
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
typeof result === 'object' &&
|
|
170
|
-
typeof (result as any).then === 'function'
|
|
171
|
-
) {
|
|
172
|
-
const thenable = result as PromiseLike<T>;
|
|
173
|
+
if (isPromiseLike(result)) {
|
|
174
|
+
const thenable = result;
|
|
173
175
|
promiseStatus = 'pending';
|
|
174
176
|
thenable.then(
|
|
175
177
|
(resolvedValue) => {
|
package/vite.config.ts
CHANGED
|
@@ -22,6 +22,10 @@ export default defineConfig(() => ({
|
|
|
22
22
|
alias: {
|
|
23
23
|
'@vitest/spy': path.resolve(__dirname, 'node_modules/@vitest/spy'),
|
|
24
24
|
'@vitest/expect': path.resolve(__dirname, 'node_modules/@vitest/expect'),
|
|
25
|
+
'react-native-url-polyfill': path.resolve(
|
|
26
|
+
__dirname,
|
|
27
|
+
'src/test-utils/react-native-url-polyfill.ts',
|
|
28
|
+
),
|
|
25
29
|
},
|
|
26
30
|
},
|
|
27
31
|
}));
|