384 lines
12 KiB
Rust
384 lines
12 KiB
Rust
use gpui::{
|
|
AppContext as _, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
|
TestAppContext, VisualTestContext, Window, div, px,
|
|
};
|
|
use jiff::{SignedDuration, Timestamp, tz::TimeZone};
|
|
|
|
use super::{press, settle, setup, then};
|
|
use crate::{
|
|
files::{
|
|
ArchiveEntry, ArchiveViewer, DuplicateFinder, Duplicates, FilePreview, FileSearch,
|
|
FoundFile, RecentFile, RecentFiles, archive::saving, duplicates::freed, preview::kind,
|
|
recent::day_label,
|
|
},
|
|
forms::TextInput,
|
|
lists::DirEntry,
|
|
};
|
|
|
|
/// Moves focus to the `nth` Tab stop from none.
|
|
fn tab_to(nth: usize, cx: &mut VisualTestContext) {
|
|
cx.update(|window, cx| {
|
|
window.blur(cx);
|
|
for _ in 0..nth {
|
|
window.focus_next(cx);
|
|
}
|
|
});
|
|
settle(cx);
|
|
}
|
|
|
|
#[test]
|
|
fn a_kind_reads_from_the_extension() {
|
|
assert_eq!(kind(&DirEntry::file("Brief.md", 1, then())), "MD");
|
|
assert_eq!(kind(&DirEntry::file("Makefile", 1, then())), "File");
|
|
assert_eq!(
|
|
kind(&DirEntry::file(".env", 1, then())),
|
|
"ENV",
|
|
"as the icon map reads it"
|
|
);
|
|
assert_eq!(kind(&DirEntry::folder("Design", then())), "Folder");
|
|
}
|
|
|
|
#[test]
|
|
fn a_day_reads_as_today_yesterday_or_its_date() {
|
|
let today = jiff::civil::date(2026, 9, 22);
|
|
assert_eq!(day_label(today, today), "Today");
|
|
assert_eq!(
|
|
day_label(jiff::civil::date(2026, 9, 21), today),
|
|
"Yesterday"
|
|
);
|
|
assert_eq!(
|
|
day_label(jiff::civil::date(2026, 9, 20), today),
|
|
"Sunday, September 20"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn packing_saves_a_whole_percent_and_marks_free_their_bytes() {
|
|
assert_eq!(
|
|
(saving(1000, 250), saving(0, 0), saving(10, 20)),
|
|
(75, 0, 0)
|
|
);
|
|
let groups = [Duplicates {
|
|
name: "a.jpg".into(),
|
|
size: 100,
|
|
paths: vec!["x/a.jpg".into(), "y/a.jpg".into(), "z/a.jpg".into()],
|
|
}];
|
|
assert_eq!(freed(&groups, &["y/a.jpg".into(), "z/a.jpg".into()]), 200);
|
|
}
|
|
|
|
/// A preview of a picture four wide over three, 400 across.
|
|
struct Looking {
|
|
path: std::path::PathBuf,
|
|
}
|
|
|
|
impl Render for Looking {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
let entry = DirEntry::file("a.png", 10, then());
|
|
let preview = FilePreview::new("preview", entry).picture(self.path.clone(), 4.0 / 3.0);
|
|
div().w(px(400.0)).child(preview)
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_picture_keeps_the_hosts_shape(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let path = std::env::temp_dir().join("ely-files-preview.png");
|
|
image::RgbaImage::from_pixel(40, 30, image::Rgba([90, 120, 150, 255]))
|
|
.save(&path)
|
|
.expect("the picture writes");
|
|
let (_, cx) = cx.add_window_view(|_, _| Looking { path });
|
|
settle(cx);
|
|
let stage = cx
|
|
.debug_bounds("file-preview-stage")
|
|
.expect("the stage draws");
|
|
let shape = stage.size.width / stage.size.height;
|
|
assert!((shape - 4.0 / 3.0).abs() < 0.02, "{stage:?}");
|
|
}
|
|
|
|
/// Files opened today and yesterday in UTC, and the ones it opened.
|
|
struct Recalling {
|
|
opened: Vec<SharedString>,
|
|
}
|
|
|
|
impl Render for Recalling {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let owner = cx.entity();
|
|
let midnight = Timestamp::now()
|
|
.to_zoned(TimeZone::UTC)
|
|
.start_of_day()
|
|
.expect("a day starts")
|
|
.timestamp();
|
|
let file = |name: &str, minutes: i64| RecentFile {
|
|
name: SharedString::from(name.to_string()),
|
|
folder: "Home".into(),
|
|
opened: midnight + SignedDuration::from_mins(minutes),
|
|
};
|
|
let files = [
|
|
file("early.md", 5),
|
|
file("late.md", -300),
|
|
file("later.md", 9),
|
|
];
|
|
div().w(px(320.0)).child(
|
|
RecentFiles::new("recent", files)
|
|
.zone(TimeZone::UTC)
|
|
.on_open(move |file, _, cx| {
|
|
owner.update(cx, |host, _| host.opened.push(file.name.clone()))
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn days_list_the_newest_first_and_enter_opens(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let (host, cx) = cx.add_window_view(|_, _| Recalling { opened: Vec::new() });
|
|
cx.update(|window, _| window.activate_window());
|
|
settle(cx);
|
|
tab_to(1, cx);
|
|
press("enter", cx);
|
|
tab_to(2, cx);
|
|
press("enter", cx);
|
|
assert_eq!(
|
|
host.read_with(cx, |host, _| host.opened.clone()),
|
|
["later.md", "late.md"]
|
|
);
|
|
}
|
|
|
|
/// A file search over a field it owns, finding what the test says, and what it opened.
|
|
struct Seeking {
|
|
query: Entity<TextInput>,
|
|
found: Vec<FoundFile>,
|
|
opened: Vec<SharedString>,
|
|
}
|
|
|
|
impl Render for Seeking {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let owner = cx.entity();
|
|
div().w(px(320.0)).child(
|
|
FileSearch::new("search", &self.query, self.found.clone()).on_open(
|
|
move |file, _, cx| owner.update(cx, |host, _| host.opened.push(file.name.clone())),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn seeking<'a>(
|
|
text: &str,
|
|
found: Vec<FoundFile>,
|
|
cx: &'a mut TestAppContext,
|
|
) -> (Entity<Seeking>, &'a mut VisualTestContext) {
|
|
setup(cx);
|
|
let text = text.to_string();
|
|
let (host, cx) = cx.add_window_view(move |window, cx| {
|
|
let query = cx.new(|cx| {
|
|
let mut field = TextInput::new(window, cx);
|
|
field.set_text(text, cx);
|
|
field
|
|
});
|
|
Seeking {
|
|
query,
|
|
found,
|
|
opened: Vec::new(),
|
|
}
|
|
});
|
|
cx.update(|window, _| window.activate_window());
|
|
settle(cx);
|
|
(host, cx)
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_search_with_nothing_found_says_so(cx: &mut TestAppContext) {
|
|
let (_, cx) = seeking("zebra", Vec::new(), cx);
|
|
assert!(cx.debug_bounds("file-search-note").is_some());
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_found_file_opens_on_enter(cx: &mut TestAppContext) {
|
|
let found = vec![FoundFile {
|
|
name: "Brief.md".into(),
|
|
folder: "Home/Atrium".into(),
|
|
}];
|
|
let (host, cx) = seeking("brief", found, cx);
|
|
tab_to(3, cx);
|
|
press("enter", cx);
|
|
assert_eq!(
|
|
host.read_with(cx, |host, _| host.opened.clone()),
|
|
["Brief.md"]
|
|
);
|
|
}
|
|
|
|
/// Two copies of a file with the second marked, and what it was told.
|
|
struct Sparing {
|
|
removed: Vec<SharedString>,
|
|
marks: Vec<(SharedString, bool)>,
|
|
}
|
|
|
|
impl Render for Sparing {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (remove, mark) = (cx.entity(), cx.entity());
|
|
let group = Duplicates {
|
|
name: "a.jpg".into(),
|
|
size: 100,
|
|
paths: vec!["x/a.jpg".into(), "y/a.jpg".into()],
|
|
};
|
|
div().w(px(320.0)).child(
|
|
DuplicateFinder::new("dupes", [group])
|
|
.marked(["y/a.jpg"])
|
|
.on_remove(move |paths, _, cx| {
|
|
remove.update(cx, |host, _| host.removed.extend(paths.iter().cloned()))
|
|
})
|
|
.on_mark(move |path, on, _, cx| {
|
|
mark.update(cx, |host, _| host.marks.push((path.clone(), on)))
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn the_last_copy_stays_and_remove_takes_the_marked(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let (host, cx) = cx.add_window_view(|_, _| Sparing {
|
|
removed: Vec::new(),
|
|
marks: Vec::new(),
|
|
});
|
|
cx.update(|window, _| window.activate_window());
|
|
settle(cx);
|
|
tab_to(1, cx);
|
|
press("space", cx);
|
|
tab_to(2, cx);
|
|
press("space", cx);
|
|
let heard = host.read_with(cx, |host, _| (host.removed.clone(), host.marks.clone()));
|
|
assert_eq!(
|
|
heard,
|
|
(vec!["y/a.jpg".into()], vec![("y/a.jpg".into(), false)]),
|
|
"x/a.jpg is kept, its mark shut"
|
|
);
|
|
}
|
|
|
|
/// An archive of four files in two folders, and the files it handed over.
|
|
struct Unpacking {
|
|
opened: Vec<SharedString>,
|
|
}
|
|
|
|
impl Render for Unpacking {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let owner = cx.entity();
|
|
let entry = |path: &str| ArchiveEntry {
|
|
path: SharedString::from(path.to_string()),
|
|
size: 400,
|
|
packed: 100,
|
|
};
|
|
let entries = ["docs/readme.md", "docs/guide.md", "src/main.rs", "LICENSE"].map(entry);
|
|
div().w(px(320.0)).h(px(320.0)).child(
|
|
ArchiveViewer::new("archive", "site.zip", entries).on_open(move |entry, _, cx| {
|
|
owner.update(cx, |host, _| host.opened.push(entry.path.clone()))
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_file_in_an_open_folder_goes_to_the_host_on_enter(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let (host, cx) = cx.add_window_view(|_, _| Unpacking { opened: Vec::new() });
|
|
cx.update(|window, _| window.activate_window());
|
|
settle(cx);
|
|
tab_to(1, cx);
|
|
press("down", cx);
|
|
press("enter", cx);
|
|
assert_eq!(
|
|
host.read_with(cx, |host, _| host.opened.clone()),
|
|
["docs/guide.md"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "LICENSE twice")]
|
|
fn an_archive_lists_a_path_once() {
|
|
let entry = ArchiveEntry {
|
|
path: "LICENSE".into(),
|
|
size: 1,
|
|
packed: 1,
|
|
};
|
|
let _ = ArchiveViewer::new("archive", "a.zip", [entry.clone(), entry]);
|
|
}
|
|
|
|
/// Three files opened today; an open moves a file to the top, as a host records it.
|
|
struct Reopening {
|
|
files: Vec<RecentFile>,
|
|
opened: Vec<SharedString>,
|
|
}
|
|
|
|
impl Render for Reopening {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let owner = cx.entity();
|
|
div().w(px(320.0)).child(
|
|
RecentFiles::new("recent", self.files.clone())
|
|
.zone(TimeZone::UTC)
|
|
.on_open(move |file, _, cx| {
|
|
owner.update(cx, |host, cx| {
|
|
host.opened.push(file.name.clone());
|
|
let now = Timestamp::now();
|
|
for kept in &mut host.files {
|
|
if kept.name == file.name {
|
|
kept.opened = now;
|
|
}
|
|
}
|
|
cx.notify();
|
|
})
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn the_cursor_stays_on_a_file_an_open_moves(cx: &mut TestAppContext) {
|
|
setup(cx);
|
|
let midnight = Timestamp::now()
|
|
.to_zoned(TimeZone::UTC)
|
|
.start_of_day()
|
|
.expect("a day starts")
|
|
.timestamp();
|
|
let file = |name: &str, minutes: i64| RecentFile {
|
|
name: SharedString::from(name.to_string()),
|
|
folder: "Home".into(),
|
|
opened: midnight + SignedDuration::from_mins(minutes),
|
|
};
|
|
let files = vec![file("a.md", 3), file("b.md", 2), file("c.md", 1)];
|
|
let (host, cx) = cx.add_window_view(move |_, _| Reopening {
|
|
files,
|
|
opened: Vec::new(),
|
|
});
|
|
cx.update(|window, _| window.activate_window());
|
|
settle(cx);
|
|
tab_to(1, cx);
|
|
press("down", cx);
|
|
press("enter", cx);
|
|
press("enter", cx);
|
|
assert_eq!(
|
|
host.read_with(cx, |host, _| host.opened.clone()),
|
|
["b.md", "b.md"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "Home/a.md twice")]
|
|
fn a_recent_file_is_listed_once() {
|
|
let file = RecentFile {
|
|
name: "a.md".into(),
|
|
folder: "Home".into(),
|
|
opened: then(),
|
|
};
|
|
let _ = RecentFiles::new("recent", [file.clone(), file]);
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[should_panic(expected = "Home/a.md twice")]
|
|
fn a_found_file_is_listed_once(cx: &mut TestAppContext) {
|
|
let file = FoundFile {
|
|
name: "a.md".into(),
|
|
folder: "Home".into(),
|
|
};
|
|
let _ = seeking("a", vec![file.clone(), file], cx);
|
|
}
|