@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.
Files changed (99) hide show
  1. package/dist/client/factory.d.ts +2 -1
  2. package/dist/client/factory.d.ts.map +1 -1
  3. package/dist/client/factory.js +53 -59
  4. package/dist/client/store.d.ts +3 -3
  5. package/dist/client/store.d.ts.map +1 -1
  6. package/dist/client/store.js +7 -7
  7. package/dist/collector/functions.d.ts +3 -3
  8. package/dist/collector/functions.d.ts.map +1 -1
  9. package/dist/collector/types.d.ts +3 -2
  10. package/dist/collector/types.d.ts.map +1 -1
  11. package/dist/collector/validation.d.ts +2 -2
  12. package/dist/collector/validation.d.ts.map +1 -1
  13. package/dist/device/index.d.ts +12 -0
  14. package/dist/device/index.d.ts.map +1 -0
  15. package/dist/device/index.js +62 -0
  16. package/dist/expect/matchers/toMatchImageSnapshot.d.ts +1 -1
  17. package/dist/expect/matchers/toMatchImageSnapshot.d.ts.map +1 -1
  18. package/dist/expect/matchers/toMatchImageSnapshot.js +4 -12
  19. package/dist/hmr.d.ts +2 -0
  20. package/dist/hmr.d.ts.map +1 -0
  21. package/dist/hmr.js +5 -0
  22. package/dist/initialize.js +14 -5
  23. package/dist/jsx/jsx-dev-runtime.d.ts +2 -1
  24. package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -1
  25. package/dist/jsx/jsx-dev-runtime.js +16 -7
  26. package/dist/logbox.d.ts +4 -0
  27. package/dist/logbox.d.ts.map +1 -0
  28. package/dist/logbox.js +18 -0
  29. package/dist/runner/hooks.d.ts +2 -1
  30. package/dist/runner/hooks.d.ts.map +1 -1
  31. package/dist/runner/hooks.js +27 -17
  32. package/dist/runner/runSuite.d.ts.map +1 -1
  33. package/dist/runner/runSuite.js +56 -6
  34. package/dist/runner/test-context.d.ts +16 -0
  35. package/dist/runner/test-context.d.ts.map +1 -0
  36. package/dist/runner/test-context.js +57 -0
  37. package/dist/runner/types.d.ts +2 -1
  38. package/dist/runner/types.d.ts.map +1 -1
  39. package/dist/test-utils/react-native-url-polyfill.d.ts +9 -0
  40. package/dist/test-utils/react-native-url-polyfill.d.ts.map +1 -0
  41. package/dist/test-utils/react-native-url-polyfill.js +1 -0
  42. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  43. package/dist/waitFor.d.ts.map +1 -1
  44. package/dist/waitFor.js +5 -3
  45. package/out-tsc/vitest/src/__tests__/device.test.d.ts +2 -0
  46. package/out-tsc/vitest/src/__tests__/device.test.d.ts.map +1 -0
  47. package/out-tsc/vitest/src/__tests__/logbox.test.d.ts +2 -0
  48. package/out-tsc/vitest/src/__tests__/logbox.test.d.ts.map +1 -0
  49. package/out-tsc/vitest/src/__tests__/runner-context.test.d.ts +2 -0
  50. package/out-tsc/vitest/src/__tests__/runner-context.test.d.ts.map +1 -0
  51. package/out-tsc/vitest/src/client/factory.d.ts +2 -1
  52. package/out-tsc/vitest/src/client/factory.d.ts.map +1 -1
  53. package/out-tsc/vitest/src/client/store.d.ts +3 -3
  54. package/out-tsc/vitest/src/client/store.d.ts.map +1 -1
  55. package/out-tsc/vitest/src/collector/functions.d.ts +3 -3
  56. package/out-tsc/vitest/src/collector/functions.d.ts.map +1 -1
  57. package/out-tsc/vitest/src/collector/types.d.ts +3 -2
  58. package/out-tsc/vitest/src/collector/types.d.ts.map +1 -1
  59. package/out-tsc/vitest/src/collector/validation.d.ts +2 -2
  60. package/out-tsc/vitest/src/collector/validation.d.ts.map +1 -1
  61. package/out-tsc/vitest/src/device/index.d.ts +12 -0
  62. package/out-tsc/vitest/src/device/index.d.ts.map +1 -0
  63. package/out-tsc/vitest/src/expect/matchers/toMatchImageSnapshot.d.ts +1 -1
  64. package/out-tsc/vitest/src/expect/matchers/toMatchImageSnapshot.d.ts.map +1 -1
  65. package/out-tsc/vitest/src/hmr.d.ts +2 -0
  66. package/out-tsc/vitest/src/hmr.d.ts.map +1 -0
  67. package/out-tsc/vitest/src/jsx/jsx-dev-runtime.d.ts +2 -1
  68. package/out-tsc/vitest/src/jsx/jsx-dev-runtime.d.ts.map +1 -1
  69. package/out-tsc/vitest/src/logbox.d.ts +4 -0
  70. package/out-tsc/vitest/src/logbox.d.ts.map +1 -0
  71. package/out-tsc/vitest/src/runner/hooks.d.ts +2 -1
  72. package/out-tsc/vitest/src/runner/hooks.d.ts.map +1 -1
  73. package/out-tsc/vitest/src/runner/runSuite.d.ts.map +1 -1
  74. package/out-tsc/vitest/src/runner/test-context.d.ts +16 -0
  75. package/out-tsc/vitest/src/runner/test-context.d.ts.map +1 -0
  76. package/out-tsc/vitest/src/runner/types.d.ts +2 -1
  77. package/out-tsc/vitest/src/runner/types.d.ts.map +1 -1
  78. package/out-tsc/vitest/src/test-utils/react-native-url-polyfill.d.ts +9 -0
  79. package/out-tsc/vitest/src/test-utils/react-native-url-polyfill.d.ts.map +1 -0
  80. package/out-tsc/vitest/src/waitFor.d.ts.map +1 -1
  81. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -1
  82. package/out-tsc/vitest/vite.config.d.ts.map +1 -1
  83. package/package.json +2 -2
  84. package/src/__tests__/runner-context.test.ts +483 -0
  85. package/src/client/factory.ts +63 -74
  86. package/src/client/store.ts +8 -8
  87. package/src/collector/functions.ts +5 -4
  88. package/src/collector/types.ts +4 -1
  89. package/src/collector/validation.ts +2 -2
  90. package/src/expect/matchers/toMatchImageSnapshot.ts +9 -23
  91. package/src/initialize.ts +14 -5
  92. package/src/jsx/jsx-dev-runtime.ts +34 -15
  93. package/src/runner/hooks.ts +43 -19
  94. package/src/runner/runSuite.ts +75 -9
  95. package/src/runner/test-context.ts +84 -0
  96. package/src/runner/types.ts +3 -0
  97. package/src/test-utils/react-native-url-polyfill.ts +1 -0
  98. package/src/waitFor.ts +8 -6
  99. 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: TestFn[];
28
- afterAll: TestFn[];
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: TestFn) {
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: TestFn) {
330
+ export function afterAll(fn: SuiteHookFn) {
330
331
  validateTestFunction(fn, 'afterAll');
331
332
 
332
333
  const currentSuite = getCurrentSuite();
@@ -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 { getClientInstance } from '../../client/store.js';
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 client = getClientInstance();
17
+ const handle = getHandle();
21
18
  const context = getHarnessContext();
22
19
 
23
- const transferId = generateTransferId();
24
- client.sendBinary(transferId, received.data);
20
+ const screenshotFile = await handle.transferScreenshot(received.data, {
21
+ width: received.width,
22
+ height: received.height,
23
+ });
25
24
 
26
- const screenshotFile = await client.rpc['device.screenshot.receive'](
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 = Shim.Event;
9
- globalThis.EventTarget = Shim.EventTarget;
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 client = await getClient();
37
+ const handle = await getClient();
29
38
 
30
39
  const deviceDescriptor = getDeviceDescriptor();
31
- await client.rpc.reportReady(deviceDescriptor);
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: any,
7
- props: any,
8
- key: any,
9
- isStaticChildren: any,
10
- source: any,
11
- self: any
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
- props = { ...props, collapsable: true };
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
- props,
42
+ nextProps,
24
43
  key,
25
44
  isStaticChildren,
26
45
  source,
@@ -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: HookType
8
- ): (() => void | Promise<void>)[] => {
9
- const hooks: (() => void | Promise<void>)[] = [];
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 === 'beforeEach' || hookType === 'beforeAll') {
20
- // For beforeEach/beforeAll: run parent hooks first (reverse the chain)
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
- if (hookType === 'beforeEach') {
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
- // For afterEach/afterAll: run child hooks first (use chain as-is)
46
+ // Run child suite hooks before parent suite hooks.
30
47
  for (const suiteInChain of suiteChain) {
31
- if (hookType === 'afterEach') {
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
  };
@@ -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
- // Run all beforeEach hooks from the current suite and its parents
82
- await runHooks(suite, 'beforeEach');
83
-
84
- // Run the actual test
85
- await test.fn();
86
-
87
- // Run all afterEach hooks from the current suite and its parents
88
- await runHooks(suite, 'afterEach');
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
+ };
@@ -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
- result !== null &&
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
  }));