Skip to main content

day_core/
testing.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! Opt-in presentation policy for functional walkthroughs: fast mode is reduced motion forced
5//! on (docs/accessibility.md), nothing more. This is not a virtual clock: timers, network
6//! requests, physics and media retain their real timing.
7
8static FORCED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9
10/// Whether the launch forced reduced motion: `DAY_TEST_FAST=1` (`day launch --fast`) or
11/// `DAY_REDUCE_MOTION=1`, the override that lets a script or a screenshot run the reduced
12/// variant without the system setting. Neither is read after launch, and the user's own
13/// setting is OR-ed in by [`crate::reduce_motion_now`].
14pub(crate) fn forced_reduce_motion() -> bool {
15    *FORCED.get_or_init(|| {
16        let on = |key: &str| std::env::var(key).as_deref() == Ok("1");
17        on("DAY_TEST_FAST") || on("DAY_REDUCE_MOTION")
18    })
19}
20
21/// Skip decorative motion, applying its destination state immediately: the reduced-motion
22/// gate ([`crate::reduce_motion_now`]), which fast mode forces on. Kept under this name for
23/// the backends that ask it before animating a native transition.
24pub fn fast_animations() -> bool {
25    crate::reduce_motion_now()
26}
27
28/// Browser hosts have no process environment. Initialize once before constructing the UI.
29#[cfg(target_arch = "wasm32")]
30pub fn init_fast_animations(enabled: bool) {
31    let _ = FORCED.set(enabled);
32}
33
34#[cfg(test)]
35mod tests {
36    use crate::frame::{FrameClock, install_frame_requester};
37    use crate::tween::{Timing, Tweened, animate};
38    use day_spec::{AnimSpec, FrameStamp};
39    use std::{cell::RefCell, collections::VecDeque, rc::Rc};
40
41    #[test]
42    fn fast_motion_preserves_endpoints_and_continuous_clocks() {
43        // Exercise both startup policies in isolated processes, without racing other
44        // tests over a process-global environment variable or the cached policy.
45        if std::env::var_os("DAY_MOTION_TEST_CHILD").is_none() {
46            for fast in ["0", "1"] {
47                assert!(
48                    std::process::Command::new(std::env::current_exe().unwrap())
49                        .args([
50                            "--exact",
51                            "testing::tests::fast_motion_preserves_endpoints_and_continuous_clocks"
52                        ])
53                        .env("DAY_MOTION_TEST_CHILD", "1")
54                        .env("DAY_TEST_FAST", fast)
55                        .status()
56                        .unwrap()
57                        .success()
58                );
59            }
60            return;
61        }
62        let fast = super::fast_animations();
63        let pending = Rc::new(RefCell::new(VecDeque::new()));
64        let q = pending.clone();
65        install_frame_requester(move |_, cb| {
66            q.borrow_mut().push_back(cb);
67            Box::new(|| {})
68        });
69        let clock = FrameClock::for_root(crate::RNode::default());
70        let tween = Tweened::on(clock, 0.0);
71        tween.animate_to(42.0, AnimSpec::linear(1000));
72        assert_eq!(tween.get_untracked(), if fast { 42.0 } else { 0.0 });
73        let samples = Rc::new(RefCell::new(Vec::new()));
74        let s = samples.clone();
75        let finite = animate(
76            clock,
77            Timing::new(AnimSpec {
78                repeat: 1,
79                autoreverse: true,
80                delay_ms: 500,
81                ..AnimSpec::linear(1000)
82            }),
83            move |sample| s.borrow_mut().push(sample),
84        );
85        let continuous = clock.subscribe(|_| std::ops::ControlFlow::Continue(()));
86        let forever = animate(
87            clock,
88            Timing::new(AnimSpec {
89                repeat: u32::MAX,
90                ..AnimSpec::linear(1000)
91            }),
92            |sample| assert!(!sample.done),
93        );
94        let instant = Rc::new(RefCell::new(None));
95        let i = instant.clone();
96        let _instant = animate(
97            clock,
98            Timing::new(AnimSpec {
99                repeat: 1,
100                autoreverse: true,
101                ..AnimSpec::linear(0)
102            }),
103            move |sample| *i.borrow_mut() = Some(sample),
104        );
105        let cb = pending.borrow_mut().pop_front().unwrap();
106        cb(FrameStamp::new(1.0));
107        assert_eq!(samples.borrow()[0].done, fast);
108        assert_eq!(samples.borrow()[0].progress, 0.0); // autoreverse ends at its start
109        assert_eq!(finite.is_active(), !fast);
110        assert!(continuous.is_active() && forever.is_active());
111        let instant = instant.borrow().unwrap();
112        assert!(instant.done);
113        assert_eq!(instant.progress, 1.0); // zero-duration timing always applies its target
114    }
115}