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:
irisandClaude Opus 5 committed 2026-09-04 19:27:57 -04:00
1 parent 68a7f41ed0
commit ff7e9c0435
1 file changed
+155
+155
View File
@@ -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("", EditMode::SingleLine);
ctx(&mut t, &mut d).set_caret(3);
ctx(&mut t, &mut d).backspace(false);
assert_eq!(content(&t), "a");
}
}