test(core): add headless end-to-end tests and fix bugs they found
Adds crates/ghostty-core/tests/e2e_*.rs: 81 tests that drive the real core Surface, termio, pty and child processes through a fake runtime (recording apprt actions, in-memory clipboard) with a headless renderer (ghostty-renderer/src/headless.rs). They cover keys (legacy, DECCKM, kitty, modifyOtherKeys), keybinding actions and sequences, mouse selection/reporting/scrolling/links, clipboard incl. OSC 52 and paste safety, resize, focus, titles/pwd/bell/notifications/progress, child exit, and IME preedit. Fixes for issues found by the tests and by the Codex audit (docs/rewrite/notes/review-core-codex.md, all 9 items): - replacing a selection with copy-on-select panicked (stale tracked handles); global new_window no longer inherits a parent; the child environment uses the runtime's env wholesale so GTK's removals stick; Flatpak host resources path; locale recovery (ensureLocale) in apprt-gtk before GTK init and for CLI actions; failed screen-file opens propagate errors; OSC 52 base64 rules, CSI/ESC binding length limits and GHOSTTY_LOG parsing match Zig. - Also includes V2's termio cleanups (tmux viewer wiring in the stream handler, Pty slave semantics docs, shared cgroup wait loop). apprt-gtk gains a debug input replay hook (GHOSTTY_DEBUG_INPUT_SCRIPT) exercising the real GTK key path. With it, the real app was verified on Wayland: typing and output, new tab, splits, select-all/copy/paste and font size all work (docs/rewrite/notes/verification-V1.md). Not yet exercised at GTK level: mouse, real IM-context IME, clipboard dialogs. Test Plan: - cargo test --workspace (2983 passed) - cargo test -p ghostty-core --tests (81 e2e tests)
This commit is contained in:
1 parent
d650e5171b
commit
be6d706ba5
33 files changed
+3016
-222
No files matched your search
@@ -0,0 +1,119 @@
|
||||
//! IME: preedit state and rendering, commit, composing key events.
|
||||
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use ghostty_input::{Key, KeyEvent};
|
||||
|
||||
/// RGBA pixels of the cells `cols` on `row` of the last drawn frame.
|
||||
fn region(h: &Harness, cols: std::ops::Range<u32>, row: u32) -> Vec<u8> {
|
||||
h.surface.draw().expect("draw");
|
||||
let Some((w, hgt, px)) = h.surface.frame_pixels() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let s = h.surface.size();
|
||||
let x0 = (s.padding.left + cols.start * s.cell.width) as usize;
|
||||
let x1 = (s.padding.left + cols.end * s.cell.width) as usize;
|
||||
let y0 = (s.padding.top + row * s.cell.height) as usize;
|
||||
let y1 = y0 + s.cell.height as usize;
|
||||
let mut out = Vec::new();
|
||||
for y in y0..y1 {
|
||||
// frame is bottom-up
|
||||
let fy = hgt - 1 - y;
|
||||
let start = (fy * w + x0) * 4;
|
||||
let end = (fy * w + x1) * 4;
|
||||
out.extend_from_slice(&px[start..end]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preedit_state_and_ime_point() {
|
||||
let h = harness!(Harness::recorder("", &[]));
|
||||
let before = h.surface.ime_point();
|
||||
h.surface
|
||||
.preedit_callback(Some("\u{3042}\u{3044}"))
|
||||
.unwrap();
|
||||
{
|
||||
let st = h.surface.lock_state();
|
||||
let p = st.preedit.as_ref().expect("preedit set");
|
||||
assert_eq!(p.codepoints.len(), 2);
|
||||
assert_eq!(p.codepoints[0].codepoint, 0x3042);
|
||||
assert!(p.codepoints[0].wide);
|
||||
}
|
||||
// The candidate window sits below the cursor and spans the preedit.
|
||||
let after = h.surface.ime_point();
|
||||
assert_eq!(after.x, before.x);
|
||||
assert_eq!(after.y, before.y);
|
||||
assert!(after.width >= f64::from(2 * 2 * h.surface.size().cell.width) - 1.0);
|
||||
h.surface.preedit_callback(None).unwrap();
|
||||
assert!(h.surface.lock_state().preedit.is_none());
|
||||
// Nothing is sent to the child by preedit alone.
|
||||
h.settle(50);
|
||||
assert_eq!(h.recorded(), Vec::<u8>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preedit_is_rendered_and_cleared() {
|
||||
let h = harness!(Harness::recorder("", &[]));
|
||||
// Cursor sits after "READY" at column 5, row 0. Disable blink noise by
|
||||
// comparing the cells after the cursor cell.
|
||||
let cols = 7..10;
|
||||
let baseline = {
|
||||
let mut b = Vec::new();
|
||||
h.wait("first frame", |h| {
|
||||
b = region(h, cols.clone(), 0);
|
||||
!b.is_empty()
|
||||
});
|
||||
b
|
||||
};
|
||||
h.surface.preedit_callback(Some("abcd")).unwrap();
|
||||
h.wait("preedit visible", |h| {
|
||||
let r = region(h, cols.clone(), 0);
|
||||
!r.is_empty() && r != baseline
|
||||
});
|
||||
h.surface.preedit_callback(None).unwrap();
|
||||
h.wait("preedit cleared", |h| {
|
||||
region(h, cols.clone(), 0) == baseline
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_sends_text_and_composing_keys_do_not() {
|
||||
let h = harness!(Harness::recorder("", &[]));
|
||||
// A key event that is part of a composition is not encoded.
|
||||
h.key_event(KeyEvent {
|
||||
key: Key::KeyK,
|
||||
composing: true,
|
||||
utf8: "k",
|
||||
unshifted_codepoint: 'k' as u32,
|
||||
..KeyEvent::default()
|
||||
});
|
||||
h.settle(50);
|
||||
assert_eq!(h.recorded(), Vec::<u8>::new());
|
||||
|
||||
// Commit: preedit ends, committed text is sent with no key.
|
||||
h.surface
|
||||
.preedit_callback(Some("\u{3053}\u{3093}"))
|
||||
.unwrap();
|
||||
h.surface.preedit_callback(None).unwrap();
|
||||
h.key_event(KeyEvent {
|
||||
key: Key::Unidentified,
|
||||
utf8: "\u{3053}\u{3093}\u{306b}\u{3061}\u{306f}",
|
||||
..KeyEvent::default()
|
||||
});
|
||||
h.expect_input("\u{3053}\u{3093}\u{306b}\u{3061}\u{306f}".as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_with_committed_text_sends_text_not_cr() {
|
||||
// IME confirmation sends enter with utf8: the text is sent, not "\r".
|
||||
let h = harness!(Harness::recorder("", &[]));
|
||||
h.key_event(KeyEvent {
|
||||
key: Key::Enter,
|
||||
utf8: "\u{6f22}",
|
||||
..KeyEvent::default()
|
||||
});
|
||||
h.expect_input("\u{6f22}".as_bytes());
|
||||
}
|
||||
Reference in new issue
Block a user