use rdom_core::{Position, Range, Selection};
use crate::TuiDom;
use crate::layout::{Display, Size, UserSelect};
use crate::render::{LayoutExt, Rect};
use crate::runtime::selection::clipboard::{Clipboard, MemoryClipboard, serialize_selection};
use crate::style::{CascadeExt, Stylesheet, TuiStyle};
fn prepare(dom: &mut TuiDom, sheet: &Stylesheet) {
dom.cascade(sheet);
dom.layout_dom(Rect::new(0, 0, 40, 10));
}
#[test]
fn memory_clipboard_roundtrip() {
let mut cb = MemoryClipboard::new();
assert_eq!(cb.read_text(), None);
cb.write_text("hello".to_string());
assert_eq!(cb.read_text().as_deref(), Some("hello"));
}
#[test]
fn memory_clipboard_with_text_seeds() {
let mut cb = MemoryClipboard::with_text("seeded");
assert_eq!(cb.read_text().as_deref(), Some("seeded"));
}
#[test]
fn memory_clipboard_overwrites() {
let mut cb = MemoryClipboard::with_text("old");
cb.write_text("new".to_string());
assert_eq!(cb.peek(), Some("new"));
}
#[test]
fn serialize_single_node_range_returns_sliced_text() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hello world");
dom.append_child(p, t).unwrap();
dom.append_child(root, p).unwrap();
prepare(&mut dom, &Stylesheet::bare());
let range = Range::ordered_unchecked(Position::new(t, 6), Position::new(t, 11));
assert_eq!(serialize_selection(&dom, &range), "world");
}
#[test]
fn serialize_cross_node_range_concats_text_in_doc_order() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t_ab = dom.create_text_node("ab");
dom.append_child(p, t_ab).unwrap();
let code = dom.create_element("code");
let t_xy = dom.create_text_node("XY");
dom.append_child(code, t_xy).unwrap();
dom.append_child(p, code).unwrap();
let t_cd = dom.create_text_node("cd");
dom.append_child(p, t_cd).unwrap();
dom.append_child(root, p).unwrap();
prepare(&mut dom, &Stylesheet::bare());
let range = Range::ordered_unchecked(Position::new(t_ab, 1), Position::new(t_cd, 1));
assert_eq!(serialize_selection(&dom, &range), "bXYc");
}
#[test]
fn serialize_skips_user_select_none_subtree() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t_ab = dom.create_text_node("ab");
dom.append_child(p, t_ab).unwrap();
let chrome = dom.create_element("span");
dom.add_class(chrome, "chrome").unwrap();
let t_hidden = dom.create_text_node("HIDDEN");
dom.append_child(chrome, t_hidden).unwrap();
dom.append_child(p, chrome).unwrap();
let t_cd = dom.create_text_node("cd");
dom.append_child(p, t_cd).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(40)),
)
.rule_unchecked(".chrome", TuiStyle::new().user_select(UserSelect::None));
prepare(&mut dom, &sheet);
let range = Range::ordered_unchecked(Position::new(t_ab, 0), Position::new(t_cd, 2));
assert_eq!(serialize_selection(&dom, &range), "abcd");
}
#[test]
fn serialize_cjk_preserves_bytes() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("中文");
dom.append_child(p, t).unwrap();
dom.append_child(root, p).unwrap();
prepare(&mut dom, &Stylesheet::bare());
let range = Range::ordered_unchecked(Position::new(t, 0), Position::new(t, 3));
assert_eq!(serialize_selection(&dom, &range), "中");
}
#[test]
fn serialize_respects_dom_set_selection_via_selection_range() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hello world");
dom.append_child(p, t).unwrap();
dom.append_child(root, p).unwrap();
prepare(&mut dom, &Stylesheet::bare());
dom.set_selection(Some(Selection::new(
Position::new(t, 6),
Position::new(t, 11),
)));
let range = dom.selection_range().expect("selection is set");
assert_eq!(serialize_selection(&dom, &range), "world");
}
use crate::runtime::selection::clipboard::OscClipboard;
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct ByteSink(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for ByteSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl ByteSink {
fn snapshot(&self) -> Vec<u8> {
self.0.lock().unwrap().clone()
}
}
#[test]
fn osc52_write_emits_base64_in_osc_sequence() {
let sink = ByteSink::default();
let snapshot = sink.clone();
let mut cb = OscClipboard::with_writer(sink);
cb.write_text("hello".into());
let out = String::from_utf8(snapshot.snapshot()).unwrap();
assert_eq!(out, "\x1b]52;c;aGVsbG8=\x1b\\");
}
#[test]
fn osc52_write_handles_three_byte_aligned_input_without_padding() {
let sink = ByteSink::default();
let snapshot = sink.clone();
let mut cb = OscClipboard::with_writer(sink);
cb.write_text("foo".into()); let out = String::from_utf8(snapshot.snapshot()).unwrap();
assert_eq!(out, "\x1b]52;c;Zm9v\x1b\\");
}
#[test]
fn osc52_write_handles_two_byte_remainder_with_single_pad() {
let sink = ByteSink::default();
let snapshot = sink.clone();
let mut cb = OscClipboard::with_writer(sink);
cb.write_text("hi".into()); let out = String::from_utf8(snapshot.snapshot()).unwrap();
assert_eq!(out, "\x1b]52;c;aGk=\x1b\\");
}
#[test]
fn osc52_write_handles_empty_string() {
let sink = ByteSink::default();
let snapshot = sink.clone();
let mut cb = OscClipboard::with_writer(sink);
cb.write_text(String::new());
let out = String::from_utf8(snapshot.snapshot()).unwrap();
assert_eq!(out, "\x1b]52;c;\x1b\\");
}
#[test]
fn osc52_write_roundtrips_unicode_via_base64() {
let sink = ByteSink::default();
let snapshot = sink.clone();
let mut cb = OscClipboard::with_writer(sink);
cb.write_text("héllo — 中".into());
let out = String::from_utf8(snapshot.snapshot()).unwrap();
assert!(out.starts_with("\x1b]52;c;"));
assert!(out.ends_with("\x1b\\"));
let payload = &out["\x1b]52;c;".len()..out.len() - "\x1b\\".len()];
assert_eq!(payload, "aMOpbGxvIOKAlCDkuK0=");
}
#[test]
fn osc52_read_returns_none_in_v1() {
let mut cb = OscClipboard::with_writer(ByteSink::default());
assert!(cb.read_text().is_none());
}