tests.rs
raw
use guiduck_scene::geom::{Point, Size};
use taffy::prelude::{length, percent};
use super::*;
use crate::clipboard::{LocalClipboard, Selection};
use crate::event::{Modifiers, PointerButton, PointerInput};
use crate::widget::{Container, WidgetTree};
fn sample_font() -> guiduck_scene::paint::Blob<u8> {
static FONT: &[u8] = include_bytes!("../../../../guiduck-samples/fonts/DejaVuSans.ttf");
static BLOB: std::sync::OnceLock<guiduck_scene::paint::Blob<u8>> = std::sync::OnceLock::new();
BLOB.get_or_init(|| guiduck_scene::paint::Blob::new(std::sync::Arc::new(FONT)))
.clone()
}
/// Root with two text inputs stacked in a column.
fn fixture() -> (WidgetTree, crate::widget::WidgetId, crate::widget::WidgetId) {
let mut tree =
WidgetTree::with_text_context(crate::text::TextContext::hermetic([sample_font()]));
let root = tree.insert(
Container::new(),
taffy::Style {
size: taffy::Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
flex_direction: taffy::FlexDirection::Column,
padding: taffy::Rect::length(10.0_f32),
gap: taffy::Size {
width: length(10.0_f32),
height: length(10.0_f32),
},
align_items: Some(taffy::AlignItems::FLEX_START),
..Default::default()
},
None,
);
let first = tree.insert(
TextInput::new(14.0).family("DejaVu Sans"),
taffy::Style {
size: taffy::Size {
width: length(200.0_f32),
height: taffy::prelude::auto(),
},
..Default::default()
},
Some(root),
);
let second = tree.insert(
TextInput::new(14.0).family("DejaVu Sans"),
taffy::Style {
size: taffy::Size {
width: length(200.0_f32),
height: taffy::prelude::auto(),
},
..Default::default()
},
Some(root),
);
tree.render_frame(Size::new(400.0, 300.0));
(tree, first, second)
}
fn button_press(
tree: &mut WidgetTree,
clipboard: &mut dyn Clipboard,
pos: Point,
button: PointerButton,
) {
tree.dispatch_pointer(
PointerInput::Down {
time: std::time::Instant::now(),
pos,
button,
},
clipboard,
);
tree.dispatch_pointer(PointerInput::Up { pos, button }, clipboard);
}
fn click(tree: &mut WidgetTree, clipboard: &mut LocalClipboard, pos: Point) {
button_press(tree, clipboard, pos, PointerButton::Left);
}
/// A left click with an explicit press time, for multi-click tests.
fn timed_click(
tree: &mut WidgetTree,
clipboard: &mut LocalClipboard,
pos: Point,
time: std::time::Instant,
) {
tree.dispatch_pointer(
PointerInput::Down {
pos,
button: PointerButton::Left,
time,
},
clipboard,
);
tree.dispatch_pointer(
PointerInput::Up {
pos,
button: PointerButton::Left,
},
clipboard,
);
}
fn type_str(tree: &mut WidgetTree, clipboard: &mut LocalClipboard, text: &str) {
for c in text.chars() {
press(
tree,
clipboard,
Key::Character(c.to_string()),
Modifiers::default(),
);
}
}
fn press(tree: &mut WidgetTree, clipboard: &mut LocalClipboard, key: Key, modifiers: Modifiers) {
tree.dispatch_key(
&KeyInput {
key,
modifiers,
pressed: true,
},
clipboard,
);
}
const CTRL: Modifiers = Modifiers {
ctrl: true,
shift: false,
alt: false,
logo: false,
};
const SHIFT: Modifiers = Modifiers {
ctrl: false,
shift: true,
alt: false,
logo: false,
};
fn content(tree: &WidgetTree, id: crate::widget::WidgetId) -> String {
tree.widget::<TextInput>(id).unwrap().content()
}
#[test]
fn click_focuses_and_typing_inserts() {
let (mut tree, first, second) = fixture();
let mut clipboard = LocalClipboard::default();
// The focus invariant grants the first field focus on the initial
// frame, before any interaction.
assert_eq!(tree.focus(), Some(first));
// Click-to-focus moves it.
click(&mut tree, &mut clipboard, Point::new(50.0, 60.0));
assert_eq!(tree.focus(), Some(second));
type_str(&mut tree, &mut clipboard, "hello");
assert_eq!(content(&tree, second), "hello");
assert!(tree.needs_frame(), "edits schedule a frame");
}
#[test]
fn focus_invariant_refocuses_after_removal() {
let (mut tree, first, second) = fixture();
assert_eq!(tree.focus(), Some(first));
tree.remove(first);
tree.render_frame(Size::new(400.0, 300.0));
assert_eq!(
tree.focus(),
Some(second),
"removing the focused widget refocuses the next frame"
);
}
#[test]
fn background_click_keeps_focus() {
let (mut tree, first, _second) = fixture();
let mut clipboard = LocalClipboard::default();
assert_eq!(tree.focus(), Some(first));
// The root container is not focusable; clicking it keeps the focus.
click(&mut tree, &mut clipboard, Point::new(300.0, 250.0));
assert_eq!(tree.focus(), Some(first));
}
#[test]
fn select_all_then_typing_replaces() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "hello");
press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL);
type_str(&mut tree, &mut clipboard, "bye");
assert_eq!(content(&tree, first), "bye");
}
#[test]
fn editing_keys() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "abcd");
press(
&mut tree,
&mut clipboard,
Key::Backspace,
Modifiers::default(),
);
assert_eq!(content(&tree, first), "abc");
press(&mut tree, &mut clipboard, Key::Home, Modifiers::default());
press(&mut tree, &mut clipboard, Key::Delete, Modifiers::default());
assert_eq!(content(&tree, first), "bc");
press(&mut tree, &mut clipboard, Key::Right, Modifiers::default());
type_str(&mut tree, &mut clipboard, "x");
assert_eq!(content(&tree, first), "bxc");
// Shift+End selects to end; typing replaces the selection.
press(&mut tree, &mut clipboard, Key::Home, Modifiers::default());
press(&mut tree, &mut clipboard, Key::End, SHIFT);
type_str(&mut tree, &mut clipboard, "z");
assert_eq!(content(&tree, first), "z");
}
#[test]
fn clipboard_round_trip() {
let (mut tree, first, second) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "copy me");
press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL);
press(&mut tree, &mut clipboard, Key::Character("c".into()), CTRL);
assert_eq!(
clipboard.get_text(Selection::Clipboard).as_deref(),
Some("copy me")
);
// Paste into the second input.
press(&mut tree, &mut clipboard, Key::Tab, Modifiers::default());
assert_eq!(tree.focus(), Some(second));
press(&mut tree, &mut clipboard, Key::Character("v".into()), CTRL);
assert_eq!(content(&tree, second), "copy me");
assert_eq!(content(&tree, first), "copy me");
// Cut clears the source.
press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL);
press(&mut tree, &mut clipboard, Key::Character("x".into()), CTRL);
assert_eq!(content(&tree, second), "");
assert_eq!(
clipboard.get_text(Selection::Clipboard).as_deref(),
Some("copy me")
);
}
#[test]
fn newlines_never_enter_a_single_line_field() {
let (mut tree, first, second) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
// Typing a newline inserts nothing and does not consume the key (so a
// default-button activation could still see it).
type_str(&mut tree, &mut clipboard, "ab\ncd");
assert_eq!(content(&tree, first), "abcd", "a typed newline is dropped");
// A multi-line clipboard paste lands as one line — the defect: the paste
// path used to insert the newlines the typed path rejects.
clipboard.set_text(Selection::Clipboard, "one\ntwo\r\nthree");
press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL);
press(&mut tree, &mut clipboard, Key::Character("v".into()), CTRL);
assert_eq!(
content(&tree, first),
"onetwothree",
"paste collapses lines"
);
// A multi-line primary-selection paste, likewise.
clipboard.set_text(Selection::Primary, "left\nright");
button_press(
&mut tree,
&mut clipboard,
Point::new(50.0, 60.0),
PointerButton::Middle,
);
assert_eq!(content(&tree, second), "leftright", "primary paste too");
// And an IME commit carrying a newline.
press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL);
tree.dispatch_ime(&ImeInput::Commit("here\nthere".into()));
assert_eq!(content(&tree, second), "herethere", "IME commit too");
}
#[test]
fn multiline_field_accepts_line_breaks() {
let mut tree =
WidgetTree::with_text_context(crate::text::TextContext::hermetic([sample_font()]));
let root = tree.insert(Container::new(), taffy::Style::default(), None);
let field = tree.insert(
TextInput::new(14.0).family("DejaVu Sans").multiline(true),
taffy::Style {
size: taffy::Size {
width: length(200.0_f32),
height: taffy::prelude::auto(),
},
..Default::default()
},
Some(root),
);
tree.render_frame(Size::new(400.0, 300.0));
let mut clipboard = LocalClipboard::default();
assert_eq!(tree.focus(), Some(field));
// Enter inserts a newline instead of being declined.
type_str(&mut tree, &mut clipboard, "a");
press(&mut tree, &mut clipboard, Key::Enter, Modifiers::default());
type_str(&mut tree, &mut clipboard, "b");
assert_eq!(content(&tree, field), "a\nb");
// Pasted newlines survive in a multi-line field.
clipboard.set_text(Selection::Clipboard, "c\nd");
press(&mut tree, &mut clipboard, Key::End, Modifiers::default());
press(&mut tree, &mut clipboard, Key::Character("v".into()), CTRL);
assert_eq!(content(&tree, field), "a\nbc\nd");
// It names itself a multi-line editor to assistive technology.
assert_eq!(
tree.widget::<TextInput>(field).unwrap().role(),
accesskit::Role::MultilineTextInput
);
}
#[test]
fn single_line_field_declines_enter() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "text");
// Enter is not consumed by a single-line field, so a default button
// could still act on it, and it inserts nothing.
let consumed = tree.dispatch_key(
&KeyInput {
key: Key::Enter,
modifiers: Modifiers::default(),
pressed: true,
},
&mut clipboard,
);
assert!(!consumed, "a single-line field declines Enter");
assert_eq!(content(&tree, first), "text", "and inserts no newline");
assert_eq!(
tree.widget::<TextInput>(first).unwrap().role(),
accesskit::Role::TextInput
);
}
#[test]
fn tab_cycles_focus() {
let (mut tree, first, second) = fixture();
let mut clipboard = LocalClipboard::default();
// The focus invariant starts the cycle at the first field.
assert_eq!(tree.focus(), Some(first));
press(&mut tree, &mut clipboard, Key::Tab, Modifiers::default());
assert_eq!(tree.focus(), Some(second));
press(&mut tree, &mut clipboard, Key::Tab, Modifiers::default());
assert_eq!(tree.focus(), Some(first), "cycles");
press(&mut tree, &mut clipboard, Key::Tab, SHIFT);
assert_eq!(tree.focus(), Some(second), "shift-tab goes backwards");
}
#[test]
fn ime_preedit_and_commit() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "ab");
tree.dispatch_ime(&ImeInput::Preedit {
text: "しか".into(),
cursor: Some((6, 6)),
});
// Preedit is displayed but not part of the committed content.
assert_eq!(
tree.widget::<TextInput>(first).unwrap().display_content(),
"abしか"
);
assert_eq!(content(&tree, first), "ab");
tree.dispatch_ime(&ImeInput::Commit("鹿".into()));
assert_eq!(content(&tree, first), "ab鹿");
// A cancelled composition leaves committed text alone.
tree.dispatch_ime(&ImeInput::Preedit {
text: "x".into(),
cursor: Some((1, 1)),
});
tree.dispatch_ime(&ImeInput::Preedit {
text: String::new(),
cursor: None,
});
assert_eq!(content(&tree, first), "ab鹿");
}
#[test]
fn drag_selects_and_replaces() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "wide text");
// Press near the start, drag right, release: a selection exists.
tree.dispatch_pointer(
PointerInput::Down {
time: std::time::Instant::now(),
pos: Point::new(20.0, 25.0),
button: PointerButton::Left,
},
&mut clipboard,
);
tree.dispatch_pointer(PointerInput::Moved(Point::new(180.0, 25.0)), &mut clipboard);
tree.dispatch_pointer(
PointerInput::Up {
pos: Point::new(180.0, 25.0),
button: PointerButton::Left,
},
&mut clipboard,
);
// Finishing the drag stored the selection as the primary selection,
// without touching the regular clipboard.
assert_eq!(
clipboard.get_text(Selection::Primary).as_deref(),
Some("wide text")
);
assert_eq!(clipboard.get_text(Selection::Clipboard), None);
type_str(&mut tree, &mut clipboard, "replaced");
assert_eq!(content(&tree, first), "replaced");
}
#[test]
fn keyboard_selection_updates_primary() {
let (mut tree, _first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "primary");
press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL);
assert_eq!(
clipboard.get_text(Selection::Primary).as_deref(),
Some("primary"),
"select-all populates the primary selection"
);
// Collapsing the selection leaves the primary alone.
press(&mut tree, &mut clipboard, Key::Home, Modifiers::default());
press(&mut tree, &mut clipboard, Key::Right, SHIFT);
assert_eq!(
clipboard.get_text(Selection::Primary).as_deref(),
Some("p"),
"shift-selection replaces the primary selection"
);
}
#[test]
fn middle_click_pastes_primary() {
let (mut tree, first, second) = fixture();
let mut clipboard = LocalClipboard::default();
clipboard.set_text(Selection::Primary, "pasted");
button_press(
&mut tree,
&mut clipboard,
Point::new(50.0, 25.0),
PointerButton::Middle,
);
assert_eq!(content(&tree, first), "pasted");
assert_eq!(
tree.focus(),
Some(first),
"middle-click focuses like any click"
);
// Select the pasted word by dragging, then middle-click the second
// field: the drag's selection is what pastes.
tree.dispatch_pointer(
PointerInput::Down {
time: std::time::Instant::now(),
pos: Point::new(20.0, 25.0),
button: PointerButton::Left,
},
&mut clipboard,
);
tree.dispatch_pointer(PointerInput::Moved(Point::new(180.0, 25.0)), &mut clipboard);
tree.dispatch_pointer(
PointerInput::Up {
pos: Point::new(180.0, 25.0),
button: PointerButton::Left,
},
&mut clipboard,
);
button_press(
&mut tree,
&mut clipboard,
Point::new(50.0, 60.0),
PointerButton::Middle,
);
assert_eq!(content(&tree, second), "pasted");
}
#[test]
fn double_click_selects_word() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "hello");
let base = std::time::Instant::now();
let pos = Point::new(30.0, 25.0);
timed_click(&mut tree, &mut clipboard, pos, base);
timed_click(
&mut tree,
&mut clipboard,
pos,
base + std::time::Duration::from_millis(100),
);
assert_eq!(
clipboard.get_text(Selection::Primary).as_deref(),
Some("hello"),
"double-click selects the word and syncs the primary selection"
);
type_str(&mut tree, &mut clipboard, "bye");
assert_eq!(content(&tree, first), "bye", "typing replaces the word");
}
#[test]
fn triple_click_selects_line() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "hello world");
let base = std::time::Instant::now();
let pos = Point::new(30.0, 25.0);
for i in 0..3 {
timed_click(
&mut tree,
&mut clipboard,
pos,
base + std::time::Duration::from_millis(100 * i),
);
}
assert_eq!(
clipboard.get_text(Selection::Primary).as_deref(),
Some("hello world"),
"triple-click selects the line"
);
type_str(&mut tree, &mut clipboard, "z");
assert_eq!(content(&tree, first), "z");
}
#[test]
fn slow_second_click_just_moves_the_caret() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "hello");
let base = std::time::Instant::now();
let pos = Point::new(30.0, 25.0);
timed_click(&mut tree, &mut clipboard, pos, base);
timed_click(
&mut tree,
&mut clipboard,
pos,
base + std::time::Duration::from_secs(1),
);
assert_eq!(
clipboard.get_text(Selection::Primary),
None,
"no selection was made"
);
type_str(&mut tree, &mut clipboard, "X");
assert_eq!(content(&tree, first).len(), 6, "insert, not replace");
}
#[test]
fn user_edits_raise_changed_events() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
let root = tree.root().unwrap();
let seen: std::rc::Rc<std::cell::RefCell<Vec<String>>> = Default::default();
let seen2 = seen.clone();
// Subscribed on the root: Changed bubbles like any other event.
tree.on_event(root, crate::event::EventKind::Changed, move |_ctx, ev| {
seen2
.borrow_mut()
.push(ev.changed().expect("changed payload").to_owned());
});
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "hi");
assert_eq!(*seen.borrow(), vec!["h".to_owned(), "hi".to_owned()]);
// Programmatic set_content is silent…
tree.widget_mut::<TextInput>(first)
.unwrap()
.set_content("reset");
assert_eq!(seen.borrow().len(), 2);
// …and the next user edit compares against the programmatic value.
press(&mut tree, &mut clipboard, Key::End, Modifiers::default());
press(
&mut tree,
&mut clipboard,
Key::Backspace,
Modifiers::default(),
);
assert_eq!(seen.borrow().last().map(String::as_str), Some("rese"));
// Copy does not edit; paste at the end does.
press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL);
press(&mut tree, &mut clipboard, Key::Character("c".into()), CTRL);
assert_eq!(seen.borrow().len(), 3);
press(&mut tree, &mut clipboard, Key::End, Modifiers::default());
press(&mut tree, &mut clipboard, Key::Character("v".into()), CTRL);
assert_eq!(seen.borrow().last().map(String::as_str), Some("reserese"));
assert_eq!(seen.borrow().len(), 4);
// An IME commit edits; a bare preedit does not.
tree.dispatch_ime(&ImeInput::Preedit {
text: "x".into(),
cursor: Some((1, 1)),
});
assert_eq!(seen.borrow().len(), 4);
tree.dispatch_ime(&ImeInput::Commit("鹿".into()));
assert_eq!(seen.borrow().last().map(String::as_str), Some("reserese鹿"));
}
#[test]
fn right_click_does_not_move_caret() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "abc");
// Caret sits at the end; a right-click near the start must not move it.
button_press(
&mut tree,
&mut clipboard,
Point::new(12.0, 25.0),
PointerButton::Right,
);
type_str(&mut tree, &mut clipboard, "d");
assert_eq!(content(&tree, first), "abcd");
}
#[test]
fn focus_reflects_in_accessibility_tree() {
let (mut tree, first, _) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 25.0));
type_str(&mut tree, &mut clipboard, "a11y");
let update = tree.accessibility_tree();
let focused = update
.nodes
.iter()
.find(|(id, _)| *id == update.focus)
.map(|(_, n)| n)
.expect("focused node present");
assert_eq!(focused.role(), accesskit::Role::TextInput);
assert_eq!(focused.value(), Some("a11y"));
let _ = first;
}
/// Whether the focused input currently paints its caret (the retained
/// fragment holds one more fill than the blink-hidden phase).
fn caret_painted(tree: &mut WidgetTree, id: crate::widget::WidgetId) -> bool {
tree.render_frame(Size::new(400.0, 300.0));
let mut fragment = guiduck_scene::Fragment::new();
let size = tree.layout(id).size();
tree.widget_mut::<TextInput>(id)
.expect("input exists")
.paint(&mut fragment, size);
let fills = fragment
.items
.iter()
.filter(|i| matches!(i, guiduck_scene::DisplayItem::Fill { .. }))
.count();
// Background fill + (optionally) the caret fill; no selection here.
fills > 1
}
#[test]
fn the_caret_blinks_deterministically() {
let (mut tree, first, _second) = fixture();
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(60.0, 25.0));
assert!(caret_painted(&mut tree, first), "fresh focus: caret solid");
// The focused input scheduled a wake; unfocused trees schedule none.
let now = std::time::Instant::now();
let deadline = tree.next_wake(now).expect("a blink is scheduled");
assert_eq!(deadline, now + BLINK_INTERVAL);
// The deadline arrives: the caret toggles off, and the next toggle is
// scheduled.
tree.tick(deadline);
assert!(tree.needs_frame(), "the toggle repaints");
assert!(
!caret_painted(&mut tree, first),
"blink phase: caret hidden"
);
let second_deadline = tree
.next_wake(deadline)
.expect("the blink keeps rescheduling");
assert_eq!(second_deadline, deadline + BLINK_INTERVAL);
// Typing resets the phase to solid and re-anchors the schedule.
type_str(&mut tree, &mut clipboard, "x");
assert!(caret_painted(&mut tree, first), "edits show the caret");
let anchored = tree
.next_wake(second_deadline)
.expect("typing re-anchored the blink");
// Focus loss stops the blink. Plain `set_focus(None)` would not do it —
// the focus invariant refocuses the input on the next frame — so take
// the focusables away by disabling the tree: focus clears, the
// already-anchored deadline fires once (the unfocused input declines
// it), and nothing further is scheduled.
let root = tree.root().expect("root");
tree.set_enabled(root, false);
tree.render_frame(Size::new(400.0, 300.0));
assert_eq!(tree.focus(), None, "no enabled focusables remain");
tree.tick(anchored);
tree.render_frame(Size::new(400.0, 300.0));
assert_eq!(
tree.next_wake(anchored + BLINK_INTERVAL),
None,
"an unfocused input requests no wakes (zero-idle preserved)"
);
}
/// A clipboard whose reads always answer `Pending`, like a platform that
/// must negotiate with the selection's owner. Records what was asked for.
#[derive(Default)]
struct PendingClipboard {
requests: Vec<Selection>,
}
impl Clipboard for PendingClipboard {
fn request_text(&mut self, selection: Selection) -> crate::clipboard::TextRequest {
self.requests.push(selection);
crate::clipboard::TextRequest::Pending
}
fn set_text(&mut self, _selection: Selection, _text: &str) {}
}
#[test]
fn a_pending_paste_inserts_on_delivery_not_on_the_gesture() {
let (mut tree, first, _second) = fixture();
let mut clipboard = PendingClipboard::default();
assert_eq!(tree.focus(), Some(first));
// The gesture itself inserts nothing: the platform hasn't answered yet.
tree.dispatch_key(
&KeyInput {
key: Key::Character("v".into()),
modifiers: CTRL,
pressed: true,
},
&mut clipboard,
);
assert_eq!(clipboard.requests, vec![Selection::Clipboard]);
assert_eq!(content(&tree, first), "");
// The delivery is what pastes.
tree.dispatch_paste("arrived");
assert_eq!(content(&tree, first), "arrived");
assert!(tree.needs_frame(), "a delivered paste schedules a frame");
}
#[test]
fn a_delivered_paste_follows_focus() {
let (mut tree, first, second) = fixture();
let mut pending = PendingClipboard::default();
assert_eq!(tree.focus(), Some(first));
// Middle-click asks for the primary selection; the answer is Pending.
button_press(
&mut tree,
&mut pending,
Point::new(50.0, 20.0),
PointerButton::Middle,
);
assert!(pending.requests.contains(&Selection::Primary));
// Focus moves before the platform answers. Delivery goes to the
// focused field — pasted text is input, and input follows focus.
let mut clipboard = LocalClipboard::default();
click(&mut tree, &mut clipboard, Point::new(50.0, 60.0));
assert_eq!(tree.focus(), Some(second));
tree.dispatch_paste("late");
assert_eq!(content(&tree, first), "");
assert_eq!(content(&tree, second), "late");
}
#[test]
fn a_delivered_paste_is_sanitized_like_any_other() {
let (mut tree, first, _second) = fixture();
assert_eq!(tree.focus(), Some(first));
// A single-line field collapses a delivered multi-line paste, exactly
// as it would a synchronous one.
tree.dispatch_paste("one\ntwo");
assert_eq!(content(&tree, first), "onetwo");
}