tests.rs raw

use super::*;
use crate::clipboard::NoClipboard;
use crate::event::{Key, KeyInput, Modifiers};
use crate::text::TextContext;

const VIEWPORT: Size = Size::new(300.0, 200.0);

fn sample_font() -> guiduck_scene::paint::Blob<u8> {
    static FONT: &[u8] = include_bytes!("../../../../guiduck-samples/fonts/DejaVuSans.ttf");
    guiduck_scene::paint::Blob::new(std::sync::Arc::new(FONT))
}

fn fixture() -> (WidgetTree, WidgetId) {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([sample_font()]));
    let root = tree.insert(
        super::super::Container::new(),
        taffy::Style::default(),
        None,
    );
    let dialog = tree.insert(
        Dialog::new()
            .title("Delete draft?")
            .content(ContentBuilder::new(|tree, parent| {
                tree.insert(
                    super::super::Button::new("Cancel"),
                    taffy::Style::default(),
                    Some(parent),
                );
            })),
        taffy::Style::default(),
        Some(root),
    );
    tree.render_frame(VIEWPORT);
    (tree, dialog)
}

/// Search, rather than assume a depth: the sheet sits inside the scrim.
fn sheet_showing(tree: &WidgetTree) -> bool {
    let mut stack: Vec<WidgetId> = tree.overlay_roots();
    while let Some(id) = stack.pop() {
        if tree.widget::<DialogSheet>(id).is_some() {
            return true;
        }
        stack.extend(tree.children(id).iter().copied());
    }
    false
}

#[test]
fn open_is_controlled_by_the_binding() {
    let (mut tree, dialog) = fixture();
    assert!(!sheet_showing(&tree), "a closed dialog has no sheet at all");

    // The app owns `:open`; the tree reconciles to it.
    tree.widget_mut::<Dialog>(dialog).unwrap().set_open(true);
    tree.render_frame(VIEWPORT);
    assert!(sheet_showing(&tree));

    tree.widget_mut::<Dialog>(dialog).unwrap().set_open(false);
    tree.render_frame(VIEWPORT);
    assert!(!sheet_showing(&tree), "the body is gone, not hidden");
}

#[test]
fn the_body_is_lazy_like_every_other_deferred_content() {
    /// Is the body's Cancel button anywhere in the tree?
    fn body_built(tree: &WidgetTree) -> bool {
        let mut stack: Vec<WidgetId> = tree.overlay_roots();
        stack.extend(tree.root());
        while let Some(id) = stack.pop() {
            if tree.widget::<super::super::Button>(id).is_some() {
                return true;
            }
            stack.extend(tree.children(id).iter().copied());
        }
        false
    }

    let (mut tree, dialog) = fixture();
    assert!(!body_built(&tree), "a closed dialog's body does not exist");

    tree.widget_mut::<Dialog>(dialog).unwrap().set_open(true);
    tree.render_frame(VIEWPORT);
    assert!(body_built(&tree), "opening built it");

    tree.widget_mut::<Dialog>(dialog).unwrap().set_open(false);
    tree.render_frame(VIEWPORT);
    assert!(!body_built(&tree), "and closing took it away, not hid it");
}

#[test]
fn escape_reports_rather_than_closing() {
    // The heart of the controlled contract. If Escape dismissed the sheet,
    // `:open` would still say true and the app's state would have quietly
    // diverged from the screen.
    let (mut tree, dialog) = fixture();
    let closed = std::rc::Rc::new(std::cell::Cell::new(0));
    let seen = std::rc::Rc::clone(&closed);
    tree.on_event(dialog, crate::event::EventKind::Close, move |_, _| {
        seen.set(seen.get() + 1);
    });
    tree.widget_mut::<Dialog>(dialog).unwrap().set_open(true);
    tree.render_frame(VIEWPORT);

    tree.dispatch_key(
        &KeyInput {
            key: Key::Escape,
            modifiers: Modifiers::default(),
            pressed: true,
        },
        &mut NoClipboard,
    );
    tree.render_frame(VIEWPORT);

    assert_eq!(closed.get(), 1, "the request reached the app");
    assert!(
        sheet_showing(&tree),
        "and the sheet is still up: only the app may close it"
    );
    assert!(tree.widget::<Dialog>(dialog).unwrap().is_open());

    // The app answers, and now it goes.
    tree.widget_mut::<Dialog>(dialog).unwrap().set_open(false);
    tree.render_frame(VIEWPORT);
    assert!(!sheet_showing(&tree));
}

#[test]
fn focus_lands_inside_and_comes_back() {
    let (mut tree, dialog) = fixture();
    let outside = tree.insert(
        super::super::Button::new("Behind"),
        taffy::Style::default(),
        tree.root(),
    );
    tree.render_frame(VIEWPORT);
    tree.set_focus(Some(outside));

    tree.widget_mut::<Dialog>(dialog).unwrap().set_open(true);
    tree.render_frame(VIEWPORT);
    // The overlay's focus trap and the focus invariant do this between them;
    // the dialog adds nothing.
    assert_ne!(tree.focus(), Some(outside), "focus moved into the sheet");

    tree.widget_mut::<Dialog>(dialog).unwrap().set_open(false);
    tree.render_frame(VIEWPORT);
    assert_eq!(tree.focus(), Some(outside), "and came back on close");
}