use super::*; use crate::client::tests::input::paragraphs; use ely_platform::clipboard::{self, Clip}; use gpui::{InputHandler, Keystroke, TestAppContext, Window}; /// Steps the scheduler so queued input reaches LOK. fn flush(cx: &TestAppContext) { for _ in 0..80 { cx.executor().tick(); } } /// The document's paragraphs, saved through the shared worker. fn saved(view: &Entity, cx: &mut TestAppContext) -> Vec { let out = tempfile::tempdir() .expect("a temp dir") .keep() .join("out.fodt"); cx.update(|app| { let view = view.read(app); let mut worker = view.worker.lock().expect("the worker lock"); view.doc .save_as(&mut worker, &format!("file://{}", out.display()), "fodt") .expect("the save works"); }); paragraphs(&std::fs::read_to_string(&out).expect("the save reads")) } fn typed(key: &str) -> Keystroke { let mut stroke = Keystroke::parse(key).expect("a keystroke"); stroke.key_char = Some(key.to_string()); stroke } #[test] fn typing_and_composing_reach_the_document() { let Some(install) = crate::client::tests::install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let mut cx = TestAppContext::single(); let (view, _) = cx.update(|app| live_view(app, &install)); cx.update(|app| { view.update(app, |view, _| { assert!(!view.key_down(&typed("a")), "text waits for the IME"); view.commit(None, "ab"); view.compose(None, "n", None); view.compose(None, "ni", None); view.compose(None, "你", None); assert_eq!(view.marked.as_ref().map(|m| m.text.as_str()), Some("你")); view.commit(None, "你"); assert!(view.marked.is_none()); view.compose(None, "x", None); view.compose(None, "", None); assert!(view.marked.is_none(), "an empty composition cancels"); assert!(view.key_down(&Keystroke::parse("enter").expect("enter"))); }) }); flush(&cx); assert_eq!(saved(&view, &mut cx), ["ab你", "Hello 世界"]); } struct Blank; impl gpui::Render for Blank { fn render(&mut self, _: &mut Window, _: &mut gpui::Context) -> impl gpui::IntoElement { gpui::Empty } } #[test] fn the_platform_ime_callbacks_compose_in_utf16() { let Some(install) = crate::client::tests::install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let mut cx = TestAppContext::single(); let (view, _) = cx.update(|app| live_view(app, &install)); let window = cx.add_window(|_, _| Blank); cx.update_window(window.into(), |_, window, app| { let bounds = Bounds::default(); let mut ime = gpui::ElementInputHandler::new(bounds, view.clone()); ime.replace_and_mark_text_in_range(None, "nihao", Some(2..2), window, app); let selected = ime .selected_text_range(false, window, app) .expect("a selection"); assert_eq!(selected.range, 2..2, "the IME caret sits inside"); let mut adjusted = None; let head = ime.text_for_range(0..2, &mut adjusted, window, app); assert_eq!(head.as_deref(), Some("ni")); assert_eq!(adjusted, Some(0..2)); assert_eq!(ime.marked_text_range(window, app), Some(0..5)); ime.replace_and_mark_text_in_range(Some(0..2), "你", Some(1..1), window, app); let marked = view.read(app).marked.as_ref().map(|m| m.text.clone()); assert_eq!(marked.as_deref(), Some("你hao")); let selected = ime.selected_text_range(false, window, app).map(|s| s.range); assert_eq!(selected, Some(1..1)); ime.replace_text_in_range(Some(1..4), "好", window, app); assert_eq!( ime.marked_text_range(window, app), None, "the commit ends it" ); ime.replace_and_mark_text_in_range(None, "x", None, window, app); ime.unmark_text(window, app); }) .expect("the window lives"); flush(&cx); assert_eq!(saved(&view, &mut cx), ["你好xHello 世界"]); } #[test] fn a_cut_keeps_the_text_when_the_clipboard_fails() { let Some(install) = crate::client::tests::install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let mut cx = TestAppContext::single(); let (view, shared) = cx.update(|app| live_view(app, &install)); let doc = cx.update(|app| view.read(app).doc.clone()); { let mut worker = shared.lock().expect("the worker lock"); worker .run(&doc, "edit.select_all", &[]) .expect("select all"); let failed = crate::view::clip::copy_out(&mut worker, &doc, true, |_| { Err(std::io::Error::other("the clipboard refused")) }); assert!(failed.is_err(), "the write failure surfaces"); assert!( !worker.copy(&doc).expect("copy runs").is_empty(), "the selection stays" ); } assert_eq!(saved(&view, &mut cx), ["Hello 世界"], "the text stays"); } #[test] fn a_drag_selects_and_events_carry_the_cursor() { let Some(install) = crate::client::tests::install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let cx = TestAppContext::single(); let (view, _) = cx.update(|app| live_view(app, &install)); cx.update(|app| { view.update(app, |view, _| { let none = gpui::Modifiers::default(); let y = px(view.geom.px_of(1500)); view.mouse(0, point(px(view.geom.px_of(1500)), y), 1, 1, &none); view.mouse(2, point(px(view.geom.px_of(4000)), y), 1, 1, &none); view.mouse(1, point(px(view.geom.px_of(4000)), y), 1, 1, &none); }) }); let mut selected = false; for _ in 0..40 { flush(&cx); selected = cx.update(|app| !view.read(app).selection.is_empty()); if selected { break; } } assert!(selected, "the drag selection reaches the view"); cx.update(|app| { let view = view.read(app); let bounds = view.cursor_bounds().expect("the cursor arrived"); assert!(bounds.size.height > px(0.0), "the caret has height"); }); } #[test] fn a_dead_document_surfaces_an_error() { let Some(install) = crate::client::tests::install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let cx = TestAppContext::single(); let (view, shared) = cx.update(|app| live_view(app, &install)); let errors = std::rc::Rc::new(std::cell::RefCell::new(Vec::new())); cx.update(|app| { let errors = errors.clone(); app.subscribe(&view, move |_, error: &LokError, _| { errors.borrow_mut().push(error.0.clone()) }) .detach(); let doc = view.read(app).doc.clone(); shared .lock() .expect("the worker lock") .close_doc(doc) .expect("the doc closes"); view.update(app, |view, _| { view.key_down(&Keystroke::parse("enter").expect("enter")); }); }); flush(&cx); let errors = errors.borrow(); assert!( errors .iter() .any(|error| error.contains("no such document")), "the failure reaches subscribers: {errors:?}" ); } #[test] fn the_clipboard_moves_html_tables_and_images() { let Some(install) = crate::client::tests::install() else { eprintln!("SKIP: no pinned LOK build"); return; }; if std::env::var_os("ELY_CLIPBOARD_TEST").is_none() { eprintln!("SKIP: set ELY_CLIPBOARD_TEST=1; this replaces the clipboard"); return; } let mut cx = TestAppContext::single(); let (view, _) = cx.update(|app| live_view(app, &install)); let primary = if cfg!(target_os = "macos") { "cmd-a" } else { "ctrl-a" }; cx.update(|app| { view.update(app, |view, _| { view.key_down(&Keystroke::parse(primary).expect("select all")); view.send(crate::view::input::Input::Copy { cut: false }); }) }); flush(&cx); let Some(Clip::Html { html, text }) = clipboard::read().expect("the clipboard reads") else { panic!("the copy lands as html"); }; assert!( html.contains("Hello") && text == "Hello 世界", "{html} / {text}" ); clipboard::write(&Clip::Html { html: "
leftright
".into(), text: "left\tright".into(), }) .expect("the table writes"); cx.update(|app| view.update(app, |view, _| view.send(crate::view::input::Input::Paste))); flush(&cx); let text = saved(&view, &mut cx); assert!( text.contains(&"left".to_string()) && text.contains(&"right".to_string()), "{text:?}" ); let mut png = Vec::new(); image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 0, 0, 255])) .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) .expect("the png encodes"); clipboard::write(&Clip::Png(png)).expect("the image writes"); cx.update(|app| view.update(app, |view, _| view.send(crate::view::input::Input::Paste))); flush(&cx); let out = tempfile::tempdir() .expect("a temp dir") .keep() .join("img.fodt"); cx.update(|app| { let view = view.read(app); let mut worker = view.worker.lock().expect("the worker lock"); view.doc .save_as(&mut worker, &format!("file://{}", out.display()), "fodt") .expect("the save works"); }); let xml = std::fs::read_to_string(&out).expect("the save reads"); assert!(xml.contains("