Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/integration/step.rs
1use super::{action_log, overlay, TestSetupUtils};
2use crate::keymap::PerPlatformKeystroke;
3use crate::platform::OperatingSystem;
4use crate::{
5 event::{Event, KeyEventDetails},
6 keymap::Keystroke,
7 platform::Window,
8 r#async::Timer,
9 App, WindowId,
10};
11use instant::Instant;
12use std::{
13 any::Any,
14 backtrace::Backtrace,
15 collections::{HashMap, VecDeque},
16 sync::atomic::{AtomicBool, Ordering},
17 time::Duration,
18};
19 
20const MAX_WAKEUPS_PER_SECOND: u64 = 60;
21const THROTTLE_PERIOD: Duration = Duration::from_micros(1000 * 1000 / MAX_WAKEUPS_PER_SECOND);
22 
23/// Used for data that is used from step to step.
24#[derive(Default)]
25pub struct StepDataMap {
26 inner: HashMap<String, Box<dyn Any>>,
27}
28 
29impl StepDataMap {
30 pub fn get<K, V>(&self, key: K) -> Option<&V>
31 where
32 K: AsRef<str>,
33 V: 'static,
34 {
35 let boxed = self.inner.get(key.as_ref())?;
36 boxed.as_ref().downcast_ref::<V>()
37 }
38 
39 pub fn get_mut<K, V>(&mut self, key: K) -> Option<&mut V>
40 where
41 K: AsRef<str>,
42 V: 'static,
43 {
44 let boxed = self.inner.get_mut(key.as_ref())?;
45 boxed.as_mut().downcast_mut::<V>()
46 }
47 
48 pub fn insert<K, V>(&mut self, key: K, value: V)
49 where
50 K: Into<String>,
51 V: Any + 'static,
52 {
53 self.insert_step_data(StepData::new(key, value));
54 }
55 
56 pub fn remove<K, V>(&mut self, key: K) -> Option<V>
57 where
58 K: AsRef<str>,
59 V: 'static,
60 {
61 let boxed = self.inner.remove(key.as_ref())?;
62 boxed.downcast::<V>().ok().map(|b| *b)
63 }
64 
65 fn insert_step_data(&mut self, step_data: StepData) {
66 self.inner.insert(step_data.key, step_data.data);
67 }
68}
69 
70fn record_overlay_kind(kind: overlay::OverlayKind, step_data_map: &mut StepDataMap) {
71 let is_recording = super::video_recorder::get_recorder(step_data_map)
72 .is_some_and(super::video_recorder::VideoRecorder::is_recording);
73 if !is_recording {
74 return;
75 }
76 if let Some(ol) = overlay::get_overlay_log_mut(step_data_map) {
77 ol.record(kind);
78 }
79}
80 
81/// Data to pass from one step to the next.
82pub struct StepData {
83 /// The name (key) of the data.
84 pub key: String,
85 
86 /// The data itself. Can be any type.
87 pub data: Box<dyn Any>,
88}
89 
90impl StepData {
91 pub fn new<K, V>(key: K, data: V) -> Self
92 where
93 K: Into<String>,
94 V: Any + 'static,
95 {
96 let data: Box<dyn Any> = Box::new(data);
97 Self {
98 key: key.into(),
99 data,
100 }
101 }
102}
103 
104pub type PersistedDataMap = HashMap<String, String>;
105 
106/// The result of an assertion. Use this rather than a normal assertion
107/// when the thing you are testing may take up to a timeout to be true.
108#[must_use = "AssertionOutcome must be returned to the test runner to allow retrying"]
109pub enum AssertionOutcome {
110 /// The step succeeded.
111 Success,
112 
113 // The test successfully completed, and we want to return some data to the
114 // next step or persist it.
115 SuccessWithData(StepData),
116 
117 /// The step failed. Stores a backtrace from where the failure happened.
118 Failure {
119 message: String,
120 backtrace: Backtrace,
121 failed_assertion_name: Option<String>,
122 },
123 
124 /// The step failed, and we should not wait until the timeout.
125 /// Use this if you need to do things in the app on test failure (like export information) -
126 /// the test will fail, but the app remains running for any export steps.
127 /// If you don't need to export data from the app on failure, use a normal assertion instead.
128 ImmediateFailure {
129 message: String,
130 backtrace: Backtrace,
131 failed_assertion_name: Option<String>,
132 },
133 
134 /// Return this when there is a timing condition that prevents us from
135 /// running the rest of the test - we don't treat this as a failure
136 /// but instead skip the rest of the steps and log the flake.
137 PreconditionFailed(String),
138 
139 /// The test was canceled by user (e.g. Ctrl+C)
140 Canceled,
141}
142 
143impl AssertionOutcome {
144 /// Creates a failure outcome with a stacktrace.
145 pub fn failure(message: String) -> Self {
146 AssertionOutcome::Failure {
147 message,
148 backtrace: Backtrace::capture(),
149 failed_assertion_name: None,
150 }
151 }
152 
153 pub fn immediate_failure(message: String) -> Self {
154 AssertionOutcome::ImmediateFailure {
155 message,
156 backtrace: Backtrace::capture(),
157 failed_assertion_name: None,
158 }
159 }
160 
161 pub fn as_failure_message(&self) -> Option<&str> {
162 match self {
163 AssertionOutcome::Failure { message, .. }
164 | AssertionOutcome::ImmediateFailure { message, .. } => Some(message.as_str()),
165 AssertionOutcome::Success
166 | AssertionOutcome::SuccessWithData(_)
167 | AssertionOutcome::PreconditionFailed(_)
168 | AssertionOutcome::Canceled => None,
169 }
170 }
171}
172 
173/// An assertion callback checks the state of the app and last presenter
174/// (current element tree and last scene) for the given window_id.
175/// It should be idempotent because it can be called multiple times until
176/// the timeout is reached.
177pub type AssertionCallback = Box<dyn FnMut(&mut App, WindowId) -> AssertionOutcome>;
178 
179/// An assertion callback checks the state of the app and last presenter
180/// (current element tree and last scene) for the given window_id.
181/// It should be idempotent because it can be called multiple times until
182/// the timeout is reached. This variant also passes in a map of data from prior steps.
183pub type AssertionWithDataCallback =
184 Box<dyn FnMut(&mut App, WindowId, &mut StepDataMap) -> AssertionOutcome>;
185 
186enum CallbackType {
187 Assertion(AssertionCallback),
188 AssertionWithData(AssertionWithDataCallback),
189}
190 
191struct Assertion {
192 name: Option<String>,
193 callback: CallbackType,
194}
195 
196pub type SavedPositionFn = Box<dyn Fn(&mut App, WindowId) -> String>;
197pub type EventFn = Box<dyn Fn(&mut App, WindowId) -> Event>;
198 
199/// A TestStep can include integration events that are handled
200/// before asserting some state of the app.
201pub enum IntegrationTestEvent {
202 /// A plain-old-event to be processed.
203 /// Note that the event will be created at build time.
204 WithEvent(Event),
205 /// Given a callback that produces an Event, this allows you to
206 /// create an event at runtime using the state of the app at the time
207 /// of the step.
208 WithEventFn(EventFn),
209 /// Similar to WithEvent, but used to dispatch an event at the saved position.
210 WithSavedPosition(String, MouseEvent),
211 /// Similar to WithEventFn, but used to dispatch an event at the saved position.
212 WithSavedPositionFn(SavedPositionFn, MouseEvent),
213}
214 
215pub enum MouseEvent {
216 ClickOnce,
217 RightClickOnce,
218 Hover,
219}
220 
221pub type IntegrationTestActionFn = Box<dyn Fn(&mut App, WindowId, &mut StepDataMap)>;
222pub type IntegrationTestSetupFn = Box<dyn Fn(&mut TestSetupUtils)>;
223 
224/// A test step consists of
225/// 1) A queue of setup functions, that might e.g. modify the filesystem.
226/// 2) A queue of events to dispatch against the active window
227/// 3) Queue of actions called *before* any assertions are checked. Since they receive &mut App, it
228/// is possible that the action modifies the app state (ie. by dispatching a global action).
229/// 4) An assertion callback that verifies the state of the app and last frame
230/// 5) An optional timeout for the assertion callback - the app will continue
231/// to test the assertion until the timeout is reached or the assertion succeeds
232pub struct TestStep {
233 name: String,
234 setup_functions: VecDeque<IntegrationTestSetupFn>,
235 events: VecDeque<IntegrationTestEvent>,
236 actions: VecDeque<IntegrationTestActionFn>,
237 assertions: Vec<Assertion>,
238 timeout: Duration,
239 post_step_pause: Option<Duration>,
240 
241 /// This causes the test to wait after a failure rather than immediately
242 /// panicking - can be useful in combination with running with a real delegate
243 /// to observe the state that the app is in when failure happens.
244 pause_on_failure: Option<Duration>,
245 
246 /// An optional final assertion that is run when the timeout has hit.
247 /// If omitted the test fails.
248 on_failure_handler: Option<Assertion>,
249 
250 /// The name of the group this step belongs to, used for failure reporting.
251 pub(super) step_group_name: Option<String>,
252 
253 /// Number of times to retry this step if it fails (defaults to 0, meaning no retries)
254 retries: u32,
255}
256 
257const DEFAULT_STEP_TIMEOUT: Duration = Duration::from_secs(10);
258const DEFAULT_POST_STEP_PAUSE: Duration = Duration::from_secs(3);
259const DEFAULT_PAUSE_ON_FAILURE: Duration = Duration::from_secs(1000);
260 
261impl TestStep {
262 pub fn new(name: &str) -> Self {
263 // Enable these two pauses for better local debugging
264 let pause_on_failure = if std::env::var("WARPUI_PAUSE_INTEGRATION_TEST_ON_FAILURE").is_ok()
265 {
266 Some(DEFAULT_PAUSE_ON_FAILURE)
267 } else {
268 None
269 };
270 
271 let post_step_pause =
272 if std::env::var("WARPUI_PAUSE_INTEGRATION_TEST_AT_EVERY_STEP").is_ok() {
273 Some(DEFAULT_POST_STEP_PAUSE)
274 } else {
275 None
276 };
277 
278 Self {
279 name: name.to_owned(),
280 setup_functions: Default::default(),
281 events: Default::default(),
282 actions: Default::default(),
283 assertions: Default::default(),
284 timeout: DEFAULT_STEP_TIMEOUT,
285 post_step_pause,
286 pause_on_failure,
287 on_failure_handler: None,
288 step_group_name: None,
289 retries: 0,
290 }
291 }
292 
293 pub fn name(&self) -> &str {
294 &self.name
295 }
296 
297 pub fn set_step_group_name(mut self, name: &str) -> Self {
298 self.step_group_name = Some(name.to_string());
299 self
300 }
301 
302 pub fn add_named_assertion<N, F>(mut self, name: N, callback: F) -> Self
303 where
304 N: Into<String>,
305 F: FnMut(&mut App, WindowId) -> AssertionOutcome + 'static,
306 {
307 self.assertions.push(Assertion {
308 name: Some(name.into()),
309 callback: CallbackType::Assertion(Box::new(callback)),
310 });
311 self
312 }
313 
314 /// Adds a named assertion with a callback that expects a map of data from prior steps.
315 pub fn add_named_assertion_with_data_from_prior_step<N, F>(
316 mut self,
317 name: N,
318 callback: F,
319 ) -> Self
320 where
321 N: Into<String>,
322 F: FnMut(&mut App, WindowId, &mut StepDataMap) -> AssertionOutcome + 'static,
323 {
324 self.assertions.push(Assertion {
325 name: Some(name.into()),
326 callback: CallbackType::AssertionWithData(Box::new(callback)),
327 });
328 self
329 }
330 
331 pub fn add_assertion<F>(mut self, callback: F) -> Self
332 where
333 F: FnMut(&mut App, WindowId) -> AssertionOutcome + 'static,
334 {
335 self.assertions.push(Assertion {
336 name: None,
337 callback: CallbackType::Assertion(Box::new(callback)),
338 });
339 self
340 }
341 
342 pub fn set_on_failure_handler<N, F>(mut self, name: N, callback: F) -> Self
343 where
344 N: Into<String>,
345 F: FnMut(&mut App, WindowId) -> AssertionOutcome + 'static,
346 {
347 self.on_failure_handler = Some(Assertion {
348 name: Some(name.into()),
349 callback: CallbackType::Assertion(Box::new(callback)),
350 });
351 self
352 }
353 
354 pub fn set_timeout(mut self, timeout: Duration) -> Self {
355 self.timeout = timeout;
356 self
357 }
358 
359 pub fn set_post_step_pause(mut self, pause: Duration) -> Self {
360 self.post_step_pause = Some(pause);
361 self
362 }
363 
364 pub fn set_pause_on_failure(mut self, pause: Duration) -> Self {
365 self.pause_on_failure = Some(pause);
366 self
367 }
368 
369 pub fn set_retries(mut self, retries: u32) -> Self {
370 self.retries = retries;
371 self
372 }
373 
374 pub(super) fn retries(&self) -> u32 {
375 self.retries
376 }
377 
378 pub fn with_input_string(self, input: &str, extra_keystrokes: Option<&[&str]>) -> Self {
379 let v: Vec<String> = input
380 .chars()
381 .map(|x| {
382 if x.is_ascii_uppercase() {
383 format!("shift-{x}")
384 } else {
385 x.to_string()
386 }
387 })
388 .collect();
389 let v2: Vec<&str> = v.iter().map(|s| &**s).collect();
390 
391 self.with_keystrokes(v2.as_slice())
392 .with_keystrokes(extra_keystrokes.unwrap_or(&[]))
393 }
394 
395 pub fn with_per_platform_keystroke(self, keystrokes: PerPlatformKeystroke) -> Self {
396 let keystroke = if OperatingSystem::get().is_mac() {
397 keystrokes.mac
398 } else {
399 keystrokes.linux_and_windows
400 };
401 
402 self.with_keystrokes(&[keystroke])
403 }
404 
405 pub fn with_keystrokes(mut self, keystrokes: &[impl AsRef<str>]) -> Self {
406 for keystroke in keystrokes
407 .iter()
408 .map(|keystroke| Keystroke::parse(keystroke).expect("failed to parse keystroke"))
409 {
410 // Match macOS by mapping control/special keys to their ASCII characters.
411 // This covers common characters, but is non-exhaustive (it's mainly
412 // missing arrow and function keys).
413 let chars = match (&keystroke, keystroke.key.as_str()) {
414 (Keystroke { ctrl: true, .. }, "c") => "\u{3}".to_string(),
415 (_, "enter") => "\r".to_string(),
416 (Keystroke { shift: true, .. }, "tab") => "\u{19}".to_string(),
417 (_, "tab") => "\t".to_string(),
418 (_, "backspace") => "\u{7f}".to_string(),
419 (_, "numpadenter") => "\u{3}".to_string(),
420 (_, "escape") => "\u{1b}".to_string(),
421 (keystroke, _) => keystroke.key.clone(),
422 };
423 
424 self.events
425 .push_back(IntegrationTestEvent::WithEvent(Event::KeyDown {
426 chars,
427 keystroke,
428 details: KeyEventDetails::default(),
429 is_composing: false,
430 }));
431 }
432 self
433 }
434 
435 pub fn with_keystrokes_in_composing(mut self, keystrokes: &[&str]) -> Self {
436 for keystroke in keystrokes
437 .iter()
438 .map(|keystroke| Keystroke::parse(keystroke).expect("failed to parse keystroke"))
439 {
440 let chars = if keystroke.ctrl && keystroke.key.as_str() == "c" {
441 "\u{3}".to_string()
442 } else {
443 keystroke.key.clone()
444 };
445 self.events
446 .push_back(IntegrationTestEvent::WithEvent(Event::KeyDown {
447 chars,
448 keystroke,
449 details: KeyEventDetails::default(),
450 is_composing: true,
451 }));
452 }
453 self
454 }
455 
456 pub fn with_typed_characters(mut self, characters: &[&str]) -> Self {
457 for character in characters.iter() {
458 self.events
459 .push_back(IntegrationTestEvent::WithEvent(Event::TypedCharacters {
460 chars: String::from(*character),
461 }));
462 }
463 self
464 }
465 
466 pub fn with_event(mut self, event: Event) -> Self {
467 self.events
468 .push_back(IntegrationTestEvent::WithEvent(event));
469 self
470 }
471 
472 pub fn with_event_fn<F>(mut self, event_fn: F) -> Self
473 where
474 F: Fn(&mut App, WindowId) -> Event + 'static,
475 {
476 self.events
477 .push_back(IntegrationTestEvent::WithEventFn(Box::new(event_fn)));
478 self
479 }
480 
481 pub fn with_click_on_saved_position_fn<F>(mut self, position_fn: F) -> Self
482 where
483 F: Fn(&mut App, WindowId) -> String + 'static,
484 {
485 self.events
486 .push_back(IntegrationTestEvent::WithSavedPositionFn(
487 Box::new(position_fn),
488 MouseEvent::ClickOnce,
489 ));
490 self
491 }
492 
493 pub fn with_click_on_saved_position<S: Into<String>>(mut self, position_id: S) -> Self {
494 self.events
495 .push_back(IntegrationTestEvent::WithSavedPosition(
496 position_id.into(),
497 MouseEvent::ClickOnce,
498 ));
499 self
500 }
501 
502 pub fn with_right_click_on_saved_position_fn<F>(mut self, position_fn: F) -> Self
503 where
504 F: Fn(&mut App, WindowId) -> String + 'static,
505 {
506 self.events
507 .push_back(IntegrationTestEvent::WithSavedPositionFn(
508 Box::new(position_fn),
509 MouseEvent::RightClickOnce,
510 ));
511 self
512 }
513 
514 pub fn with_right_click_on_saved_position<S: Into<String>>(mut self, position_id: S) -> Self {
515 self.events
516 .push_back(IntegrationTestEvent::WithSavedPosition(
517 position_id.into(),
518 MouseEvent::RightClickOnce,
519 ));
520 self
521 }
522 
523 pub fn with_hover_on_saved_position_fn<F>(mut self, position_fn: F) -> Self
524 where
525 F: Fn(&mut App, WindowId) -> String + 'static,
526 {
527 self.events
528 .push_back(IntegrationTestEvent::WithSavedPositionFn(
529 Box::new(position_fn),
530 MouseEvent::Hover,
531 ));
532 self
533 }
534 
535 pub fn with_hover_over_saved_position<S: Into<String>>(mut self, position_id: S) -> Self {
536 self.events
537 .push_back(IntegrationTestEvent::WithSavedPosition(
538 position_id.into(),
539 MouseEvent::Hover,
540 ));
541 self
542 }
543 
544 pub fn with_action<F>(mut self, callback: F) -> Self
545 where
546 F: Fn(&mut App, WindowId, &mut StepDataMap) + 'static,
547 {
548 self.actions.push_back(Box::new(callback));
549 self
550 }
551 
552 /// Adds an action that captures a screenshot and saves it to the artifacts
553 /// directory with the given filename (e.g. `"after_bootstrap.png"`).
554 pub fn with_take_screenshot(self, filename: impl Into<String>) -> Self {
555 let filename = filename.into();
556 self.with_action(move |_app, _window_id, step_data_map| {
557 step_data_map.insert(super::video_recorder::SCREENSHOT_PATH_KEY, filename.clone());
558 })
559 }
560 
561 /// Adds an action that starts video recording.
562 pub fn with_start_recording(self) -> Self {
563 self.with_action(|_app, _window_id, step_data_map| {
564 if let Some(recorder) = super::video_recorder::get_recorder_mut(step_data_map) {
565 recorder.start_recording();
566 log::info!("VideoRecorder: recording started");
567 #[cfg(feature = "integration_tests")]
568 let recording_start = recorder.recording_start();
569 #[cfg(not(feature = "integration_tests"))]
570 let recording_start: Option<instant::Instant> = None;
571 if let Some(log) = super::action_log::get_action_log_mut(step_data_map) {
572 if let Some(start) = recording_start {
573 log.set_recording_start(start);
574 }
575 log.record("Recording started");
576 }
577 }
578 })
579 }
580 
581 /// Adds an action that stops video recording.
582 pub fn with_stop_recording(self) -> Self {
583 self.with_action(|_app, _window_id, step_data_map| {
584 if let Some(recorder) = super::video_recorder::get_recorder_mut(step_data_map) {
585 recorder.stop_recording();
586 log::info!("VideoRecorder: recording stopped");
587 if let Some(log) = super::action_log::get_action_log_mut(step_data_map) {
588 log.record("Recording stopped");
589 }
590 }
591 })
592 }
593 
594 /// Add a setup function which runs before any events, actions, or
595 /// assertions in the test step.
596 ///
597 /// This is a good place for any filesystem operations relevant for a
598 /// test step.
599 pub fn with_setup<F>(mut self, callback: F) -> Self
600 where
601 F: Fn(&mut TestSetupUtils) + 'static,
602 {
603 self.setup_functions.push_back(Box::new(callback));
604 self
605 }
606}
607 
608pub(super) async fn run_step(
609 step: &mut TestStep,
610 app: &mut App,
611 window_id: WindowId,
612 window: &dyn Window,
613 step_data_map: &mut StepDataMap,
614 test_setup_utils: &mut TestSetupUtils,
615 sigint_received: &AtomicBool,
616) -> AssertionOutcome {
617 let deadline = Instant::now() + step.timeout;
618 
619 let original_frame_count = {
620 let presenter_rc = app.presenter(window_id).expect("Invalid window id");
621 let presenter = presenter_rc.borrow();
622 presenter.frame_count()
623 };
624 
625 log::info!(
626 "Running test step '{}' at frame {} with {} events",
627 step.name,
628 original_frame_count,
629 step.events.len()
630 );
631 
632 for setup in step.setup_functions.iter() {
633 if sigint_received.load(Ordering::Relaxed) {
634 return AssertionOutcome::Canceled;
635 }
636 setup(test_setup_utils);
637 }
638 
639 for e in &step.events {
640 if sigint_received.load(Ordering::Relaxed) {
641 return AssertionOutcome::Canceled;
642 }
643 
644 // TODO would be cool to move it under IntegrationTestEvent
645 match e {
646 IntegrationTestEvent::WithEvent(..) | IntegrationTestEvent::WithEventFn(..) => {
647 let event = if let IntegrationTestEvent::WithEvent(e) = e {
648 e.clone()
649 } else if let IntegrationTestEvent::WithEventFn(f) = e {
650 f(app, window_id)
651 } else {
652 unreachable!("only handling WithEvent variants here")
653 };
654 
655 log::info!("Dispatching event {event:?}");
656 if let Some(log) = action_log::get_action_log_mut(step_data_map) {
657 log.record(format!("Event: {}", action_log::event_description(&event)));
658 }
659 record_overlay_event_for_event(&event, step_data_map);
660 let dispatch_result =
661 app.update(|ctx| (window.callbacks().event_callback)(event.clone(), ctx));
662 if !dispatch_result.handled {
663 if let Event::KeyDown {
664 chars,
665 is_composing,
666 ..
667 } = event
668 {
669 if !is_composing {
670 // The input system expects a TypedCharacters event to follow keydown
671 // in order to update the editor's input unless is_composing is set
672 app.update(|ctx| {
673 (window.callbacks().event_callback)(
674 Event::TypedCharacters {
675 chars: chars.clone(),
676 },
677 ctx,
678 )
679 });
680 }
681 }
682 }
683 }
684 IntegrationTestEvent::WithSavedPosition(_, mouse_event)
685 | IntegrationTestEvent::WithSavedPositionFn(_, mouse_event) => {
686 let presenter = app.presenter(window_id).expect("Invalid window id");
687 let position_id = match e {
688 IntegrationTestEvent::WithSavedPosition(position_id, _) => {
689 position_id.to_string()
690 }
691 IntegrationTestEvent::WithSavedPositionFn(position_id_fn, _) => {
692 position_id_fn(app, window_id)
693 }
694 IntegrationTestEvent::WithEvent(..) | IntegrationTestEvent::WithEventFn(..) => {
695 unreachable!("already handled WithEvent variants")
696 }
697 };
698 let bounds = {
699 let presenter_ref = presenter.borrow();
700 presenter_ref.position_cache().get_position(&position_id)
701 };
702 
703 // Note we are not using unwrap_or_else here because async closures
704 // are experimental and we await in the failure case.
705 if bounds.is_none() {
706 if let Some(pause) = step.pause_on_failure {
707 log::error!(
708 "Test step '{}' failed to find saved position {}, pausing...",
709 step.name,
710 position_id
711 );
712 Timer::at(Instant::now() + pause).await;
713 }
714 return AssertionOutcome::failure(format!("No position for {position_id}"));
715 }
716 let bounds = bounds.unwrap();
717 
718 let center = bounds.center();
719 match mouse_event {
720 MouseEvent::ClickOnce => {
721 record_overlay_kind(
722 overlay::OverlayKind::MouseDown {
723 x: center.x(),
724 y: center.y(),
725 },
726 step_data_map,
727 );
728 let mouse_down = Event::LeftMouseDown {
729 position: center,
730 modifiers: Default::default(),
731 click_count: 1,
732 is_first_mouse: false,
733 };
734 let mouse_up = Event::LeftMouseUp {
735 position: center,
736 modifiers: Default::default(),
737 };
738 for event in [mouse_down, mouse_up] {
739 app.update(|ctx| (window.callbacks().event_callback)(event, ctx));
740 }
741 record_overlay_kind(
742 overlay::OverlayKind::MouseUp {
743 x: center.x(),
744 y: center.y(),
745 },
746 step_data_map,
747 );
748 }
749 MouseEvent::RightClickOnce => {
750 record_overlay_kind(
751 overlay::OverlayKind::MouseDown {
752 x: center.x(),
753 y: center.y(),
754 },
755 step_data_map,
756 );
757 app.update(|ctx| {
758 (window.callbacks().event_callback)(
759 Event::RightMouseDown {
760 position: center,
761 cmd: false,
762 shift: false,
763 click_count: 1,
764 },
765 ctx,
766 )
767 });
768 record_overlay_kind(
769 overlay::OverlayKind::MouseUp {
770 x: center.x(),
771 y: center.y(),
772 },
773 step_data_map,
774 );
775 }
776 MouseEvent::Hover => {
777 app.update(|ctx| {
778 (window.callbacks().event_callback)(
779 Event::MouseMoved {
780 position: center,
781 cmd: false,
782 shift: false,
783 is_synthetic: false,
784 },
785 ctx,
786 )
787 });
788 }
789 }
790 }
791 }
792 
793 if let Err(err) = maybe_render_frame(app, window_id, deadline).await {
794 return err;
795 }
796 }
797 
798 for action in step.actions.iter() {
799 if sigint_received.load(Ordering::Relaxed) {
800 return AssertionOutcome::Canceled;
801 }
802 
803 action(app, window_id, step_data_map);
804 if let Some(log) = action_log::get_action_log_mut(step_data_map) {
805 log.record("Action executed");
806 }
807 
808 if let Err(err) = maybe_render_frame(app, window_id, deadline).await {
809 return err;
810 }
811 }
812 
813 let mut last_failure = None;
814 let mut last_assertion_name = None;
815 'outer: for assertion in step.assertions.iter_mut() {
816 // We loop through until the assertion is true or the timeout is reached.
817 // If the timeout is reached in a failure state we panic and fail the test.
818 // Regardless of the assertion timeout, always run the assertion loop at least once
819 // for each assertion.
820 let mut idx = 0;
821 while idx == 0 || Instant::now() < deadline {
822 // Check for Ctrl+C before running the assertion
823 if sigint_received.load(Ordering::Relaxed) {
824 log::info!(
825 "Test interrupted by Ctrl+C during assertion '{}'",
826 assertion
827 .name
828 .as_ref()
829 .map_or("unnamed", |name| name.as_str())
830 );
831 return AssertionOutcome::Canceled;
832 }
833 
834 Timer::at(Instant::now() + THROTTLE_PERIOD).await;
835 if idx == 0 {
836 let name = assertion
837 .name
838 .as_ref()
839 .map_or("unnamed", |name| name.as_str());
840 last_assertion_name = Some(name);
841 log::info!("entering assertion loop for '{name}'");
842 if let Some(log) = action_log::get_action_log_mut(step_data_map) {
843 log.record(format!("Assertion started: {name}"));
844 }
845 }
846 idx += 1;
847 let res = match &mut assertion.callback {
848 CallbackType::Assertion(cb) => cb(app, window_id),
849 CallbackType::AssertionWithData(cb) => cb(app, window_id, step_data_map),
850 };
851 match res {
852 AssertionOutcome::Success => {
853 if let Some(log) = action_log::get_action_log_mut(step_data_map) {
854 let name = assertion.name.as_deref().unwrap_or("unnamed");
855 log.record(format!("Assertion passed: {name}"));
856 }
857 continue 'outer;
858 }
859 AssertionOutcome::SuccessWithData(step_data) => {
860 if let Some(log) = action_log::get_action_log_mut(step_data_map) {
861 let name = assertion.name.as_deref().unwrap_or("unnamed");
862 log.record(format!("Assertion passed: {name}"));
863 }
864 step_data_map.insert_step_data(step_data);
865 continue 'outer;
866 }
867 AssertionOutcome::PreconditionFailed(s) => {
868 // Early exit if we've flaked.
869 return AssertionOutcome::PreconditionFailed(s);
870 }
871 AssertionOutcome::Failure {
872 message,
873 backtrace,
874 failed_assertion_name,
875 } => {
876 last_failure = Some(AssertionOutcome::Failure {
877 message,
878 backtrace,
879 failed_assertion_name: failed_assertion_name.or(assertion.name.clone()),
880 });
881 }
882 AssertionOutcome::ImmediateFailure {
883 message,
884 backtrace,
885 failed_assertion_name,
886 } => {
887 return AssertionOutcome::ImmediateFailure {
888 message,
889 backtrace,
890 failed_assertion_name: failed_assertion_name.or(assertion.name.clone()),
891 };
892 }
893 AssertionOutcome::Canceled => {
894 return AssertionOutcome::Canceled;
895 }
896 }
897 }
898 
899 // If we are here, the timer for the current assertion has elapsed without hitting success.
900 // Check if there is a final assertion to run, and if so run it.
901 if let Some(mut final_assertion) = step.on_failure_handler.take() {
902 // Log the timed-out assertion's failure message before running the final assertion
903 if let Some(AssertionOutcome::Failure { message, .. }) = &last_failure {
904 log::error!(
905 "Assertion '{}' timed out with message: {}",
906 last_assertion_name.unwrap_or("unknown"),
907 message
908 );
909 }
910 let res = match &mut final_assertion.callback {
911 CallbackType::Assertion(cb) => cb(app, window_id),
912 CallbackType::AssertionWithData(cb) => cb(app, window_id, step_data_map),
913 };
914 match res {
915 AssertionOutcome::Success => {
916 continue 'outer;
917 }
918 AssertionOutcome::SuccessWithData(step_data) => {
919 step_data_map.insert_step_data(step_data);
920 continue 'outer;
921 }
922 AssertionOutcome::PreconditionFailed(s) => {
923 // Early exit if we've flaked.
924 return AssertionOutcome::PreconditionFailed(s);
925 }
926 AssertionOutcome::Failure {
927 message,
928 backtrace,
929 failed_assertion_name,
930 } => {
931 last_failure = Some(AssertionOutcome::Failure {
932 message,
933 backtrace,
934 failed_assertion_name: failed_assertion_name
935 .or(final_assertion.name.clone()),
936 });
937 }
938 AssertionOutcome::ImmediateFailure {
939 message,
940 backtrace,
941 failed_assertion_name,
942 } => {
943 return AssertionOutcome::ImmediateFailure {
944 message,
945 backtrace,
946 failed_assertion_name: failed_assertion_name
947 .or(final_assertion.name.clone()),
948 };
949 }
950 AssertionOutcome::Canceled => {
951 return AssertionOutcome::Canceled;
952 }
953 }
954 }
955 
956 // We only get this far in the case of a test failure.
957 let last_failure = last_failure.expect("last_failure should be set");
958 if let Some(msg) = last_failure.as_failure_message().map(str::to_owned) {
959 if let Some(log) = action_log::get_action_log_mut(step_data_map) {
960 let name = last_assertion_name.unwrap_or("unknown");
961 log.record(format!("Assertion failed: {name}: {msg}"));
962 }
963 }
964 if let Some(pause) = step.pause_on_failure {
965 let AssertionOutcome::Failure { message, .. } = &last_failure else {
966 panic!("last_failure should be a failure assertion");
967 };
968 log::error!(
969 "Test step '{}' failed with error '{}', pausing...",
970 step.name,
971 message
972 );
973 Timer::at(Instant::now() + pause).await;
974 }
975 // Mostly logging to get a timestamp - the test driver will panic right
976 // after this.
977 log::error!(
978 "Test step '{}' failed on '{}'",
979 step.name,
980 last_assertion_name.unwrap_or("unknown")
981 );
982 return last_failure;
983 }
984 if let Some(pause) = step.post_step_pause {
985 Timer::at(Instant::now() + pause).await;
986 }
987 AssertionOutcome::Success
988}
989 
990/// Renders a frame for the window, if necessary.
991///
992/// Returns a `Err(AssertionOutcome)` if an error occurred that should cause
993/// the test to fail.
994async fn maybe_render_frame(
995 app: &mut App,
996 window_id: WindowId,
997 deadline: Instant,
998) -> Result<(), AssertionOutcome> {
999 let Some(initial_frame_count) = frame_count(app, window_id) else {
1000 // If we can't compute the frame count, the window has been closed, in
1001 // which case we should skip rendering the frame and move on.
1002 return Ok(());
1003 };
1004 if app.has_window_invalidations(window_id) {
1005 log::info!("app needs to render a frame");
1006 
1007 // Allow at least one frame to pass if the app needs to redraw the window
1008 let mut rerendered = false;
1009 while Instant::now() < deadline {
1010 Timer::at(Instant::now() + THROTTLE_PERIOD).await;
1011 let Some(next_frame_count) = frame_count(app, window_id) else {
1012 // If we can't compute the frame count, the window has been
1013 // closed, in which case we should skip rendering the frame
1014 // and move on.
1015 return Ok(());
1016 };
1017 if next_frame_count > initial_frame_count {
1018 log::info!("at least one frame has passed, moving on.");
1019 rerendered = true;
1020 break;
1021 }
1022 }
1023 
1024 if !rerendered {
1025 return Err(AssertionOutcome::failure(
1026 "Test step failed because no frames were rendered".to_string(),
1027 ));
1028 }
1029 } else {
1030 log::debug!("not checking for a frame to pass");
1031 }
1032 
1033 Ok(())
1034}
1035 
1036/// Returns the total number of frames rendered in the given window.
1037fn frame_count(app: &mut App, window_id: WindowId) -> Option<usize> {
1038 let presenter_rc = app.presenter(window_id)?;
1039 let presenter = presenter_rc.borrow();
1040 Some(presenter.frame_count())
1041}
1042 
1043fn record_overlay_event_for_event(event: &Event, step_data_map: &mut StepDataMap) {
1044 let kind = match event {
1045 Event::LeftMouseDown { position, .. }
1046 | Event::RightMouseDown { position, .. }
1047 | Event::MiddleMouseDown { position, .. }
1048 | Event::ForwardMouseDown { position, .. }
1049 | Event::BackMouseDown { position, .. } => Some(overlay::OverlayKind::MouseDown {
1050 x: position.x(),
1051 y: position.y(),
1052 }),
1053 Event::LeftMouseDragged { position, .. } => Some(overlay::OverlayKind::MouseMove {
1054 x: position.x(),
1055 y: position.y(),
1056 }),
1057 Event::LeftMouseUp { position, .. } => Some(overlay::OverlayKind::MouseUp {
1058 x: position.x(),
1059 y: position.y(),
1060 }),
1061 Event::KeyDown { keystroke, .. } => {
1062 let text = overlay::keystroke_display_text(keystroke);
1063 Some(overlay::OverlayKind::KeyPress { display_text: text })
1064 }
1065 _ => None,
1066 };
1067 if let Some(kind) = kind {
1068 record_overlay_kind(kind, step_data_map);
1069 }
1070}
1071