1#![cfg(test)]
2
3use collections::HashSet;
4use fs::{FakeFs, Fs};
5use gpui::{Entity, VisualTestContext};
6use project::Project;
7use serde_json::{Value, json};
8use std::path::Path;
9use std::sync::Arc;
10use workspace::MultiWorkspace;
11
12use crate::project_panel_tests::{self, find_project_entry, select_path};
13use crate::{NewDirectory, NewFile, ProjectPanel, Redo, Rename, Trash, Undo};
14
15struct TestContext {
16 panel: Entity<ProjectPanel>,
17 fs: Arc<FakeFs>,
18 cx: VisualTestContext,
19}
20
21// Using the `util::path` macro requires a string literal, which would mean that
22// callers of, for example, `rename`, would now need to know about `/` and
23// use `path!` in tests.
24//
25// As such, we define it as a function here to make the helper methods more
26// ergonomic for our use case.
27fn path(path: impl AsRef<str>) -> String {
28 let path = path.as_ref();
29 #[cfg(target_os = "windows")]
30 {
31 let mut path = path.replace("/", "\\");
32 if path.starts_with("\\") {
33 path = format!("C:{}", &path);
34 }
35 path
36 }
37
38 #[cfg(not(target_os = "windows"))]
39 {
40 path.to_string()
41 }
42}
43
44impl TestContext {
45 async fn undo(&mut self) {
46 self.panel.update_in(&mut self.cx, |panel, window, cx| {
47 panel.undo(&Undo, window, cx);
48 });
49 self.cx.run_until_parked();
50 }
51 async fn redo(&mut self) {
52 self.panel.update_in(&mut self.cx, |panel, window, cx| {
53 panel.redo(&Redo, window, cx);
54 });
55 self.cx.run_until_parked();
56 }
57
58 /// Note this only works when every file has an extension
59 fn assert_fs_state_is(&mut self, state: &[&str]) {
60 let state: HashSet<_> = state
61 .into_iter()
62 .map(|s| path(format!("/workspace/{s}")))
63 .chain([path("/workspace"), path("/")])
64 .map(|s| Path::new(&s).to_path_buf())
65 .collect();
66
67 let dirs: HashSet<_> = state
68 .iter()
69 .map(|p| match p.extension() {
70 Some(_) => p.parent().unwrap_or(Path::new(&path("/"))).to_owned(),
71 None => p.clone(),
72 })
73 .collect();
74
75 assert_eq!(
76 self.fs
77 .directories(true)
78 .into_iter()
79 .collect::<HashSet<_>>(),
80 dirs
81 );
82 assert_eq!(
83 self.fs.paths(true).into_iter().collect::<HashSet<_>>(),
84 state
85 );
86 }
87
88 fn assert_exists(&mut self, file: &str) {
89 assert!(
90 find_project_entry(&self.panel, &format!("workspace/{file}"), &mut self.cx).is_some(),
91 "{file} should exist"
92 );
93 }
94
95 fn assert_not_exists(&mut self, file: &str) {
96 assert_eq!(
97 find_project_entry(&self.panel, &format!("workspace/{file}"), &mut self.cx),
98 None,
99 "{file} should not exist"
100 );
101 }
102
103 async fn rename(&mut self, from: &str, to: &str) {
104 let from = format!("workspace/{from}");
105 let Self { panel, cx, .. } = self;
106 select_path(&panel, &from, cx);
107 panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
108 cx.run_until_parked();
109
110 let confirm = panel.update_in(cx, |panel, window, cx| {
111 panel
112 .filename_editor
113 .update(cx, |editor, cx| editor.set_text(to, window, cx));
114 panel.confirm_edit(true, window, cx).unwrap()
115 });
116 confirm.await.unwrap();
117 cx.run_until_parked();
118 }
119
120 async fn create_file(&mut self, path: &str) {
121 let Self { panel, cx, .. } = self;
122 select_path(&panel, "workspace", cx);
123 panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
124 cx.run_until_parked();
125
126 let confirm = panel.update_in(cx, |panel, window, cx| {
127 panel
128 .filename_editor
129 .update(cx, |editor, cx| editor.set_text(path, window, cx));
130 panel.confirm_edit(true, window, cx).unwrap()
131 });
132 confirm.await.unwrap();
133 cx.run_until_parked();
134 }
135
136 async fn create_directory(&mut self, path: &str) {
137 let Self { panel, cx, .. } = self;
138
139 select_path(&panel, "workspace", cx);
140 panel.update_in(cx, |panel, window, cx| {
141 panel.new_directory(&NewDirectory, window, cx)
142 });
143 cx.run_until_parked();
144
145 let confirm = panel.update_in(cx, |panel, window, cx| {
146 panel
147 .filename_editor
148 .update(cx, |editor, cx| editor.set_text(path, window, cx));
149 panel.confirm_edit(true, window, cx).unwrap()
150 });
151 confirm.await.unwrap();
152 cx.run_until_parked();
153 }
154
155 /// Drags the `files` to the provided `directory`.
156 fn drag(&mut self, files: &[&str], directory: &str) {
157 self.panel
158 .update(&mut self.cx, |panel, _| panel.marked_entries.clear());
159 files.into_iter().for_each(|file| {
160 project_panel_tests::select_path_with_mark(
161 &self.panel,
162 &format!("workspace/{file}"),
163 &mut self.cx,
164 )
165 });
166 project_panel_tests::drag_selection_to(
167 &self.panel,
168 &format!("workspace/{directory}"),
169 false,
170 &mut self.cx,
171 );
172 }
173
174 /// Only supports files in root (otherwise would need toggle_expand_dir).
175 /// For undo redo the paths themselves do not matter so this is fine
176 async fn cut(&mut self, file: &str) {
177 project_panel_tests::select_path_with_mark(
178 &self.panel,
179 &format!("workspace/{file}"),
180 &mut self.cx,
181 );
182 self.panel.update_in(&mut self.cx, |panel, window, cx| {
183 panel.cut(&Default::default(), window, cx);
184 });
185 }
186
187 /// Only supports files in root (otherwise would need toggle_expand_dir).
188 /// For undo redo the paths themselves do not matter so this is fine
189 async fn paste(&mut self, directory: &str) {
190 select_path(&self.panel, &format!("workspace/{directory}"), &mut self.cx);
191 self.panel.update_in(&mut self.cx, |panel, window, cx| {
192 panel.paste(&Default::default(), window, cx);
193 });
194 self.cx.run_until_parked();
195 }
196
197 async fn trash(&mut self, paths: &[&str]) {
198 paths.iter().for_each(|p| {
199 project_panel_tests::select_path_with_mark(
200 &self.panel,
201 &format!("workspace/{p}"),
202 &mut self.cx,
203 )
204 });
205
206 self.panel.update_in(&mut self.cx, |panel, window, cx| {
207 panel.trash(&Trash { skip_prompt: true }, window, cx);
208 });
209
210 self.cx.run_until_parked();
211 }
212
213 /// The test tree is:
214 /// ```txt
215 /// a.txt
216 /// b.txt
217 /// ```
218 /// a and b are empty, x has the text "content" inside
219 async fn new(cx: &mut gpui::TestAppContext) -> TestContext {
220 Self::new_with_tree(
221 cx,
222 json!({
223 "a.txt": "",
224 "b.txt": "",
225 }),
226 )
227 .await
228 }
229
230 async fn new_with_tree(cx: &mut gpui::TestAppContext, tree: Value) -> TestContext {
231 project_panel_tests::init_test(cx);
232
233 let fs = FakeFs::new(cx.executor());
234 fs.insert_tree("/workspace", tree).await;
235 let project = Project::test(fs.clone(), ["/workspace".as_ref()], cx).await;
236 let window =
237 cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
238 let workspace = window
239 .read_with(cx, |mw, _| mw.workspace().clone())
240 .unwrap();
241 let mut cx = VisualTestContext::from_window(window.into(), cx);
242 let panel = workspace.update_in(&mut cx, ProjectPanel::new);
243 cx.run_until_parked();
244
245 TestContext { panel, fs, cx }
246 }
247}
248
249#[gpui::test]
250async fn rename_undo_redo(cx: &mut gpui::TestAppContext) {
251 let mut cx = TestContext::new(cx).await;
252
253 cx.rename("a.txt", "renamed.txt").await;
254 cx.assert_fs_state_is(&["b.txt", "renamed.txt"]);
255
256 cx.undo().await;
257 cx.assert_fs_state_is(&["a.txt", "b.txt"]);
258
259 cx.redo().await;
260 cx.assert_fs_state_is(&["b.txt", "renamed.txt"]);
261}
262
263#[gpui::test]
264async fn create_undo_redo(cx: &mut gpui::TestAppContext) {
265 let mut cx = TestContext::new(cx).await;
266 let path = path("/workspace/c.txt");
267
268 cx.create_file("c.txt").await;
269 cx.assert_exists("c.txt");
270
271 // We'll now insert some content into `c.txt` in order to ensure that, after
272 // undoing the trash operation, i.e., when the file is restored, the actual
273 // file's contents are preserved instead of a new one with the same path
274 // being created.
275 cx.fs.write(Path::new(&path), b"Hello!").await.unwrap();
276
277 cx.undo().await;
278 cx.assert_not_exists("c.txt");
279
280 cx.redo().await;
281 cx.assert_exists("c.txt");
282 assert_eq!(cx.fs.load(Path::new(&path)).await.unwrap(), "Hello!");
283}
284
285#[gpui::test]
286async fn create_dir_undo(cx: &mut gpui::TestAppContext) {
287 let mut cx = TestContext::new(cx).await;
288
289 cx.create_directory("new_dir").await;
290 cx.assert_exists("new_dir");
291 cx.undo().await;
292 cx.assert_not_exists("new_dir");
293}
294
295#[gpui::test]
296async fn cut_paste_undo(cx: &mut gpui::TestAppContext) {
297 let mut cx = TestContext::new(cx).await;
298
299 cx.create_directory("files").await;
300 cx.cut("a.txt").await;
301 cx.paste("files").await;
302 cx.assert_fs_state_is(&["b.txt", "files/", "files/a.txt"]);
303
304 cx.undo().await;
305 cx.assert_fs_state_is(&["a.txt", "b.txt", "files/"]);
306
307 cx.redo().await;
308 cx.assert_fs_state_is(&["b.txt", "files/", "files/a.txt"]);
309}
310
311#[gpui::test]
312async fn drag_undo_redo(cx: &mut gpui::TestAppContext) {
313 let mut cx = TestContext::new(cx).await;
314
315 cx.create_directory("src").await;
316 cx.create_file("src/a.rs").await;
317 cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/a.rs"]);
318
319 cx.drag(&["src/a.rs"], "");
320 cx.assert_fs_state_is(&["a.txt", "b.txt", "a.rs", "src/"]);
321
322 cx.undo().await;
323 cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/a.rs"]);
324
325 cx.redo().await;
326 cx.assert_fs_state_is(&["a.txt", "b.txt", "a.rs", "src/"]);
327}
328
329#[gpui::test]
330async fn drag_multiple_undo_redo(cx: &mut gpui::TestAppContext) {
331 let mut cx = TestContext::new(cx).await;
332
333 cx.create_directory("src").await;
334 cx.create_file("src/x.rs").await;
335 cx.create_file("src/y.rs").await;
336
337 cx.drag(&["src/x.rs", "src/y.rs"], "");
338 cx.assert_fs_state_is(&["a.txt", "b.txt", "x.rs", "y.rs", "src/"]);
339
340 cx.undo().await;
341 cx.assert_fs_state_is(&["a.txt", "b.txt", "src/", "src/x.rs", "src/y.rs"]);
342
343 cx.redo().await;
344 cx.assert_fs_state_is(&["a.txt", "b.txt", "x.rs", "y.rs", "src/"]);
345}
346
347#[gpui::test]
348async fn two_sequential_undos(cx: &mut gpui::TestAppContext) {
349 let mut cx = TestContext::new(cx).await;
350
351 cx.rename("a.txt", "x.txt").await;
352 cx.create_file("y.txt").await;
353 cx.assert_fs_state_is(&["b.txt", "x.txt", "y.txt"]);
354
355 cx.undo().await;
356 cx.assert_fs_state_is(&["b.txt", "x.txt"]);
357
358 cx.undo().await;
359 cx.assert_fs_state_is(&["a.txt", "b.txt"]);
360}
361
362#[gpui::test]
363async fn undo_without_history(cx: &mut gpui::TestAppContext) {
364 let mut cx = TestContext::new(cx).await;
365
366 // Undoing without any history should just result in the filesystem state
367 // remaining unchanged.
368 cx.undo().await;
369 cx.assert_fs_state_is(&["a.txt", "b.txt"])
370}
371
372#[gpui::test]
373async fn trash_undo_redo(cx: &mut gpui::TestAppContext) {
374 let mut cx = TestContext::new(cx).await;
375
376 cx.trash(&["a.txt", "b.txt"]).await;
377 cx.assert_fs_state_is(&[]);
378
379 cx.undo().await;
380 cx.assert_fs_state_is(&["a.txt", "b.txt"]);
381
382 cx.redo().await;
383 cx.assert_fs_state_is(&[]);
384}