Skip to content

Commit fd221aa

Browse files
committed
fix: Make runtime tests pass in the whole Debug/Release x BundleMode/LegacyEval matrix
1 parent 579d7be commit fd221aa

12 files changed

Lines changed: 290 additions & 70 deletions

File tree

apps/common-app/runtime-tests/ReJest/AutoRunRuntimeTestsRunner.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,24 +81,29 @@ export default function AutoRunRuntimeTestsRunner({
8181
setStatus(message);
8282
}
8383
},
84-
onStart: async ({ only }) => {
84+
onStart: async ({ only, include }) => {
8585
const filterSet = only ? new Set(only) : null;
86+
const includeSet = include ? new Set(include) : null;
8687
const selected = tests.filter((test) => {
8788
if (test.disabled) {
8889
return false;
8990
}
91+
if (includeSet?.has(test.testSuiteName)) {
92+
return true;
93+
}
9094
if (filterSet) {
9195
return filterSet.has(test.testSuiteName);
9296
}
9397
return !test.skipByDefault;
9498
});
9599

96-
if (filterSet) {
97-
const known = new Set(tests.map((test) => test.testSuiteName));
98-
const unknown = [...filterSet].filter((name) => !known.has(name));
99-
if (unknown.length > 0) {
100-
throw new Error(`Unknown test suites: ${unknown.join(', ')}`);
101-
}
100+
const known = new Set(tests.map((test) => test.testSuiteName));
101+
const unknown = [
102+
...(filterSet ?? []),
103+
...(includeSet ?? []),
104+
].filter((name) => !known.has(name));
105+
if (unknown.length > 0) {
106+
throw new Error(`Unknown test suites: ${unknown.join(', ')}`);
102107
}
103108

104109
const { configure, runTests } = require('./RuntimeTestsApi') as {

apps/common-app/runtime-tests/ReJest/matchers/Comparators.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { isColor, processColorNumber } from '../utils/colorUtils';
2-
1+
/* eslint-disable @typescript-eslint/no-var-requires */
32
import type { TestValue, ValidPropNames } from '../types';
43
import { ComparisonMode, isValidPropName } from '../types';
54

@@ -37,10 +36,12 @@ const COMPARATORS: {
3736
},
3837

3938
[ComparisonMode.COLOR]: (expected, value) => {
39+
const { isColor, processColor } =
40+
require('react-native-reanimated') as typeof import('react-native-reanimated');
4041
if (!isColor(expected) || !isColor(value)) {
4142
return false;
4243
}
43-
return processColorNumber(expected) === processColorNumber(value);
44+
return processColor(expected) === processColor(value);
4445
},
4546

4647
[ComparisonMode.PIXEL]: (expected, value) => {

apps/common-app/runtime-tests/ReJest/matchers/rawMatchers.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,17 +208,56 @@ export const toThrowMatcher: AsyncMatcher<ToThrowArgs> = async (
208208
let thrownException = false;
209209
let thrownExceptionMessage = null;
210210

211+
// Errors thrown on a Worklet Runtime inside a synchronous call (e.g.
212+
// `runOnUISync`) don't reject on the RN runtime — they are reported
213+
// asynchronously through `ErrorUtils.reportFatalError`. Capture the global
214+
// handler for the duration of the call so the matcher sees them regardless
215+
// of which handler (LogBox, remote reporter) is installed around the run.
216+
const errorUtils = (
217+
globalThis as {
218+
ErrorUtils?: {
219+
getGlobalHandler: () => (error: Error, isFatal?: boolean) => void;
220+
setGlobalHandler: (
221+
handler: (error: Error, isFatal?: boolean) => void
222+
) => void;
223+
};
224+
}
225+
).ErrorUtils;
226+
let uncaughtErrorMessage: string | null = null;
227+
const previousGlobalHandler = errorUtils?.getGlobalHandler();
228+
errorUtils?.setGlobalHandler((error) => {
229+
if (uncaughtErrorMessage === null) {
230+
uncaughtErrorMessage = error?.message ?? String(error);
231+
}
232+
});
233+
211234
try {
212235
await throwingFunction();
213236
} catch (e) {
214237
thrownException = true;
215238
thrownExceptionMessage = (e as Error)?.message || '';
216239
}
240+
241+
const deadline = Date.now() + 500;
242+
while (
243+
!thrownException &&
244+
uncaughtErrorMessage === null &&
245+
getCapturedConsoleErrors().consoleErrorCount < 1 &&
246+
Date.now() < deadline
247+
) {
248+
await new Promise((resolve) => setTimeout(resolve, 20));
249+
}
250+
251+
if (previousGlobalHandler) {
252+
errorUtils?.setGlobalHandler(previousGlobalHandler);
253+
}
217254
await restoreConsole();
218255

219256
const { consoleErrorCount, consoleErrorMessage } = getCapturedConsoleErrors();
220-
const errorWasThrown = thrownException || consoleErrorCount >= 1;
221-
const capturedMessage = thrownExceptionMessage || consoleErrorMessage;
257+
const errorWasThrown =
258+
thrownException || uncaughtErrorMessage !== null || consoleErrorCount >= 1;
259+
const capturedMessage =
260+
thrownExceptionMessage || uncaughtErrorMessage || consoleErrorMessage;
222261
const messageIsCorrect = errorMessage
223262
? capturedMessage.includes(errorMessage)
224263
: true;

apps/common-app/runtime-tests/ReJest/utils/remoteReporter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ export interface RemoteReporterOptions {
3737
library: string;
3838
declaredSuites: DeclaredSuite[];
3939
onStatus: (message: string) => void;
40-
onStart: (params: { only?: string[] }) => Promise<RunSummary | void>;
40+
onStart: (params: {
41+
only?: string[];
42+
include?: string[];
43+
}) => Promise<RunSummary | void>;
4144
}
4245

4346
interface StartMessage {
4447
type: 'start';
4548
only?: string[];
49+
include?: string[];
4650
}
4751

4852
interface ErrorUtilsGlobal {

apps/common-app/runtime-tests/ReJest/utils/stringFormatUtils.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,20 @@ export function formatTestName(
136136
if (Array.isArray(variableObject)) {
137137
variableObject.forEach((value, index) => {
138138
// python-like syntax ${1} {2}
139-
testName = testName.replace('${' + index + '}', valueToString(value));
139+
testName = testName
140+
.split('${' + index + '}')
141+
.join(valueToString(value));
140142
});
141143
}
142144
if (typeof variableObject === 'object') {
143145
const keys = Object.keys(variableObject);
144146
keys.forEach((k) => {
145147
// Typical object literal syntax
146-
testName = testName.replace(
147-
'${' + k + '}',
148-
valueToString(variableObject[k as keyof typeof variableObject])
149-
);
148+
testName = testName
149+
.split('${' + k + '}')
150+
.join(
151+
valueToString(variableObject[k as keyof typeof variableObject])
152+
);
150153
});
151154
}
152155

apps/common-app/runtime-tests/worklets/suites.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isBundleModeEnabled } from 'react-native-worklets';
2+
13
import type { RuntimeTestSuite } from '../types';
24

35
export const WORKLETS_TEST_SUITES: RuntimeTestSuite[] = [
@@ -37,7 +39,11 @@ export const WORKLETS_TEST_SUITES: RuntimeTestSuite[] = [
3739
require('./tests/runtimes/reactNativeImportShim.test');
3840
require('./tests/runtimes/turboModuleRegistryShim.test');
3941
},
40-
disabled: !globalThis._WORKLETS_BUNDLE_MODE_ENABLED,
42+
// With inline requires, reading `globalThis._WORKLETS_BUNDLE_MODE_ENABLED`
43+
// here would race module initialization order — the flag is only set once
44+
// some module actually evaluates `react-native-worklets`. Calling
45+
// `isBundleModeEnabled()` forces that initialization first.
46+
disabled: !isBundleModeEnabled(),
4147
skipByDefault: true,
4248
},
4349
{

apps/common-app/runtime-tests/worklets/tests/memory/createSerializable.test.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ describe('Test createSerializable', () => {
182182
});
183183

184184
test('createSerializableHostObject', async () => {
185-
const hostObjectValue = globalThis.__reanimatedModuleProxy;
185+
const hostObjectValue = globalThis.__workletsModuleProxy;
186186
const hostObjectKeys = Object.keys(hostObjectValue);
187187
scheduleOnTarget(() => {
188188
'worklet';
@@ -674,13 +674,12 @@ if (__DEV__) {
674674
describe('createSerializable for unsupported types', () => {
675675
test('throws when trying to serialize a Promise', async () => {
676676
const promise = Promise.resolve();
677+
// The exact message differs between Bundle Mode and Legacy Eval Mode
678+
// and between the RN-side and UI-side serialization paths; both
679+
// variants name the offending type.
677680
await expect(() => {
678681
createSerializable(promise);
679-
}).toThrow(
680-
globalThis._WORKLETS_BUNDLE_MODE_ENABLED
681-
? 'Cannot copy value of type `Promise`'
682-
: 'Promises cannot be converted to serializable.'
683-
);
682+
}).toThrow('Promise');
684683
});
685684

686685
test('throws when trying to serialize a Proxy', async () => {

apps/common-app/runtime-tests/worklets/tests/memory/createSerializableOnUI.test.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -523,16 +523,20 @@ describe('Test createSerializableOnUI', () => {
523523
// });
524524

525525
test('createSerializableOnUIInaccessibleObject', async () => {
526-
const clazz = runOnUISync(() => {
527-
'worklet';
528-
class Clazz {
529-
method() {}
530-
}
526+
// In Debug the unsupported value is copied back as an inaccessible
527+
// placeholder that throws on property access; in Release `runOnUISync`
528+
// throws synchronously while copying the value back. Both paths must
529+
// land inside the asserted function.
530+
await expect(() => {
531+
const clazz = runOnUISync(() => {
532+
'worklet';
533+
class Clazz {
534+
method() {}
535+
}
531536

532-
return new Clazz();
533-
});
537+
return new Clazz();
538+
});
534539

535-
await expect(() => {
536540
clazz.method();
537541
}).toThrow();
538542
});
@@ -565,12 +569,14 @@ describe('Test createSerializableOnUI', () => {
565569

566570
if (__DEV__) {
567571
test('throws when trying to serialize a Promise', async () => {
572+
// The exact message differs between Bundle Mode and Legacy Eval Mode;
573+
// both variants name the offending type.
568574
await expect(() =>
569575
runOnUISync(() => {
570576
'worklet';
571577
return Promise.resolve();
572578
})
573-
).toThrow('Cannot copy value of type `Promise`');
579+
).toThrow('Promise');
574580
});
575581
}
576582
});

apps/common-app/runtime-tests/worklets/tests/memory/isSerializableRef.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe('Test isSerializableRef', () => {
152152
});
153153

154154
test('check if createSerializable<host object> returns serializable ref', () => {
155-
const hostObjectValue = globalThis.__reanimatedModuleProxy;
155+
const hostObjectValue = globalThis.__workletsModuleProxy;
156156
const serializableRef = createSerializable(hostObjectValue);
157157

158158
expect(isSerializableRef(serializableRef)).toBe(true);

0 commit comments

Comments
 (0)