diff --git a/RNReanimated.podspec b/RNReanimated.podspec index 8e539f565d5c..cbb4ddd1a554 100644 --- a/RNReanimated.podspec +++ b/RNReanimated.podspec @@ -86,8 +86,8 @@ Pod::Spec.new do |s| "Common/cpp/hidden_headers/**" ] - gcc_debug_definitions = "$(inherited)" - if $config[:react_native_minor_version] >= 73 || !is_release + gcc_debug_definitions = "$(inherited)" + if config[:react_native_minor_version] >= 73 || !is_release gcc_debug_definitions << " HERMES_ENABLE_DEBUGGER=1" end diff --git a/app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsApi.ts b/app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsApi.ts new file mode 100644 index 000000000000..1c1dc72e273d --- /dev/null +++ b/app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsApi.ts @@ -0,0 +1,230 @@ +import { makeMutable, runOnJS, runOnUI } from "react-native-reanimated"; +import { TestRunner } from "./TestRunner"; +import { TestComponent } from "./TestComponent"; + +const testRunner = new TestRunner(); + +export function describe(name: string, buildSuite: () => void) { + testRunner.describe(name, buildSuite); +}; + +export function beforeAll(job: () => void) { + testRunner.beforeAll(job); +} + +export function beforeEach(job: () => void) { + testRunner.beforeEach(job); +} + +export function afterEach(job: () => void) { + testRunner.afterEach(job); +} + +export function afterAll(job: () => void) { + testRunner.afterAll(job); +} + +export function test(name: string, testCase: () => void) { + testRunner.test(name, testCase); +}; + +export async function render(component: any) { + return testRunner.render(component); +} + +function waitForPropertyValueChange(targetObject, targetProperty, initialValue = true) { + return new Promise((resolve) => { + const interval = setInterval(() => { + if (targetObject[targetProperty] != initialValue) { + clearInterval(interval); + resolve(targetObject[targetProperty]); + } + }, 10); + }); +} + +export function useTestRef(name: string): React.MutableRefObject { + return testRunner.useTestRef(name); +} + +export function getTestComponent(name: string): TestComponent { + return testRunner.getTestComponent(name); +} + +export async function runTests() { + await testRunner.runTests(); +} + +export async function wait(delay: number) { + return new Promise((resolve) => { + setTimeout(resolve, delay); + }); +} + +export function expect(value: any) { + return testRunner.expect(value); +}; + +export function configure(config: any) { + return testRunner.configure(config); +} + +const lockObject = { + lock: false +} +function unlock() { + lockObject.lock = false; +} +async function runOnUiSync(worklet: () => void) { + lockObject.lock = true; + runOnUI(() => { + "worklet"; + worklet(); + runOnJS(unlock)(); + })(); + await waitForPropertyValueChange(lockObject, "lock", true); +} + +export async function mockAnimationTimer() { + await runOnUiSync(() => { + "worklet"; + global.mockedAnimationTimestamp = 0; + global.originalGetAnimationTimestamp = global._getAnimationTimestamp; + global._getAnimationTimestamp = () => { + const currentTimestamp = global.mockedAnimationTimestamp; + global.__frameTimestamp = currentTimestamp; + global.mockedAnimationTimestamp += 16; + return currentTimestamp; + }; + let originalRequestAnimationFrame = global.requestAnimationFrame; + global.originalRequestAnimationFrame = originalRequestAnimationFrame; + (global as any).requestAnimationFrame = (callback) => { + originalRequestAnimationFrame(() => { + callback(global._getAnimationTimestamp()); + }); + } + }) +} + +export async function unmockAnimationTimer() { + await runOnUiSync(() => { + "worklet"; + if (global.originalGetAnimationTimestamp) { + global._getAnimationTimestamp = global.originalGetAnimationTimestamp; + global.originalGetAnimationTimestamp = undefined; + } + if (global.originalRequestAnimationFrame) { + global.requestAnimationFrame = global.originalRequestAnimationFrame; + global.originalRequestAnimationFrame = undefined; + } + if (global.mockedAnimationTimestamp) { + global.mockedAnimationTimestamp = undefined; + } + }); +} + +export async function setAnimationTimestamp(timestamp: number) { + await runOnUiSync(() => { + "worklet"; + global.mockedAnimationTimestamp = timestamp; + }) +} + +export async function advanceAnimationByTime(time: number) { + await runOnUiSync(() => { + "worklet"; + global.mockedAnimationTimestamp += time; + }) +} + +export async function advanceAnimationByFrames(frameCount: number) { + await runOnUiSync(() => { + "worklet"; + global.mockedAnimationTimestamp += frameCount * 16; + }) +} + +export async function recordAnimationUpdates(mergeOperations = true) { + const updates = makeMutable(null); + await runOnUiSync(() => { + "worklet"; + const originalUpdateProps = global._IS_FABRIC ? global._updatePropsFabric : global._updatePropsPaper; + global.originalUpdateProps = originalUpdateProps; + const mockedUpdateProps = mergeOperations + ? (operations) => { + if (updates.value == null) { + updates.value = []; + } + const newUpdates: any = []; + for (const operation of operations) { + newUpdates.push(operation.updates); + } + for (const newUpdate of newUpdates) { + updates.value.push(newUpdate); + } + updates.value = [...updates.value]; + originalUpdateProps(operations); + } + : (operations) => { + if (updates.value == null) { + updates.value = {}; + } + for (const operation of operations) { + if (updates.value[operation.tag] == undefined) { + updates.value[operation.tag] = []; + } + updates.value[operation.tag].push(updates.value[operation.tag]); + } + updates.value = operations; + originalUpdateProps(operations); + }; + + if (global._IS_FABRIC) { + global._updatePropsFabric = mockedUpdateProps; + } else { + global._updatePropsPaper = mockedUpdateProps; + } + + const originalNotifyAboutProgress = global._notifyAboutProgress; + global.originalNotifyAboutProgress = originalNotifyAboutProgress; + global._notifyAboutProgress = mergeOperations + ? (tag, value, isSharedTransition) => { + if (updates.value == null) { + updates.value = []; + } + updates.value.push({... value}); + updates.value = [...updates.value]; + originalNotifyAboutProgress(tag, value, isSharedTransition); + } + : (tag, value, isSharedTransition) => { + if (updates.value == null) { + updates.value = {}; + } + if (updates.value[tag] == undefined) { + updates.value[tag] = []; + } + updates.value[tag].push(value); + updates.value = { ...updates.value }; + originalNotifyAboutProgress(tag, value, isSharedTransition); + } + }); + return updates; +} + +export async function stopRecordingAnimationUpdates() { + await runOnUiSync(() => { + "worklet"; + if (global.originalUpdateProps) { + if (global._IS_FABRIC) { + global._updatePropsFabric = global.originalUpdateProps; + } else { + global._updatePropsPaper = global.originalUpdateProps; + } + global.originalUpdateProps = undefined; + } + if (global.originalNotifyAboutProgress) { + global._notifyAboutProgress = global.originalNotifyAboutProgress; + global.originalNotifyAboutProgress = undefined; + } + }); +} diff --git a/app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx b/app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx new file mode 100644 index 000000000000..25dcfdd63c12 --- /dev/null +++ b/app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx @@ -0,0 +1,30 @@ +import { View, Button, StyleSheet, Text } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import { runTests, configure } from './RuntimeTestsApi'; + +let renderLock: { lock: boolean } = { lock: false }; +export default function RuntimeTestsRunner() { + const [component, renderComponent] = useState(false); + useEffect(() => { + if (renderLock) { + renderLock.lock = false; + } + }, [component]); + return ( + +