diff --git a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx index 8f8569d80bf..bfb4de4fdbc 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx +++ b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx @@ -2,9 +2,9 @@ import { View, Button, StyleSheet, Text } from 'react-native'; import type { ReactNode } from 'react'; import React, { useEffect, useState } from 'react'; import { runTests, configure } from './RuntimeTestsApi'; -import type { LockObject } from './types'; +import { RenderLock } from './SyncUIRunner'; -let renderLock: LockObject = { lock: false }; +let renderLock: RenderLock = new RenderLock(); export class ErrorBoundary extends React.Component< { children: React.JSX.Element | Array }, { hasError: boolean } @@ -32,7 +32,7 @@ export default function RuntimeTestsRunner() { const [component, setComponent] = useState(null); useEffect(() => { if (renderLock) { - renderLock.lock = false; + renderLock.unlock(); } }, [component]); return ( diff --git a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/SyncUIRunner.ts b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/SyncUIRunner.ts new file mode 100644 index 00000000000..aef35dddd6f --- /dev/null +++ b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/SyncUIRunner.ts @@ -0,0 +1,53 @@ +import { runOnJS, runOnUI } from 'react-native-reanimated'; +import type { LockObject } from './types'; + +class WaitForUnlock { + private _lock: LockObject = { + lock: false, + }; + + _setLock(value: boolean) { + this._lock = { lock: value }; + } + + _waitForUnlock(maxWaitTime?: number) { + return new Promise(resolve => { + const startTime = performance.now(); + const interval = setInterval(() => { + const currentTime = performance.now(); + const waitTimeExceeded = maxWaitTime && maxWaitTime < currentTime - startTime; + if (this._lock.lock !== true || waitTimeExceeded) { + clearInterval(interval); + resolve(this._lock.lock); + } + }, 10); + }); + } +} + +export class SyncUIRunner extends WaitForUnlock { + public async runOnUIBlocking(worklet: () => void, maxWaitTime?: number) { + const unlock = () => this._setLock(false); + this._setLock(true); + runOnUI(() => { + 'worklet'; + worklet(); + runOnJS(unlock)(); + })(); + await this._waitForUnlock(maxWaitTime); + } +} + +export class RenderLock extends WaitForUnlock { + public lock() { + this._setLock(true); + } + + public unlock() { + this._setLock(false); + } + + public async waitForUnlock(maxWaitTime?: number) { + await this._waitForUnlock(maxWaitTime); + } +} diff --git a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/TestRunner.ts b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/TestRunner.ts index 2a403b01bda..2b1a213e127 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/TestRunner.ts +++ b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/TestRunner.ts @@ -3,7 +3,6 @@ import { useRef } from 'react'; import type { BuildFunction, NullableTestValue, - LockObject, Operation, SharedValueSnapshot, TestCase, @@ -17,10 +16,11 @@ import { DescribeDecorator, TestDecorator } from './types'; import { TestComponent } from './TestComponent'; import { EMPTY_LOG_PLACEHOLDER, applyMarkdown, color, formatString, indentNestingLevel } from './stringFormatUtils'; import type { SharedValue } from 'react-native-reanimated'; -import { makeMutable, runOnUI, runOnJS } from 'react-native-reanimated'; +import { makeMutable, runOnJS } from 'react-native-reanimated'; import { Matchers, nullableMatch } from './matchers/Matchers'; import { assertMockedAnimationTimestamp, assertTestCase, assertTestSuite } from './Asserts'; import { createUpdatesContainer } from './UpdatesContainer'; +import { RenderLock, SyncUIRunner } from './SyncUIRunner'; let callTrackerRegistryJS: Record = {}; const callTrackerRegistryUI = makeMutable>({}); @@ -41,10 +41,11 @@ export class TestRunner { private _currentTestSuite: TestSuite | null = null; private _currentTestCase: TestCase | null = null; private _renderHook: (component: ReactElement | null) => void = () => {}; - private _renderLock: LockObject = { lock: false }; private _valueRegistry: Record = {}; private _wasRenderedNull: boolean = false; private _includesOnly: boolean = false; + private _syncUIRunner: SyncUIRunner = new SyncUIRunner(); + private _renderLock: RenderLock = new RenderLock(); private _summary: TestSummary = { passed: 0, failed: 0, @@ -54,10 +55,6 @@ export class TestRunner { endTime: 0, }; - private _threadLock: LockObject = { - lock: false, - }; - public notify(name: string) { 'worklet'; if (_WORKLET) { @@ -88,13 +85,14 @@ export class TestRunner { return; } this._wasRenderedNull = !component; - this._renderLock.lock = true; + this._renderLock.lock(); + try { this._renderHook(component); } catch (e) { console.log(e); } - return this.waitForPropertyValueChange(this._renderLock, 'lock'); + return this._renderLock.waitForUnlock(); } public async clearRenderOutput() { @@ -207,7 +205,7 @@ export class TestRunner { const jsValue = this._valueRegistry[name].value; const sharedValue = this._valueRegistry[name]; const valueContainer = makeMutable(null); - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; valueContainer.value = sharedValue.value; }, 1000); @@ -379,42 +377,12 @@ export class TestRunner { this._currentTestSuite.afterEach = job; } - private waitForPropertyValueChange( - targetObject: LockObject, - targetProperty: 'lock', - initialValue = true, - maxWaitTime?: number, - ) { - return new Promise(resolve => { - const startTime = performance.now(); - const interval = setInterval(() => { - const currentTime = performance.now(); - const waitTimeExceeded = maxWaitTime && maxWaitTime < currentTime - startTime; - if (targetObject[targetProperty] !== initialValue || waitTimeExceeded) { - clearInterval(interval); - resolve(targetObject[targetProperty]); - } - }, 10); - }); - } - - public async runOnUIBlocking(worklet: () => void, maxWaitTime?: number) { - const unlock = () => (this._threadLock.lock = false); - this._threadLock.lock = true; - runOnUI(() => { - 'worklet'; - worklet(); - runOnJS(unlock)(); - })(); - await this.waitForPropertyValueChange(this._threadLock, 'lock', true, maxWaitTime); - } - public async recordAnimationUpdates() { - const updatesContainer = createUpdatesContainer(this); + const updatesContainer = createUpdatesContainer(); const recordAnimationUpdates = updatesContainer.pushAnimationUpdates; const recordLayoutAnimationUpdates = updatesContainer.pushLayoutAnimationUpdates; - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; const originalUpdateProps = global._IS_FABRIC ? global._updatePropsFabric : global._updatePropsPaper; global.originalUpdateProps = originalUpdateProps; @@ -441,7 +409,7 @@ export class TestRunner { } public async stopRecordingAnimationUpdates() { - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; if (global.originalUpdateProps) { if (global._IS_FABRIC) { @@ -459,7 +427,7 @@ export class TestRunner { } public async mockAnimationTimer() { - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; global.mockedAnimationTimestamp = 0; global.originalGetAnimationTimestamp = global._getAnimationTimestamp; @@ -489,7 +457,7 @@ export class TestRunner { } public async setAnimationTimestamp(timestamp: number) { - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; assertMockedAnimationTimestamp(global.mockedAnimationTimestamp); global.mockedAnimationTimestamp = timestamp; @@ -497,7 +465,7 @@ export class TestRunner { } public async advanceAnimationByTime(time: number) { - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; assertMockedAnimationTimestamp(global.mockedAnimationTimestamp); global.mockedAnimationTimestamp += time; @@ -505,7 +473,7 @@ export class TestRunner { } public async advanceAnimationByFrames(frameCount: number) { - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; assertMockedAnimationTimestamp(global.mockedAnimationTimestamp); global.mockedAnimationTimestamp += frameCount * 16; @@ -513,7 +481,7 @@ export class TestRunner { } public async unmockAnimationTimer() { - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; if (global.originalGetAnimationTimestamp) { global._getAnimationTimestamp = global.originalGetAnimationTimestamp; @@ -583,7 +551,7 @@ export class TestRunner { }; console.error = mockedConsoleFunction; console.warn = mockedConsoleFunction; - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; console.error = mockedConsoleFunction; console.warn = mockedConsoleFunction; @@ -592,7 +560,7 @@ export class TestRunner { const restoreConsole = async () => { console.error = originalError; console.warn = originalWarning; - await this.runOnUIBlocking(() => { + await this._syncUIRunner.runOnUIBlocking(() => { 'worklet'; console.error = originalError; console.warn = originalWarning; diff --git a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/UpdatesContainer.ts b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/UpdatesContainer.ts index b67838377e4..50da1f477a5 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/UpdatesContainer.ts +++ b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/UpdatesContainer.ts @@ -1,10 +1,10 @@ import { makeMutable } from 'react-native-reanimated'; import type { Operation, OperationUpdate } from './types'; import { isValidPropName } from './types'; -import type { TestRunner } from './TestRunner'; import type { MultiViewSnapshot, SingleViewSnapshot } from './matchers/snapshotMatchers'; import { convertDecimalColor } from './util'; import type { TestComponent } from './TestComponent'; +import { SyncUIRunner } from './SyncUIRunner'; type JsUpdate = { tag: number; @@ -18,7 +18,7 @@ type NativeUpdate = { jsUpdateIndex: number; }; -export function createUpdatesContainer(testRunner: TestRunner) { +export function createUpdatesContainer() { const jsUpdates = makeMutable>([]); const nativeSnapshots = makeMutable>([]); @@ -151,7 +151,7 @@ export function createUpdatesContainer(testRunner: TestRunner) { const nativeSnapshotsCount = nativeSnapshots.value.length; const jsUpdatesCount = jsUpdates.value.length; if (jsUpdatesCount === nativeSnapshotsCount) { - await testRunner.runOnUIBlocking(() => { + await new SyncUIRunner().runOnUIBlocking(() => { 'worklet'; const lastSnapshot = nativeSnapshots.value[nativeSnapshotsCount - 1]; if (lastSnapshot) {