From c88164f2fa8325f9199df5868bc88c3401f364da Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 16 Feb 2023 11:54:53 +0800 Subject: [PATCH] feat(tree-view): add unit tests --- Cargo.lock | 44 ++ changes | 17 +- helix-term/Cargo.toml | 1 + helix-term/src/ui/explore.rs | 8 +- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/tree.rs | 755 +++++++++++++++++++++++++++-------- 6 files changed, 655 insertions(+), 172 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b70f34c4..4d722d6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "cxx" version = "1.0.82" @@ -342,6 +352,12 @@ dependencies = [ "parking_lot_core 0.9.4", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dirs" version = "4.0.0" @@ -1217,6 +1233,7 @@ dependencies = [ "indoc", "log", "once_cell", + "pretty_assertions", "pulldown-cmark", "serde", "serde_json", @@ -1609,6 +1626,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1675,6 +1701,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pretty_assertions" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +dependencies = [ + "ctor", + "diff", + "output_vt100", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.47" @@ -2553,3 +2591,9 @@ dependencies = [ "helix-view", "toml", ] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/changes b/changes index a49fafae..a436eafd 100644 --- a/changes +++ b/changes @@ -23,9 +23,18 @@ TODO - [x] bug: "h" does not realign preview - [x] bug: reveal file does not realign preview - [] "l" goes back to previous child if any history -- [] refactor, add tree.expand_children() method +- [x] Merge conflicts +- [x] Remove comments +- [x] fix warnings +- [x] refactor, add tree.expand_children() method +- [] add integration testing (test explorer rendering) - [] search highlight matching word -- [] fix warnings - [] Error didn't clear -- [] Remove comments -- [] Merge conflicts +- [] bind "o" to open/close file/folder +- [] on focus indication +- [] should preview be there by default? +- [] support creating files and folder and the same time (`mkdir -p`) +- [] Fix panic bugs (see github comments) +- [] Sticky ancestors +- [] Ctrl-o should work for 'h', 'gg', 'ge', etc +- [] explorer(preview): content not sorted diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 603f37d3..4204e4dc 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -80,3 +80,4 @@ helix-loader = { version = "0.6", path = "../helix-loader" } smallvec = "1.10" indoc = "2.0.0" tempfile = "3.3.0" +pretty_assertions = "1.3.0" diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index af431e23..b7691522 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -1,4 +1,4 @@ -use super::{Prompt, TreeItem, TreeOp, TreeView}; +use super::{Prompt, TreeOp, TreeView, TreeViewItem}; use crate::{ compositor::{Component, Context, EventResult}, ctrl, key, shift, ui, @@ -61,13 +61,9 @@ impl FileInfo { } } -impl TreeItem for FileInfo { +impl TreeViewItem for FileInfo { type Params = State; - fn is_child(&self, other: &Self) -> bool { - self.path.parent().map_or(false, |p| p == other.path) - } - fn cmp(&self, other: &Self) -> Ordering { use FileType::*; match (self.file_type, other.file_type) { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3f962dd5..0dba41e1 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -29,7 +29,7 @@ pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; -pub use tree::{TreeItem, TreeOp, TreeView}; +pub use tree::{TreeViewItem, TreeOp, TreeView}; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index acae40db..8dcb197f 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -14,12 +14,11 @@ use helix_view::{ }; use tui::buffer::Buffer as Surface; -pub trait TreeItem: Sized { +pub trait TreeViewItem: Sized { type Params; // fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; fn name(&self) -> String; - fn is_child(&self, other: &Self) -> bool; fn is_parent(&self) -> bool; fn cmp(&self, other: &Self) -> Ordering; @@ -27,23 +26,14 @@ pub trait TreeItem: Sized { self.name().to_lowercase().contains(&s.to_lowercase()) } - fn get_children(&self) -> Result> { - Ok(vec![]) - } + fn get_children(&self) -> Result>; } -fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { - if item1.is_child(item2) { - return Ordering::Greater; - } - if item2.is_child(item1) { - return Ordering::Less; - } - +fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { T::cmp(item1, item2) } -fn vec_to_tree(mut items: Vec) -> Vec> { +pub fn vec_to_tree(mut items: Vec) -> Vec> { items.sort_by(tree_item_cmp); index_elems( 0, @@ -129,7 +119,7 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} -impl Tree { +impl Tree { fn open(&mut self, filter: &String) -> Result<()> { if self.item.is_parent() { self.children = self.get_filtered_children(filter)?; @@ -295,7 +285,7 @@ impl Tree { } } -pub struct TreeView { +pub struct TreeView { tree: Tree, /// Selected item idex selected: usize, @@ -306,14 +296,12 @@ pub struct TreeView { /// View row winline: usize, - area_height: usize, - col: usize, + previous_area: Rect, + column: usize, max_len: usize, count: usize, tree_symbol_style: String, #[allow(clippy::type_complexity)] - pre_render: Option>, - #[allow(clippy::type_complexity)] on_opened_fn: Option TreeOp + 'static>>, #[allow(clippy::type_complexity)] on_folded_fn: Option>, @@ -321,19 +309,18 @@ pub struct TreeView { on_next_key: Option>, } -impl TreeView { +impl TreeView { pub fn new(root: T, items: Vec>) -> Self { Self { tree: Tree::new(root, items), selected: 0, save_view: (0, 0), winline: 0, - col: 0, + column: 0, max_len: 0, count: 0, - area_height: 0, + previous_area: Rect::new(0, 0, 0, 0), tree_symbol_style: "ui.text".into(), - pre_render: None, on_opened_fn: None, on_folded_fn: None, on_next_key: None, @@ -422,7 +409,7 @@ impl TreeView { } fn align_view_center(&mut self) { - self.winline = self.area_height / 2 + self.winline = self.previous_area.height as usize / 2 } fn align_view_top(&mut self) { @@ -430,7 +417,7 @@ impl TreeView { } fn align_view_bottom(&mut self) { - self.winline = self.area_height + self.winline = self.previous_area.height as usize } fn regenerate_index(&mut self) { @@ -447,12 +434,12 @@ impl TreeView { fn go_to_children(&mut self, filter: &String) -> Result<()> { let current = self.current_mut(); if current.is_opened { - self.selected += 1; + self.set_selected(self.selected + 1); Ok(()) } else { current.open(filter)?; if !current.children.is_empty() { - self.selected += 1; + self.set_selected(self.selected + 1); self.regenerate_index(); } Ok(()) @@ -462,19 +449,33 @@ impl TreeView { pub fn refresh(&mut self, filter: &String) -> Result<()> { self.tree.refresh(filter) } + + fn go_to_first(&mut self) { + self.move_up(usize::MAX / 2) + } + + fn go_to_last(&mut self) { + self.move_down(usize::MAX / 2) + } + + fn set_previous_area(&mut self, area: Rect) { + self.previous_area = area + } } pub fn tree_view_help() -> Vec { vec![ - "j Down", - "k Up", - "h Go to parent", - "l Expand", + "j/↓ Down", + "k/↑ Up", + "h/← Go to parent", + "l/→ Expand", + "L Scroll right", + "H Scroll left", "zz Align view center", "zt Align view top", "zb Align view bottom", - "gg Go to top", - "ge Go to end", + "gg Go to first", + "ge Go to last", "^d Page down", "^u Page up", ] @@ -483,7 +484,7 @@ pub fn tree_view_help() -> Vec { .collect() } -impl TreeView { +impl TreeView { pub fn on_enter( &mut self, cx: &mut Context, @@ -570,38 +571,30 @@ impl TreeView { } pub fn move_left(&mut self, cols: usize) { - self.col = self.col.saturating_sub(cols); + self.column = self.column.saturating_sub(cols); } pub fn move_right(&mut self, cols: usize) { - self.pre_render = Some(Box::new(move |tree: &mut Self, area: Rect| { - let max_scroll = tree.max_len.saturating_sub(area.width as usize); - tree.col = max_scroll.min(tree.col + cols); - })); + let max_scroll = self + .max_len + .saturating_sub(self.previous_area.width as usize); + self.column = max_scroll.min(self.column + cols); } pub fn move_down_half_page(&mut self) { - self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| { - tree.move_down((area.height / 2) as usize); - })); + self.move_down(self.previous_area.height as usize / 2) } pub fn move_up_half_page(&mut self) { - self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| { - tree.move_up((area.height / 2) as usize); - })); + self.move_up(self.previous_area.height as usize / 2); } pub fn move_down_page(&mut self) { - self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| { - tree.move_down((area.height) as usize); - })); + self.move_down(self.previous_area.height as usize); } pub fn move_up_page(&mut self) { - self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| { - tree.move_up((area.height) as usize); - })); + self.move_up(self.previous_area.height as usize); } pub fn save_view(&mut self) { @@ -686,129 +679,108 @@ impl TreeView { } } -impl TreeView { - pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { - if let Some(pre_render) = self.pre_render.take() { - pre_render(self, area); - } +struct RenderedLine { + indent: String, + name: String, + selected: bool, + descendant_selected: bool, +} +struct RenderTreeParams<'a, T> { + tree: &'a Tree, + prefix: &'a String, + level: usize, + selected: usize, + filter: &'a str, + column_start: usize, + max_width: usize, +} + +fn render_tree( + RenderTreeParams { + tree, + prefix, + level, + selected, + filter, + column_start, + max_width, + }: RenderTreeParams, +) -> Vec { + let indent = if level > 0 { + let indicator = if tree.item().is_parent() { + if tree.is_opened { + "" + } else { + "" + } + } else { + " " + }; + format!("{}{} ", prefix, indicator) + } else { + "".to_string() + }; + let indent = indent[column_start..].to_string(); + let indent_len = indent.len(); + let name = tree.item.name(); + println!("{max_width}"); + let head = RenderedLine { + indent, + selected: selected == tree.index, + descendant_selected: selected != tree.index && tree.get(selected).is_some(), + name: name[..(max_width - indent_len).clamp(0, name.len())].to_string(), + }; + let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " }); + vec![head] + .into_iter() + .chain(tree.children.iter().flat_map(|elem| { + render_tree(RenderTreeParams { + tree: elem, + prefix: &prefix, + level: level + 1, + selected, + filter, + column_start, + max_width, + }) + })) + .collect() +} +impl TreeView { + pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { self.max_len = 0; - self.area_height = area.height.saturating_sub(1) as usize; - self.winline = self.winline.min(self.area_height); let style = cx.editor.theme.get(&self.tree_symbol_style); let ancestor_style = cx.editor.theme.get("ui.text.focus"); - let skip = self.selected.saturating_sub(self.winline); - let params = RenderElemParams { - tree: &self.tree, - prefix: &"".to_string(), - level: 0, - selected: self.selected, - filter, - }; - - let rendered = render_tree(params); - - let iter = rendered - .iter() - .skip(skip) - .take(area.height as usize) - .enumerate(); + let iter = self.render_lines(area, filter).into_iter().enumerate(); - struct Indent(String); - struct Node { - name: String, - selected: bool, - descendant_selected: bool, - } - - struct RenderElemParams<'a, T> { - tree: &'a Tree, - prefix: &'a String, - level: usize, - selected: usize, - filter: &'a str, - } - - fn render_tree( - RenderElemParams { - tree, - prefix, - level, - selected, - filter, - }: RenderElemParams, - ) -> Vec<(Indent, Node)> { - let indent = if level > 0 { - let indicator = if tree.item().is_parent() { - if tree.is_opened { - "" - } else { - "" - } - } else { - " " - }; - format!("{}{}", prefix, indicator) - } else { - "".to_string() - }; - let head = ( - Indent(indent), - Node { - selected: selected == tree.index, - descendant_selected: selected != tree.index && tree.get(selected).is_some(), - name: format!( - "{}{}", - tree.item.name(), - if tree.item.is_parent() { - format!("{}", std::path::MAIN_SEPARATOR) - } else { - "".to_string() - } - ), - }, - ); - let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " }); - vec![head] - .into_iter() - .chain(tree.children.iter().flat_map(|elem| { - render_tree(RenderElemParams { - tree: elem, - prefix: &prefix, - level: level + 1, - selected, - filter, - }) - })) - .collect() - } - - for (index, (indent, node)) in iter { + for (index, line) in iter { let area = Rect::new(area.x, area.y + index as u16, area.width, 1); - let indent_len = indent.0.chars().count() as u16; - surface.set_stringn(area.x, area.y, indent.0.clone(), indent_len as usize, style); + let indent_len = line.indent.chars().count() as u16; + surface.set_stringn( + area.x, + area.y, + line.indent.clone(), + indent_len as usize, + style, + ); - let style = if node.selected { + let style = if line.selected { style.add_modifier(Modifier::REVERSED) } else { style }; let x = area.x.saturating_add(indent_len); - let x = if indent_len > 0 { - x.saturating_add(1) - } else { - x - }; surface.set_stringn( x, area.y, - node.name.clone(), + line.name.clone(), area.width .saturating_sub(indent_len) .saturating_sub(1) .into(), - if node.descendant_selected { + if line.descendant_selected { ancestor_style } else { style @@ -817,6 +789,50 @@ impl TreeView { } } + fn render_to_string(&mut self, filter: &String) -> String { + let area = self.previous_area; + let lines = self.render_lines(area, filter); + lines + .into_iter() + .map(|line| { + let name = if line.selected { + format!("({})", line.name) + } else if line.descendant_selected { + format!("[{}]", line.name) + } else { + line.name + }; + format!("{}{}", line.indent, name) + }) + .collect::>() + .join("\n") + } + + fn render_lines(&mut self, area: Rect, filter: &String) -> Vec { + self.previous_area = area; + self.winline = self + .winline + .min(self.previous_area.height.saturating_sub(1) as usize); + let skip = self.selected.saturating_sub(self.winline); + let params = RenderTreeParams { + tree: &self.tree, + prefix: &"".to_string(), + level: 0, + selected: self.selected, + filter, + column_start: self.column, + max_width: self.previous_area.width as usize, + }; + + let lines = render_tree(params); + + lines + .into_iter() + .skip(skip) + .take(area.height as usize) + .collect() + } + pub fn handle_event( &mut self, event: &Event, @@ -836,8 +852,8 @@ impl TreeView { let count = std::mem::replace(&mut self.count, 0); match key_event { key!(i @ '0'..='9') => self.count = i.to_digit(10).unwrap() as usize + count * 10, - key!('k') | shift!(Tab) | key!(Up) | ctrl!('k') => self.move_up(1.max(count)), - key!('j') | key!(Tab) | key!(Down) | ctrl!('j') => self.move_down(1.max(count)), + key!('k') | key!(Up) => self.move_up(1.max(count)), + key!('j') | key!(Down) => self.move_down(1.max(count)), key!('z') => { self.on_next_key = Some(Box::new(|_, tree, event| match event { key!('z') => tree.align_view_center(), @@ -846,18 +862,20 @@ impl TreeView { _ => {} })); } - key!('h') => self.go_to_parent(), - key!('l') => match self.go_to_children(filter) { + key!('h') | key!(Left) => self.go_to_parent(), + key!('l') | key!(Right) => match self.go_to_children(filter) { Ok(_) => {} Err(err) => cx.editor.set_error(err.to_string()), }, + shift!('H') => self.move_left(1), + shift!('L') => self.move_right(1), key!(Enter) => self.on_enter(cx, params, self.selected, filter), ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), key!('g') => { self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('g') => tree.move_up(usize::MAX / 2), - key!('e') => tree.move_down(usize::MAX / 2), + key!('g') => tree.go_to_first(), + key!('e') => tree.go_to_last(), _ => {} })); } @@ -907,6 +925,421 @@ fn index_elems(parent_index: usize, elems: Vec>) -> Vec> { index_elems(parent_index + 1, elems, parent_index).1 } +#[cfg(test)] +mod test_tree_view { + use helix_view::graphics::Rect; + + use super::{vec_to_tree, TreeView, TreeViewItem}; + use pretty_assertions::assert_eq; + + #[derive(Clone)] + struct Item<'a> { + name: &'a str, + } + + fn item<'a>(name: &'a str) -> Item<'a> { + Item { name } + } + + impl<'a> TreeViewItem for Item<'a> { + type Params = (); + + fn name(&self) -> String { + self.name.to_string() + } + + fn is_parent(&self) -> bool { + self.name.len() > 2 + } + + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.name.cmp(other.name) + } + + fn get_children(&self) -> anyhow::Result> { + if self.is_parent() { + let (left, right) = self.name.split_at(self.name.len() / 2); + Ok(vec![item(left), item(right)]) + } else { + Ok(vec![]) + } + } + } + + fn dummy_tree_view<'a>() -> TreeView> { + let root = item("who_lives_in_a_pineapple_under_the_sea"); + let mut view = TreeView::new( + root, + vec_to_tree(vec![ + item("gary_the_snail"), + item("krabby_patty"), + item("larry_the_lobster"), + item("patrick_star"), + item("sandy_cheeks"), + item("spongebob_squarepants"), + item("mrs_puff"), + item("king_neptune"), + item("karen"), + item("plankton"), + ]), + ); + + view.set_previous_area(dummy_area()); + + view + } + + fn dummy_area() -> Rect { + Rect::new(0, 0, 50, 5) + } + + fn render<'a>(view: &mut TreeView>) -> String { + view.render_to_string(&"".to_string()) + } + + #[test] + fn test_init() { + let mut view = dummy_tree_view(); + + // Expect the items to be sorted + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn test_move_up_down() { + let mut view = dummy_tree_view(); + view.move_down(1); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + (gary_the_snail) + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_down(3); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + karen + king_neptune + (krabby_patty) +" + .trim() + ); + + view.move_down(1); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + + view.move_up(1); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + (krabby_patty) + larry_the_lobster +" + .trim() + ); + + view.move_up(3); + assert_eq!( + render(&mut view), + " + (gary_the_snail) + karen + king_neptune + krabby_patty + larry_the_lobster +" + .trim() + ); + + view.move_up(1); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn test_align_view() { + let mut view = dummy_tree_view(); + view.move_down(5); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + + view.align_view_center(); + assert_eq!( + render(&mut view), + " + king_neptune + krabby_patty + (larry_the_lobster) + mrs_puff + patrick_star +" + .trim() + ); + + view.align_view_bottom(); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + } + + #[test] + fn test_go_to_first_last() { + let mut view = dummy_tree_view(); + + view.go_to_last(); + assert_eq!( + render(&mut view), + " + mrs_puff + patrick_star + plankton + sandy_cheeks + (spongebob_squarepants) +" + .trim() + ); + + view.go_to_first(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn test_move_half() { + let mut view = dummy_tree_view(); + view.move_down_half_page(); + assert_eq!(view.selected, 2); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + (karen) + king_neptune + krabby_patty +" + .trim() + ); + + view.move_down_half_page(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + karen + king_neptune + (krabby_patty) +" + .trim() + ); + + view.move_down_half_page(); + assert_eq!( + render(&mut view), + " + karen + king_neptune + krabby_patty + larry_the_lobster + (mrs_puff) +" + .trim() + ); + + view.move_up_half_page(); + assert_eq!( + render(&mut view), + " + karen + king_neptune + (krabby_patty) + larry_the_lobster + mrs_puff +" + .trim() + ); + + view.move_up_half_page(); + assert_eq!( + render(&mut view), + " + (karen) + king_neptune + krabby_patty + larry_the_lobster + mrs_puff +" + .trim() + ); + + view.move_up_half_page(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn go_to_children_parent() { + let filter = "".to_string(); + let mut view = dummy_tree_view(); + view.move_down(1); + view.go_to_children(&filter).unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + [gary_the_snail] +  (e_snail) +  gary_th + karen + " + .trim() + ); + + view.move_down(1); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + [gary_the_snail] +  e_snail +  (gary_th) + karen + " + .trim() + ); + + view.go_to_parent(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + (gary_the_snail) +  e_snail +  gary_th + karen + " + .trim() + ); + + view.go_to_last(); + view.go_to_parent(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail +  e_snail +  gary_th + karen + " + .trim() + ); + } + + #[test] + fn test_move_left_right() { + let mut view = dummy_tree_view(); + view.set_previous_area(dummy_area().with_width(20)); + + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pinea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_right(1); + assert_eq!( + render(&mut view), + " +" + .trim() + ) + } +} + #[cfg(test)] mod test_tree { use helix_core::movement::Direction;