diff --git a/CHANGELOG.md b/CHANGELOG.md index 597e7c137fb..54a5ff01249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased (develop) +- added: iOS debug-build `EdgeTestAnimations` switch to turn off or speed up animations and freeze spinners in UI tests - added: `YOLO_OTP_KEY` env setting, which lets auto-login reach a 2FA-protected account on a device that has no login stash for it yet. - added: Slow-sync explainer card on Bitcoin-family wallets with a long transaction history. - added: Logbox disable option to env.json diff --git a/ios/edge/SceneDelegate.swift b/ios/edge/SceneDelegate.swift index d3d4b45681e..8406f66b0b7 100644 --- a/ios/edge/SceneDelegate.swift +++ b/ios/edge/SceneDelegate.swift @@ -37,6 +37,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let window = UIWindow(windowScene: windowScene) self.window = window appDelegate.window = window + Self.applyTestAnimations(to: window) // A reconnecting scene already has React Native running, and starting it // again would mount a second copy of the app, so reuse the root view @@ -149,6 +150,27 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } + /** + * Test-mode animation switch for automated UI runs, read from the + * `EdgeTestAnimations` user default (the `-EdgeTestAnimations off|fast` + * launch argument). `off` makes UIKit animations complete immediately and + * `fast` runs the window's Core Animation clock 100x. Either value freezes + * activity spinners. Debug builds only. Edge addition. + */ + private static func applyTestAnimations(to window: UIWindow) { + #if DEBUG + switch UserDefaults.standard.string(forKey: "EdgeTestAnimations") { + case "off": + UIView.setAnimationsEnabled(false) + case "fast": + window.layer.speed = 100 + default: + return + } + UIActivityIndicatorView.freezeForTests() + #endif + } + /** * Rebuilds the launch options React Native expects from the scene's * connection options. @@ -181,3 +203,34 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return [:] } } + +#if DEBUG +/** + * Test-mode spinners stay visible but stop turning, so a loading screen + * settles for screenshot comparisons. Edge addition. + */ +extension UIActivityIndicatorView { + private static let swapStartAnimating: Void = { + guard + let original = class_getInstanceMethod( + UIActivityIndicatorView.self, #selector(startAnimating)), + let frozen = class_getInstanceMethod( + UIActivityIndicatorView.self, #selector(frozenStartAnimating)) + else { + return + } + method_exchangeImplementations(original, frozen) + }() + + /** Swaps `startAnimating` once, however many scenes connect. */ + static func freezeForTests() { + _ = swapStartAnimating + } + + /** After the swap, the inner call runs UIKit's own `startAnimating`. */ + @objc private func frozenStartAnimating() { + frozenStartAnimating() + layer.speed = 0 + } +} +#endif diff --git a/src/components/App.tsx b/src/components/App.tsx index 5f73973ea2d..3e7f5e0ca11 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -5,9 +5,11 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import * as React from 'react' import { StyleSheet } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' +import { ReducedMotionConfig, ReduceMotion } from 'react-native-reanimated' import { SafeAreaProvider } from 'react-native-safe-area-context' import { useHandler } from '../hooks/useHandler' +import { getTestAnimationsMode } from '../util/testAnimations' import { CrashScene } from './scenes/CrashScene' import { EdgeCoreManager } from './services/EdgeCoreManager' import { QuickActionsManager } from './services/QuickActionsManager' @@ -23,6 +25,10 @@ const queryClient = new QueryClient({ } }) +// Automated UI runs finish Reanimated loops (shimmers, chart pulses) after +// one cycle so the screen can settle between steps: +const testAnimationsMode = getTestAnimationsMode() + const MainApp: React.FC = () => { const handleBeforeCapture = useHandler((scope: Scope) => { scope.setLevel('fatal') @@ -34,6 +40,9 @@ const MainApp: React.FC = () => { + {testAnimationsMode == null ? null : ( + + )} } diff --git a/src/util/testAnimations.ts b/src/util/testAnimations.ts new file mode 100644 index 00000000000..65c88c35212 --- /dev/null +++ b/src/util/testAnimations.ts @@ -0,0 +1,15 @@ +import { Platform, Settings } from 'react-native' + +export type TestAnimationsMode = 'off' | 'fast' + +/** + * Test-mode animation switch for automated UI runs, read from the + * `EdgeTestAnimations` user default (the `-EdgeTestAnimations off|fast` + * launch argument, or a value the test harness wrote to the simulator). + * Debug builds on iOS only; always null elsewhere. + */ +export const getTestAnimationsMode = (): TestAnimationsMode | null => { + if (!__DEV__ || Platform.OS !== 'ios') return null + const mode: unknown = Settings.get('EdgeTestAnimations') + return mode === 'off' || mode === 'fast' ? mode : null +}