Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions ios/edge/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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')
Expand All @@ -34,6 +40,9 @@ const MainApp: React.FC = () => {
<SafeAreaProvider>
<ThemeProvider>
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
{testAnimationsMode == null ? null : (
<ReducedMotionConfig mode={ReduceMotion.Always} />
)}
<ErrorBoundary
beforeCapture={handleBeforeCapture}
fallback={<CrashScene />}
Expand Down
15 changes: 15 additions & 0 deletions src/util/testAnimations.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading