Test iris's editor, which had no coverage and was just rewritten
iris has no tests at all, and the rule here is not to erect a harness where there is none -- but the editor is the exception on both counts. It is the one part of the library that is pure logic over a string and a layout rather than something needing a GPU and a window, and it was just rewritten wholesale onto parley's selection model with no way to exercise it: input cannot be synthesised in the headless compositor the examples run under, because it has no seat devices. Fourteen tests over insert, backspace, delete, span clearing, select-all, motion, single- versus multi-line, and take. Two are there for specific things the rewrite could plausibly have broken: the IME preedit path, which resends its whole composition each keystroke so `replace` has to remove exactly what it added last time, and editing text with multi-byte characters, since parley addresses by byte offset where the old code counted (line, index). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
68a7f41ed0
commit
ff7e9c0435
1 file changed
+155
@@ -521,3 +521,158 @@ impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use iris_core::{TextAttrs, TextBuffer};
|
||||
|
||||
/// The editor is the one part of iris that is pure logic over a string and
|
||||
/// a layout, and it was rewritten wholesale when the text stack changed --
|
||||
/// so it is the one part worth testing directly. Everything else here
|
||||
/// needs a GPU and a window.
|
||||
fn edit(text: &str, mode: EditMode) -> (TextEdit, TextData) {
|
||||
let view = TextView::new(TextBuffer::new(text), TextAttrs::default(), None);
|
||||
(TextEdit::new(view, mode), TextData::default())
|
||||
}
|
||||
|
||||
fn ctx<'a>(text: &'a mut TextEdit, data: &'a mut TextData) -> TextEditCtx<'a> {
|
||||
TextEditCtx { text, data }
|
||||
}
|
||||
|
||||
fn content(text: &TextEdit) -> String {
|
||||
text.buf.text().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_at_the_caret() {
|
||||
let (mut t, mut d) = edit("ac", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(1);
|
||||
ctx(&mut t, &mut d).insert("b");
|
||||
assert_eq!(content(&t), "abc");
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_removes_the_character_before_the_caret() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(2);
|
||||
ctx(&mut t, &mut d).backspace(false);
|
||||
assert_eq!(content(&t), "ac");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_at_the_start_does_nothing() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).backspace(false);
|
||||
assert_eq!(content(&t), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_removes_the_character_after_the_caret() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(1);
|
||||
ctx(&mut t, &mut d).delete(false);
|
||||
assert_eq!(content(&t), "ac");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_at_the_end_does_nothing() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(3);
|
||||
ctx(&mut t, &mut d).delete(false);
|
||||
assert_eq!(content(&t), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_all_then_typing_replaces_everything() {
|
||||
let (mut t, mut d) = edit("hello", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
assert_eq!(t.selected_text().as_deref(), Some("hello"));
|
||||
ctx(&mut t, &mut d).insert("x");
|
||||
assert_eq!(content(&t), "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_a_span_leaves_the_caret_at_its_start() {
|
||||
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
assert!(ctx(&mut t, &mut d).clear_span());
|
||||
assert_eq!(content(&t), "");
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_line_field_refuses_newlines() {
|
||||
let (mut t, mut d) = edit("", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).insert("a\nb");
|
||||
assert_eq!(content(&t), "ab");
|
||||
ctx(&mut t, &mut d).newline();
|
||||
assert_eq!(content(&t), "ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_multi_line_field_keeps_newlines() {
|
||||
let (mut t, mut d) = edit("", EditMode::MultiLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).insert("a\nb");
|
||||
assert_eq!(content(&t), "a\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_empties_the_field_and_hands_back_what_was_there() {
|
||||
let (mut t, mut d) = edit("some text", EditMode::SingleLine);
|
||||
assert_eq!(ctx(&mut t, &mut d).take(), "some text");
|
||||
assert_eq!(content(&t), "");
|
||||
}
|
||||
|
||||
/// The IME's preedit path: each keystroke resends the whole composition,
|
||||
/// so `replace` has to remove exactly what it added last time.
|
||||
#[test]
|
||||
fn ime_preedit_replaces_its_own_previous_text() {
|
||||
let (mut t, mut d) = edit("", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).replace(0, "n");
|
||||
assert_eq!(content(&t), "n");
|
||||
ctx(&mut t, &mut d).replace(1, "ni");
|
||||
assert_eq!(content(&t), "ni");
|
||||
ctx(&mut t, &mut d).replace(2, "に");
|
||||
assert_eq!(content(&t), "に");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_moves_the_caret_and_shift_extends_a_span() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 1);
|
||||
ctx(&mut t, &mut d).motion(Motion::Right, true);
|
||||
assert_eq!(t.selected_text().as_deref(), Some("b"));
|
||||
}
|
||||
|
||||
/// Collapsing a span with an unshifted arrow goes to the near end rather
|
||||
/// than stepping one character from the focus.
|
||||
#[test]
|
||||
fn an_unshifted_arrow_collapses_a_span_to_its_edge() {
|
||||
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
ctx(&mut t, &mut d).motion(Motion::Left, false);
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 0);
|
||||
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 6);
|
||||
}
|
||||
|
||||
/// Byte offsets, not character counts: a caret placed after a multi-byte
|
||||
/// character must not split it.
|
||||
#[test]
|
||||
fn multibyte_text_is_edited_by_byte_offset() {
|
||||
let (mut t, mut d) = edit("aé", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(3);
|
||||
ctx(&mut t, &mut d).backspace(false);
|
||||
assert_eq!(content(&t), "a");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user