use super::*;
use crate::base::Size;
use crate::theme::default_theme;
use crate::ui::UiTree;
use crate::widgets::itest_util::{click, key, mount_widget, render, settle};
use std::cell::Cell;
const W: i32 = 40;
const H: i32 = 16;
#[allow(clippy::type_complexity)]
fn dock(
size: Size,
) -> (
crate::reactive::RootScope,
UiTree,
Signal<Option<String>>,
Rc<Cell<u32>>,
Rc<Cell<u32>>,
) {
let files_builds: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let chat_builds: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let fb = files_builds.clone();
let cb = chat_builds.clone();
let mut open_probe = None;
let (root, tree) = mount_widget(size, |cx| {
let t = default_theme().tokens;
let open = cx.signal(None);
open_probe = Some(open);
DrawerDock::new(crate::ui::text("main content pane"))
.drawer("files", "Files", move |_cx| {
fb.set(fb.get() + 1);
crate::ui::text("file list body")
})
.drawer("chat", "Chat", move |_cx| {
cb.set(cb.get() + 1);
crate::ui::text("chat body")
})
.open(open)
.panel_width(20)
.element(cx, &t)
.build()
});
let open = open_probe.unwrap();
(root, tree, open, files_builds, chat_builds)
}
fn row(tree: &mut UiTree, size: Size, y: i32) -> String {
render(tree, size).row_text(y).trim_end().to_string()
}
fn rail_column(tree: &mut UiTree, size: Size, x: i32) -> String {
let canvas = render(tree, size);
(0..size.h)
.filter_map(|y| canvas.row_text(y).chars().nth(x as usize))
.filter(|c| !c.is_whitespace())
.collect()
}
#[test]
fn collapsed_is_content_plus_rail_only() {
let size = Size::new(W, H);
let (root, mut tree, open, files, chat) = dock(size);
assert_eq!(open.get_untracked(), None, "starts collapsed");
assert!(row(&mut tree, size, 0).contains("main content pane"));
assert_eq!(rail_column(&mut tree, size, W - 2), "FilesChat");
let canvas = render(&mut tree, size);
let all: String = (0..H).map(|y| canvas.row_text(y)).collect();
assert!(!all.contains("file list body"));
assert_eq!(
(files.get(), chat.get()),
(0, 0),
"collapsed builds nothing"
);
root.dispose();
}
#[test]
fn tab_click_opens_active_click_collapses() {
let size = Size::new(W, H);
let (root, mut tree, open, files, _chat) = dock(size);
click(&mut tree, W - 2, 1);
let _ = settle(&mut tree, size);
assert_eq!(open.get_untracked().as_deref(), Some("files"));
let canvas = render(&mut tree, size);
let all: String = (0..H).map(|y| canvas.row_text(y)).collect();
assert!(all.contains("Files"), "panel header shows the title");
assert!(all.contains("file list body"), "panel body mounted");
assert!(all.contains(CLOSE_GLYPH), "close affordance visible");
assert_eq!(files.get(), 1);
click(&mut tree, W - 2, 1);
let _ = settle(&mut tree, size);
assert_eq!(open.get_untracked(), None);
let canvas = render(&mut tree, size);
let all: String = (0..H).map(|y| canvas.row_text(y)).collect();
assert!(
!all.contains("file list body"),
"collapsed removes the panel"
);
root.dispose();
}
#[test]
fn switching_tabs_replaces_the_open_panel() {
let size = Size::new(W, H);
let (root, mut tree, open, files, chat) = dock(size);
click(&mut tree, W - 2, 1); let _ = settle(&mut tree, size);
click(&mut tree, W - 2, 8);
let _ = settle(&mut tree, size);
assert_eq!(open.get_untracked().as_deref(), Some("chat"));
let canvas = render(&mut tree, size);
let all: String = (0..H).map(|y| canvas.row_text(y)).collect();
assert!(all.contains("chat body"));
assert!(!all.contains("file list body"), "one panel at a time");
assert_eq!((files.get(), chat.get()), (1, 1));
root.dispose();
}
#[test]
fn close_corner_and_esc_both_collapse() {
let size = Size::new(W, H);
let (root, mut tree, open, _files, _chat) = dock(size);
click(&mut tree, W - 2, 1);
let _ = settle(&mut tree, size);
assert!(open.get_untracked().is_some());
click(&mut tree, W - RAIL_W - 2, 0);
let _ = settle(&mut tree, size);
assert_eq!(open.get_untracked(), None, "close corner collapses");
click(&mut tree, W - 2, 1);
let _ = settle(&mut tree, size);
assert!(open.get_untracked().is_some());
key(&mut tree, Key::Escape);
let _ = settle(&mut tree, size);
assert!(
open.get_untracked().is_some(),
"root-targeted Esc is not the panel's"
);
root.dispose();
}
#[test]
fn closed_drawer_is_disposed_and_rebuilt_fresh() {
let size = Size::new(W, H);
let (root, mut tree, _open, files, _chat) = dock(size);
for _ in 0..3 {
click(&mut tree, W - 2, 1);
let _ = settle(&mut tree, size);
let _ = render(&mut tree, size);
click(&mut tree, W - 2, 1);
let _ = settle(&mut tree, size);
let _ = render(&mut tree, size);
}
assert_eq!(
files.get(),
3,
"one fresh build per open, none while closed"
);
root.dispose();
}
#[test]
fn external_write_drives_the_dock_without_on_change() {
let size = Size::new(W, H);
let fired: Rc<RefCell<Vec<Option<String>>>> = Rc::default();
let f2 = fired.clone();
let mut open_probe = None;
let (root, mut tree) = mount_widget(size, |cx| {
let t = default_theme().tokens;
let open = cx.signal(None);
open_probe = Some(open);
DrawerDock::new(crate::ui::text("content"))
.drawer("files", "Files", |_cx| crate::ui::text("files body"))
.open(open)
.on_change(move |id| f2.borrow_mut().push(id.map(str::to_string)))
.element(cx, &t)
.build()
});
let open = open_probe.unwrap();
open.set(Some("files".into()));
let _ = settle(&mut tree, size);
let canvas = render(&mut tree, size);
let all: String = (0..H).map(|y| canvas.row_text(y)).collect();
assert!(all.contains("files body"), "external write opens the panel");
assert!(
fired.borrow().is_empty(),
"external writes never fire on_change"
);
click(&mut tree, W - 2, 1);
let _ = settle(&mut tree, size);
assert_eq!(fired.borrow().as_slice(), &[None]);
root.dispose();
}
#[test]
fn badge_dot_tracks_its_signal() {
let size = Size::new(W, H);
let mut badge_probe = None;
let (root, mut tree) = mount_widget(size, |cx| {
let t = default_theme().tokens;
let waiting = cx.signal(false);
badge_probe = Some(waiting);
DrawerDock::new(crate::ui::text("content"))
.drawer("desk", "Desk", |_cx| crate::ui::text("desk body"))
.drawer_badge(move || waiting.get())
.element(cx, &t)
.build()
});
let waiting = badge_probe.unwrap();
assert!(
!rail_column(&mut tree, size, W - 2).contains('●'),
"no dot while quiet"
);
waiting.set(true);
let _ = settle(&mut tree, size);
assert!(
rail_column(&mut tree, size, W - 2).contains('●'),
"the badge dot appears under the label"
);
root.dispose();
}
#[test]
fn esc_collapses_while_focus_is_inside_the_panel() {
let size = Size::new(W, H);
let mut open_probe = None;
let (root, mut tree) = mount_widget(size, |cx| {
let t = default_theme().tokens;
let open = cx.signal(None);
open_probe = Some(open);
let value = cx.signal(String::new());
DrawerDock::new(crate::ui::text("content"))
.drawer("search", "Search", move |gcx| {
crate::widgets::TextInput::new()
.value(value)
.element(gcx, &default_theme().tokens)
.build()
})
.open(open)
.panel_width(20)
.element(cx, &t)
.build()
});
let open = open_probe.unwrap();
click(&mut tree, W - 2, 1); let _ = settle(&mut tree, size);
assert!(open.get_untracked().is_some());
click(&mut tree, W - 10, 2);
let _ = settle(&mut tree, size);
key(&mut tree, Key::Escape);
let _ = settle(&mut tree, size);
assert_eq!(
open.get_untracked(),
None,
"Esc with focus inside the panel collapses the dock"
);
root.dispose();
}
#[test]
#[should_panic(expected = "wrote `open` while its")]
#[cfg(debug_assertions)]
fn builder_writing_open_is_a_loud_bug() {
let size = Size::new(W, H);
let mut open_probe = None;
let (_root, mut tree) = mount_widget(size, |cx| {
let t = default_theme().tokens;
let open = cx.signal(None);
open_probe = Some(open);
DrawerDock::new(crate::ui::text("content"))
.drawer("a", "Aaa", move |_cx| {
open.set(Some("b".into())); crate::ui::text("a body")
})
.drawer("b", "Bbb", |_cx| crate::ui::text("b body"))
.open(open)
.element(cx, &t)
.build()
});
let open = open_probe.unwrap();
open.set(Some("a".into()));
let _ = settle(&mut tree, size);
}