@react-native-harness/runtime 1.0.0-alpha.11 → 1.0.0-alpha.13

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 (47) hide show
  1. package/README.md +23 -4
  2. package/assets/moduleSystem.flow.js +8 -0
  3. package/dist/client/factory.d.ts.map +1 -1
  4. package/dist/client/factory.js +7 -2
  5. package/dist/client/getDeviceDescriptor.d.ts +1 -1
  6. package/dist/client/getDeviceDescriptor.d.ts.map +1 -1
  7. package/dist/client/getDeviceDescriptor.js +18 -6
  8. package/dist/collector/functions.d.ts.map +1 -1
  9. package/dist/collector/functions.js +8 -0
  10. package/dist/filtering/index.d.ts +2 -0
  11. package/dist/filtering/index.d.ts.map +1 -0
  12. package/dist/filtering/index.js +1 -0
  13. package/dist/filtering/testNameFilter.d.ts +6 -0
  14. package/dist/filtering/testNameFilter.d.ts.map +1 -0
  15. package/dist/filtering/testNameFilter.js +25 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/mocker/index.d.ts +1 -1
  20. package/dist/mocker/index.d.ts.map +1 -1
  21. package/dist/mocker/index.js +1 -1
  22. package/dist/mocker/registry.d.ts +2 -2
  23. package/dist/mocker/registry.d.ts.map +1 -1
  24. package/dist/mocker/registry.js +10 -4
  25. package/dist/namespace.d.ts +18 -0
  26. package/dist/namespace.d.ts.map +1 -0
  27. package/dist/namespace.js +19 -0
  28. package/dist/runner/runSuite.d.ts.map +1 -1
  29. package/dist/runner/runSuite.js +37 -0
  30. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  31. package/dist/ui/state.d.ts +1 -1
  32. package/dist/waitFor.d.ts +21 -0
  33. package/dist/waitFor.d.ts.map +1 -0
  34. package/dist/waitFor.js +133 -0
  35. package/package.json +2 -2
  36. package/src/client/factory.ts +13 -2
  37. package/src/client/getDeviceDescriptor.ts +29 -8
  38. package/src/collector/functions.ts +14 -0
  39. package/src/filtering/index.ts +1 -0
  40. package/src/filtering/testNameFilter.ts +37 -0
  41. package/src/index.ts +2 -0
  42. package/src/mocker/index.ts +7 -1
  43. package/src/mocker/metro-require.d.ts +1 -0
  44. package/src/mocker/registry.ts +13 -5
  45. package/src/namespace.ts +41 -0
  46. package/src/runner/runSuite.ts +43 -0
  47. package/src/waitFor.ts +194 -0
@@ -1,28 +1,49 @@
1
- import { Platform } from 'react-native';
1
+ import { Platform, PlatformConstants, PlatformStatic } from 'react-native';
2
+
3
+ interface PlatformKeplerStatic extends PlatformStatic {
4
+ constants: PlatformConstants;
5
+ OS: 'kepler';
6
+ Version: number;
7
+ }
8
+
9
+ const getPlatform = (): Platform | PlatformKeplerStatic => {
10
+ return Platform as Platform | PlatformKeplerStatic;
11
+ };
2
12
 
3
13
  export type DeviceDescriptor = {
4
- platform: 'ios' | 'android';
14
+ platform: 'ios' | 'android' | 'vega';
5
15
  manufacturer: string;
6
16
  model: string;
7
17
  osVersion: string;
8
18
  };
9
19
 
10
20
  export const getDeviceDescriptor = (): DeviceDescriptor => {
11
- if (Platform.OS === 'ios') {
21
+ const platform = getPlatform();
22
+
23
+ if (platform.OS === 'ios') {
12
24
  return {
13
25
  platform: 'ios',
14
26
  manufacturer: 'Apple',
15
27
  model: 'Unknown',
16
- osVersion: Platform.constants.osVersion,
28
+ osVersion: platform.constants.osVersion,
17
29
  };
18
30
  }
19
31
 
20
- if (Platform.OS === 'android') {
32
+ if (platform.OS === 'android') {
21
33
  return {
22
34
  platform: 'android',
23
- manufacturer: Platform.constants.Manufacturer,
24
- model: Platform.constants.Model,
25
- osVersion: Platform.constants.Release,
35
+ manufacturer: platform.constants.Manufacturer,
36
+ model: platform.constants.Model,
37
+ osVersion: platform.constants.Release,
38
+ };
39
+ }
40
+
41
+ if (platform.OS === 'kepler') {
42
+ return {
43
+ platform: 'vega',
44
+ manufacturer: '',
45
+ model: '',
46
+ osVersion: '',
26
47
  };
27
48
  }
28
49
 
@@ -53,7 +53,21 @@ const computeSuiteStatus = (
53
53
  ): TestStatus => {
54
54
  if (suite.options.skip) return 'skipped';
55
55
  if (suite.options.only) return 'active';
56
+
57
+ // Check if this suite has any focused content (tests or child suites)
58
+ const hasFocusedTests = suite.tests.some((test) => test.options.only);
59
+ const hasFocusedChildren = suite.suites.some(
60
+ (childSuite) =>
61
+ childSuite.options.only ||
62
+ childSuite.tests.some((test) => test.options.only)
63
+ );
64
+
65
+ // If this suite has focused content, it should be active
66
+ if (hasFocusedTests || hasFocusedChildren) return 'active';
67
+
68
+ // If parent has focused children and this suite has no focused content, skip it
56
69
  if (parentContext.hasFocusedChildren) return 'skipped';
70
+
57
71
  return 'active';
58
72
  };
59
73
 
@@ -0,0 +1 @@
1
+ export { filterTestsByName } from './testNameFilter.js';
@@ -0,0 +1,37 @@
1
+ import { TestSuite } from '@react-native-harness/bridge';
2
+
3
+ /**
4
+ * Filters tests by name pattern, matching against test names and suite+test combinations
5
+ */
6
+ export const filterTestsByName = (
7
+ suite: TestSuite,
8
+ testNamePattern: string
9
+ ): TestSuite => {
10
+ const regex = new RegExp(testNamePattern);
11
+ return filterSuiteRecursively(suite, regex);
12
+ };
13
+
14
+ const filterSuiteRecursively = (suite: TestSuite, regex: RegExp): TestSuite => {
15
+ // Filter tests in current suite - match against test name or "suite test" combination
16
+ const filteredTests = suite.tests.filter(test =>
17
+ regex.test(test.name) || regex.test(`${suite.name} ${test.name}`)
18
+ );
19
+
20
+ // Recursively filter child suites
21
+ const filteredChildSuites = suite.suites
22
+ .map(childSuite => filterSuiteRecursively(childSuite, regex))
23
+ .filter(childSuite => hasAnyActiveTests(childSuite));
24
+
25
+ return {
26
+ ...suite,
27
+ tests: filteredTests,
28
+ suites: filteredChildSuites,
29
+ };
30
+ };
31
+
32
+ const hasAnyActiveTests = (suite: TestSuite): boolean => {
33
+ const hasDirectTests = suite.tests.some(test => test.status === 'active');
34
+ const hasChildTests = suite.suites.some(childSuite => hasAnyActiveTests(childSuite));
35
+
36
+ return hasDirectTests || hasChildTests;
37
+ };
package/src/index.ts CHANGED
@@ -5,3 +5,5 @@ export * from './spy/index.js';
5
5
  export * from './expect/index.js';
6
6
  export * from './collector/index.js';
7
7
  export * from './mocker/index.js';
8
+ export * from './namespace.js';
9
+ export * from './waitFor.js';
@@ -1 +1,7 @@
1
- export { mock, requireActual, clearMocks } from './registry.js';
1
+ export {
2
+ mock,
3
+ requireActual,
4
+ clearMocks,
5
+ unmock,
6
+ resetModules,
7
+ } from './registry.js';
@@ -2,4 +2,5 @@ import type { Require } from './types.js';
2
2
 
3
3
  declare global {
4
4
  var __r: Require;
5
+ var __resetAllModules: () => void;
5
6
  }
@@ -15,11 +15,7 @@ export const clearMocks = (): void => {
15
15
  mockCache.clear();
16
16
  };
17
17
 
18
- export const getMockRegistry = (): Map<number, ModuleFactory> => {
19
- return mockRegistry;
20
- };
21
-
22
- export const getMockImplementation = (moduleId: number): unknown | null => {
18
+ const getMockImplementation = (moduleId: number): unknown | null => {
23
19
  if (mockCache.has(moduleId)) {
24
20
  return mockCache.get(moduleId);
25
21
  }
@@ -39,6 +35,18 @@ export const requireActual = <T = any>(moduleId: string): T =>
39
35
  // babel plugin will transform 'moduleId' to a number
40
36
  originalRequire(moduleId as unknown as ModuleId) as T;
41
37
 
38
+ export const unmock = (moduleId: string) => {
39
+ mockRegistry.delete(moduleId as unknown as ModuleId);
40
+ mockCache.delete(moduleId as unknown as ModuleId);
41
+ };
42
+
43
+ export const resetModules = (): void => {
44
+ mockCache.clear();
45
+
46
+ // Reset Metro's module cache
47
+ global.__resetAllModules();
48
+ };
49
+
42
50
  const mockRequire = (moduleId: string) => {
43
51
  // babel plugin will transform 'moduleId' to a number
44
52
  const mockedModule = getMockImplementation(moduleId as unknown as ModuleId);
@@ -0,0 +1,41 @@
1
+ import {
2
+ spyOn,
3
+ fn,
4
+ clearAllMocks,
5
+ resetAllMocks,
6
+ restoreAllMocks,
7
+ } from './spy/index.js';
8
+ import { mock, unmock, requireActual, resetModules } from './mocker/index.js';
9
+ import { waitFor, waitUntil } from './waitFor.js';
10
+
11
+ export type HarnessNamespace = {
12
+ spyOn: typeof spyOn;
13
+ fn: typeof fn;
14
+ mock: typeof mock;
15
+ unmock: typeof unmock;
16
+ requireActual: typeof requireActual;
17
+ clearAllMocks: typeof clearAllMocks;
18
+ resetAllMocks: typeof resetAllMocks;
19
+ restoreAllMocks: typeof restoreAllMocks;
20
+ resetModules: typeof resetModules;
21
+ waitFor: typeof waitFor;
22
+ waitUntil: typeof waitUntil;
23
+ };
24
+
25
+ const createHarnessNamespace = (): HarnessNamespace => {
26
+ return {
27
+ spyOn,
28
+ fn,
29
+ mock,
30
+ unmock,
31
+ requireActual,
32
+ clearAllMocks,
33
+ resetAllMocks,
34
+ restoreAllMocks,
35
+ resetModules,
36
+ waitFor,
37
+ waitUntil,
38
+ };
39
+ };
40
+
41
+ export const harness = createHarnessNamespace();
@@ -137,6 +137,49 @@ export const runSuite = async (
137
137
  file: context.testFilePath,
138
138
  });
139
139
 
140
+ // Check if suite should be skipped or is todo
141
+ if (suite.status === 'skipped') {
142
+ const result = {
143
+ name: suite.name,
144
+ tests: [],
145
+ suites: [],
146
+ status: 'skipped' as const,
147
+ duration: 0,
148
+ };
149
+
150
+ // Emit suite-finished event
151
+ context.events.emit({
152
+ type: 'suite-finished',
153
+ file: context.testFilePath,
154
+ name: suite.name,
155
+ duration: 0,
156
+ status: 'skipped',
157
+ });
158
+
159
+ return result;
160
+ }
161
+
162
+ if (suite.status === 'todo') {
163
+ const result = {
164
+ name: suite.name,
165
+ tests: [],
166
+ suites: [],
167
+ status: 'todo' as const,
168
+ duration: 0,
169
+ };
170
+
171
+ // Emit suite-finished event
172
+ context.events.emit({
173
+ type: 'suite-finished',
174
+ file: context.testFilePath,
175
+ name: suite.name,
176
+ duration: 0,
177
+ status: 'todo',
178
+ });
179
+
180
+ return result;
181
+ }
182
+
140
183
  const testResults: TestResult[] = [];
141
184
  const suiteResults: TestSuiteResult[] = [];
142
185
 
package/src/waitFor.ts ADDED
@@ -0,0 +1,194 @@
1
+ // This is adapted implementation of waitFor from Vitest, which can be found here:
2
+ // https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/integrations/wait.ts
3
+
4
+ export type WaitForCallback<T> = () => T | Promise<T>;
5
+
6
+ export interface WaitForOptions {
7
+ /**
8
+ * @description Time in ms between each check callback
9
+ * @default 50ms
10
+ */
11
+ interval?: number;
12
+ /**
13
+ * @description Time in ms after which the throw a timeout error
14
+ * @default 1000ms
15
+ */
16
+ timeout?: number;
17
+ }
18
+
19
+ function copyStackTrace(target: Error, source: Error) {
20
+ if (source.stack !== undefined) {
21
+ target.stack = source.stack.replace(source.message, target.message);
22
+ }
23
+ return target;
24
+ }
25
+
26
+ export function waitFor<T>(
27
+ callback: WaitForCallback<T>,
28
+ options: number | WaitForOptions = {}
29
+ ): Promise<T> {
30
+ const { interval = 50, timeout = 1000 } =
31
+ typeof options === 'number' ? { timeout: options } : options;
32
+ const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR');
33
+
34
+ return new Promise<T>((resolve, reject) => {
35
+ let lastError: unknown;
36
+ let promiseStatus: 'idle' | 'pending' | 'resolved' | 'rejected' = 'idle';
37
+ let timeoutId: ReturnType<typeof setTimeout>;
38
+ let intervalId: ReturnType<typeof setInterval>;
39
+
40
+ const onResolve = (result: T): void => {
41
+ if (timeoutId) {
42
+ clearTimeout(timeoutId);
43
+ }
44
+ if (intervalId) {
45
+ clearInterval(intervalId);
46
+ }
47
+
48
+ resolve(result);
49
+ };
50
+
51
+ const handleTimeout = (): void => {
52
+ if (intervalId) {
53
+ clearInterval(intervalId);
54
+ }
55
+ let error = lastError;
56
+ if (!error) {
57
+ error = copyStackTrace(
58
+ new Error('Timed out in waitFor!'),
59
+ STACK_TRACE_ERROR
60
+ );
61
+ }
62
+
63
+ reject(error);
64
+ };
65
+
66
+ const checkCallback = (): boolean | void => {
67
+ if (promiseStatus === 'pending') {
68
+ return;
69
+ }
70
+ try {
71
+ const result = callback();
72
+ if (
73
+ result !== null &&
74
+ typeof result === 'object' &&
75
+ typeof (result as any).then === 'function'
76
+ ) {
77
+ const thenable = result as PromiseLike<T>;
78
+ promiseStatus = 'pending';
79
+ thenable.then(
80
+ (resolvedValue) => {
81
+ promiseStatus = 'resolved';
82
+ onResolve(resolvedValue);
83
+ },
84
+ (rejectedValue) => {
85
+ promiseStatus = 'rejected';
86
+ lastError = rejectedValue;
87
+ }
88
+ );
89
+ } else {
90
+ onResolve(result as T);
91
+ return true;
92
+ }
93
+ } catch (error) {
94
+ lastError = error;
95
+ }
96
+ };
97
+
98
+ if (checkCallback() === true) {
99
+ return;
100
+ }
101
+
102
+ timeoutId = setTimeout(handleTimeout, timeout);
103
+ intervalId = setInterval(checkCallback, interval);
104
+ });
105
+ }
106
+
107
+ export type WaitUntilCallback<T> = () => T | Promise<T>;
108
+
109
+ export interface WaitUntilOptions
110
+ extends Pick<WaitForOptions, 'interval' | 'timeout'> {}
111
+
112
+ type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T;
113
+
114
+ export function waitUntil<T>(
115
+ callback: WaitUntilCallback<T>,
116
+ options: number | WaitUntilOptions = {}
117
+ ): Promise<Truthy<T>> {
118
+ const { interval = 50, timeout = 1000 } =
119
+ typeof options === 'number' ? { timeout: options } : options;
120
+ const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR');
121
+
122
+ return new Promise<Truthy<T>>((resolve, reject) => {
123
+ let promiseStatus: 'idle' | 'pending' | 'resolved' | 'rejected' = 'idle';
124
+ let timeoutId: ReturnType<typeof setTimeout>;
125
+ let intervalId: ReturnType<typeof setInterval>;
126
+
127
+ const onReject = (error?: Error) => {
128
+ if (intervalId) {
129
+ clearInterval(intervalId);
130
+ }
131
+ if (!error) {
132
+ error = copyStackTrace(
133
+ new Error('Timed out in waitUntil!'),
134
+ STACK_TRACE_ERROR
135
+ );
136
+ }
137
+ reject(error);
138
+ };
139
+
140
+ const onResolve = (result: T): true | void => {
141
+ if (!result) {
142
+ return;
143
+ }
144
+
145
+ if (timeoutId) {
146
+ clearTimeout(timeoutId);
147
+ }
148
+ if (intervalId) {
149
+ clearInterval(intervalId);
150
+ }
151
+
152
+ resolve(result as Truthy<T>);
153
+ return true;
154
+ };
155
+
156
+ const checkCallback = (): boolean | void => {
157
+ if (promiseStatus === 'pending') {
158
+ return;
159
+ }
160
+ try {
161
+ const result = callback();
162
+ if (
163
+ result !== null &&
164
+ typeof result === 'object' &&
165
+ typeof (result as any).then === 'function'
166
+ ) {
167
+ const thenable = result as PromiseLike<T>;
168
+ promiseStatus = 'pending';
169
+ thenable.then(
170
+ (resolvedValue) => {
171
+ promiseStatus = 'resolved';
172
+ onResolve(resolvedValue);
173
+ },
174
+ (rejectedValue) => {
175
+ promiseStatus = 'rejected';
176
+ onReject(rejectedValue);
177
+ }
178
+ );
179
+ } else {
180
+ return onResolve(result as T);
181
+ }
182
+ } catch (error) {
183
+ onReject(error as Error);
184
+ }
185
+ };
186
+
187
+ if (checkCallback() === true) {
188
+ return;
189
+ }
190
+
191
+ timeoutId = setTimeout(onReject, timeout);
192
+ intervalId = setInterval(checkCallback, interval);
193
+ });
194
+ }