diff options
Diffstat (limited to 'toolkit/crashreporter/client/app/src/ui/test.rs')
-rw-r--r-- | toolkit/crashreporter/client/app/src/ui/test.rs | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/app/src/ui/test.rs b/toolkit/crashreporter/client/app/src/ui/test.rs new file mode 100644 index 0000000000..db98c072da --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/test.rs @@ -0,0 +1,270 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! A renderer for use in tests, which doesn't actually render a GUI but allows programmatic +//! interaction. +//! +//! The [`ui!`](super::ui) macro supports labeling any element with a string identifier, which can +//! be used to access the element in this UI. +//! +//! The [`Interact`] hook must be created to interact with the test UI, before the UI is run and on +//! the same thread as the UI. +//! +//! See how this UI is used in [`crate::test`]. + +use super::model::{self, Application, Element}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::{ + atomic::{AtomicBool, AtomicU8, Ordering::Relaxed}, + mpsc, Arc, Condvar, Mutex, +}; + +thread_local! { + static INTERACT: RefCell<Option<Arc<State>>> = Default::default(); +} + +/// A test UI which allows access to the UI elements. +#[derive(Default)] +pub struct UI { + interface: Mutex<Option<UIInterface>>, +} + +impl UI { + pub fn run_loop(&self, app: Application) { + let (tx, rx) = mpsc::channel(); + let interface = UIInterface { work: tx }; + + let elements = id_elements(&app); + INTERACT.with(cc! { (interface) move |r| { + if let Some(state) = &*r.borrow() { + state.set_interface(interface); + } + }}); + *self.interface.lock().unwrap() = Some(interface.clone()); + + // Close the UI when the root windows are closed. + // Use a bitfield rather than a count in case the `close` event is fired multiple times. + assert!(app.windows.len() <= 8); + let mut windows = 0u8; + for i in 0..app.windows.len() { + windows |= 1 << i; + } + let windows = Arc::new(AtomicU8::new(windows)); + for (index, window) in app.windows.iter().enumerate() { + if let Some(c) = &window.element_type.close { + c.subscribe(cc! { (interface, windows) move |&()| { + let old = windows + .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index))) + .unwrap(); + if old == 1u8 << index { + interface.work.send(Command::Finish).unwrap(); + } + }}); + } else { + // No close event, so we must assume a closed state (and assume that _some_ window + // will have a close event registered so we don't drop the interface now). + windows + .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index))) + .unwrap(); + } + } + + while let Ok(f) = rx.recv() { + match f { + Command::Invoke(f) => f(), + Command::Interact(f) => f(&elements), + Command::Finish => break, + } + } + + *self.interface.lock().unwrap() = None; + INTERACT.with(|r| { + if let Some(state) = &*r.borrow() { + state.clear_interface(); + } + }); + } + + pub fn invoke(&self, f: model::InvokeFn) { + let guard = self.interface.lock().unwrap(); + if let Some(interface) = &*guard { + let _ = interface.work.send(Command::Invoke(f)); + } + } +} + +/// Test interaction hook. +#[derive(Clone)] +pub struct Interact { + state: Arc<State>, +} + +impl Interact { + /// Create an interaction hook for the test UI. + /// + /// This should be done before running the UI, and must be done on the same thread that + /// later runs it. + pub fn hook() -> Self { + let v = Interact { + state: Default::default(), + }; + { + let state = v.state.clone(); + INTERACT.with(move |r| *r.borrow_mut() = Some(state)); + } + v + } + + /// Wait for the render thread to be ready for interaction. + pub fn wait_for_ready(&self) { + let mut guard = self.state.interface.lock().unwrap(); + while guard.is_none() && !self.state.cancel.load(Relaxed) { + guard = self.state.waiting_for_interface.wait(guard).unwrap(); + } + } + + /// Cancel an Interact (which causes `wait_for_ready` to always return). + pub fn cancel(&self) { + self.state.cancel.store(true, Relaxed); + self.state.waiting_for_interface.notify_all(); + } + + /// Run the given function on the element with the given type and identity. + /// + /// Panics if either the id is missing or the type is incorrect. + pub fn element<'a, 'b, T: 'b, F, R>(&self, id: &'a str, f: F) -> R + where + &'b T: TryFrom<&'b model::ElementType>, + F: FnOnce(&model::ElementStyle, &T) -> R + Send + 'a, + R: Send + 'a, + { + self.interact(id, move |element: &IdElement| match element { + IdElement::Generic(e) => Some(f(&e.style, (&e.element_type).try_into().ok()?)), + IdElement::Window(_) => None, + }) + .expect("incorrect element type") + } + + /// Run the given function on the window with the given identity. + /// + /// Panics if the id is missing or the type is incorrect. + pub fn window<'a, F, R>(&self, id: &'a str, f: F) -> R + where + F: FnOnce(&model::ElementStyle, &model::Window) -> R + Send + 'a, + R: Send + 'a, + { + self.interact(id, move |element| match element { + IdElement::Window(e) => Some(f(&e.style, &e.element_type)), + IdElement::Generic(_) => None, + }) + .expect("incorrect element type") + } + + fn interact<'a, 'b, F, R>(&self, id: &'a str, f: F) -> R + where + F: FnOnce(&IdElement<'b>) -> R + Send + 'a, + R: Send + 'a, + { + let (send, recv) = std::sync::mpsc::sync_channel(0); + { + let f: Box<dyn FnOnce(&IdElements<'b>) + Send + 'a> = Box::new(move |elements| { + let _ = send.send(elements.get(id).map(f)); + }); + + // # Safety + // The function is run while `'a` is still valid (we wait here for it to complete). + let f: Box<dyn FnOnce(&IdElements) + Send + 'static> = + unsafe { std::mem::transmute(f) }; + + let guard = self.state.interface.lock().unwrap(); + let interface = guard.as_ref().expect("renderer is not running"); + let _ = interface.work.send(Command::Interact(f)); + } + recv.recv().unwrap().expect("failed to get element") + } +} + +#[derive(Clone)] +struct UIInterface { + work: mpsc::Sender<Command>, +} + +enum Command { + Invoke(Box<dyn FnOnce() + Send + 'static>), + Interact(Box<dyn FnOnce(&IdElements) + Send + 'static>), + Finish, +} + +enum IdElement<'a> { + Generic(&'a Element), + Window(&'a model::TypedElement<model::Window>), +} + +type IdElements<'a> = HashMap<String, IdElement<'a>>; + +#[derive(Default)] +struct State { + interface: Mutex<Option<UIInterface>>, + waiting_for_interface: Condvar, + cancel: AtomicBool, +} + +impl State { + /// Set the interface for the interaction client to use. + pub fn set_interface(&self, interface: UIInterface) { + *self.interface.lock().unwrap() = Some(interface); + self.waiting_for_interface.notify_all(); + } + + /// Clear the UI interface. + pub fn clear_interface(&self) { + *self.interface.lock().unwrap() = None; + } +} + +fn id_elements<'a>(app: &'a Application) -> IdElements<'a> { + let mut elements: IdElements<'a> = Default::default(); + + let mut windows_to_visit: Vec<_> = app.windows.iter().collect(); + + let mut to_visit: Vec<&'a Element> = Vec::new(); + while let Some(window) = windows_to_visit.pop() { + if let Some(id) = &window.style.id { + elements.insert(id.to_owned(), IdElement::Window(window)); + } + windows_to_visit.extend(&window.element_type.children); + + if let Some(content) = &window.element_type.content { + to_visit.push(content); + } + } + + while let Some(el) = to_visit.pop() { + if let Some(id) = &el.style.id { + elements.insert(id.to_owned(), IdElement::Generic(el)); + } + + use model::ElementType::*; + match &el.element_type { + Button(model::Button { + content: Some(content), + .. + }) + | Scroll(model::Scroll { + content: Some(content), + }) => { + to_visit.push(content); + } + VBox(model::VBox { items, .. }) | HBox(model::HBox { items, .. }) => { + for item in items { + to_visit.push(item) + } + } + _ => (), + } + } + + elements +} |