Skip to main content

tests/
tests.rs

1use std::{env, fmt, io, process};
2use std::io::{IsTerminal, Write};
3
4#[derive(Copy, Clone, Debug)]
5enum TestSelector {
6	AllTests,
7	MessageBox,
8	SaveFileDialog,
9	OpenFileDialog,
10	FolderDialog,
11	ColorPicker,
12	TextInput,
13	Notification,
14}
15
16fn main() {
17	print_environment();
18
19	let args: Vec<_> = env::args().skip(1).collect();
20	let args = args.iter().map(|s| s.as_str()).collect::<Vec<_>>();
21
22	let selector = if args.len() != 1 {
23		prompt_selector()
24	}
25	else if let Some(selector) = parse_selector(args[0]) {
26		selector
27	}
28	else {
29		prompt_selector()
30	};
31
32	match selector {
33		TestSelector::AllTests => {
34			test_message_box();
35			test_save_file_dialog();
36			test_open_file_dialog();
37			test_folder_dialog();
38			test_color_picker();
39			test_text_input();
40			test_notification();
41		}
42		TestSelector::MessageBox => test_message_box(),
43		TestSelector::SaveFileDialog => test_save_file_dialog(),
44		TestSelector::OpenFileDialog => test_open_file_dialog(),
45		TestSelector::FolderDialog => test_folder_dialog(),
46		TestSelector::ColorPicker => test_color_picker(),
47		TestSelector::TextInput => test_text_input(),
48		TestSelector::Notification => test_notification(),
49	}
50}
51
52fn print_environment() {
53	println!("\n{}", Color("Environment", "255;214;102"));
54	println!("  {}: {}", Color("OS", "170;170;170"), env::consts::OS);
55	println!("  {}: {}", Color("Arch", "170;170;170"), env::consts::ARCH);
56	let backend = env::var("RUSTY_DIALOGS_BACKEND");
57	let backend = match &backend { Ok(value) => value as &dyn fmt::Display, Err(_) => &"(not set)" as &dyn fmt::Display };
58	println!("  {}: {}", Color("RUSTY_DIALOGS_BACKEND", "170;170;170"), backend);
59	println!("  {}: {}", Color("rustc", "170;170;170"), version_command("rustc", "-V"));
60	println!("  {}: {}", Color("cargo", "170;170;170"), version_command("cargo", "-V"));
61}
62
63fn version_command(cmd: &str, arg: &str) -> String {
64	match process::Command::new(cmd).arg(arg).output() {
65		Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_string(),
66		Ok(out) => format!("(failed: exit status {})", out.status),
67		Err(err) => format!("(not available: {err})"),
68	}
69}
70
71fn prompt_selector() -> TestSelector {
72	println!("\n{}", Color("Select tests", "255;214;102"));
73	println!("  Enter = all tests");
74	println!("  m = MessageBox");
75	println!("  s = SaveFileDialog");
76	println!("  o = OpenFileDialog");
77	println!("  f = FolderDialog");
78	println!("  t = TextInput");
79	println!("  c = ColorPicker");
80	println!("  n = Notification");
81
82	loop {
83		let value = prompt_input("Choice: ");
84		match parse_selector(value.trim()) {
85			Some(selector) => return selector,
86			None => eprintln!("{}", Color("Invalid choice, please try again.", "255;107;107")),
87		}
88	}
89}
90
91fn prompt_input(prompt: &str) -> String {
92	print!("{prompt}");
93	_ = io::stdout().flush();
94	let mut line = String::new();
95	if io::stdin().read_line(&mut line).is_err() {
96		return String::new();
97	}
98	line
99}
100
101fn parse_selector(s: &str) -> Option<TestSelector> {
102	match s {
103		"" | "all" | "Enter" => Some(TestSelector::AllTests),
104		"m" => Some(TestSelector::MessageBox),
105		"s" => Some(TestSelector::SaveFileDialog),
106		"o" => Some(TestSelector::OpenFileDialog),
107		"f" => Some(TestSelector::FolderDialog),
108		"c" => Some(TestSelector::ColorPicker),
109		"t" => Some(TestSelector::TextInput),
110		"n" => Some(TestSelector::Notification),
111		_ => None,
112	}
113}
114
115fn step<F: Fn() -> T, T: fmt::Debug + PartialEq>(description: &str, expected: T, action: F) {
116	println!("\n{} {description}", Color("Step:", "255;214;102"));
117	loop {
118		let result = action();
119		if result == expected {
120			println!("  Result: {}", Color("PASS", "123;201;111"));
121			break;
122		}
123
124		println!("  Result: {} - expected {expected:?}, got {result:?}", Color("FAIL", "255;107;107"));
125		if !confirm("  Test failed, retry? [Y/n]: ", true) {
126			println!("  {}", Color("Marked as failed.", "255;107;107"));
127			break;
128		}
129		println!("  {}", Color("Retrying step...", "255;214;102"));
130	}
131}
132
133fn test_message_box() {
134	println!("\n{}", Color("==== Testing MessageBox ====", "120;190;255"));
135
136	let icons: &[rustydialogs::MessageIcon] = &[
137		rustydialogs::MessageIcon::Info,
138		rustydialogs::MessageIcon::Warning,
139		rustydialogs::MessageIcon::Error,
140		rustydialogs::MessageIcon::Question,
141	];
142
143	let matrix: &[(rustydialogs::MessageButtons, &[Option<rustydialogs::MessageResult>])] = &[
144		(rustydialogs::MessageButtons::Ok, &[Some(rustydialogs::MessageResult::Ok), None]),
145		(rustydialogs::MessageButtons::OkCancel, &[Some(rustydialogs::MessageResult::Ok), Some(rustydialogs::MessageResult::Cancel), None]),
146		(rustydialogs::MessageButtons::YesNo, &[Some(rustydialogs::MessageResult::Yes), Some(rustydialogs::MessageResult::No), None]),
147		(rustydialogs::MessageButtons::YesNoCancel, &[Some(rustydialogs::MessageResult::Yes), Some(rustydialogs::MessageResult::No), Some(rustydialogs::MessageResult::Cancel), None]),
148	];
149
150	for &icon in icons {
151		println!("\n{} Icon: {}", Color("Testing", "120;190;255"), Color(format_args!("{:?}", icon), "255;214;102"));
152		let title = format!("[tests] MessageBox - {icon:?}");
153		for &(buttons, results) in matrix {
154			for &result in results {
155				let desc = match result {
156					Some(rustydialogs::MessageResult::Ok) => "Press OK.",
157					Some(rustydialogs::MessageResult::Cancel) => "Press Cancel.",
158					Some(rustydialogs::MessageResult::Yes) => "Press Yes.",
159					Some(rustydialogs::MessageResult::No) => "Press No.",
160					None => "Dismiss the dialog.",
161				};
162				let message = format!("Instruction: {desc}");
163				let full_desc = format!("{desc}\n  Buttons: {}\n  Icon: {}", Color(format_args!("{:?}", buttons), "255;214;102"), Color(format_args!("{:?}", icon), "255;214;102"));
164				step(&full_desc,
165					result,
166					|| rustydialogs::MessageBox {
167						title: &title,
168						message: &message,
169						icon,
170						buttons,
171						owner: None,
172					}.show()
173				);
174			}
175		}
176	}
177}
178
179fn test_save_file_dialog() {
180	println!("\n{}", Color("==== Testing SaveFileDialog ====", "120;190;255"));
181
182	step("Select `readme.md` and press Save.",
183		Some(env::current_dir().unwrap().join("readme.md")),
184		|| rustydialogs::FileDialog {
185			title: "[tests] SaveFileDialog",
186			path: None,
187			filter: Some(&[
188				rustydialogs::FileFilter {
189					desc: "Markdown Files",
190					patterns: &["*.md"],
191				},
192				rustydialogs::FileFilter {
193					desc: "Text Files",
194					patterns: &["*.txt"],
195				},
196			]),
197			owner: None,
198		}.save_file()
199	);
200
201	step("Dismiss the dialog.",
202		None,
203		|| rustydialogs::FileDialog {
204			title: "[tests] Dismiss SaveFileDialog",
205			path: None,
206			filter: Some(&[
207				rustydialogs::FileFilter {
208					desc: "Text Files",
209					patterns: &["*.txt"],
210				},
211			]),
212			owner: None,
213		}.save_file()
214	);
215}
216
217fn test_open_file_dialog() {
218	println!("\n{}", Color("==== Testing OpenFileDialog ====", "120;190;255"));
219
220	step("Select `Cargo.toml` and press Open.",
221		Some(env::current_dir().unwrap().join("Cargo.toml")),
222		|| rustydialogs::FileDialog {
223			title: "[tests] OpenFileDialog",
224			path: None,
225			filter: Some(&[
226				rustydialogs::FileFilter {
227					desc: "TOML Files",
228					patterns: &["*.toml"],
229				},
230			]),
231			owner: None,
232		}.pick_file()
233	);
234	step("Select multiple files (`Cargo.toml` and `readme.md`) and press Open.",
235		Some(vec![
236			env::current_dir().unwrap().join("Cargo.toml"),
237			env::current_dir().unwrap().join("readme.md"),
238		]),
239		|| rustydialogs::FileDialog {
240			title: "[tests] OpenFileDialog (multiple)",
241			path: None,
242			filter: None,
243			owner: None,
244		}.pick_files()
245	);
246	step("Dismiss the dialog.",
247		None,
248		|| rustydialogs::FileDialog {
249			title: "[tests] Dismiss OpenFileDialog",
250			path: None,
251			filter: Some(&[
252				rustydialogs::FileFilter {
253					desc: "TOML Files",
254					patterns: &["*.toml"],
255				},
256			]),
257			owner: None,
258		}.pick_file()
259	);
260}
261
262fn test_folder_dialog() {
263	println!("\n{}", Color("==== Testing FolderDialog ====", "120;190;255"));
264
265	step("Select the `src` folder and press Open.",
266		Some(env::current_dir().unwrap().join("src")),
267		|| rustydialogs::FolderDialog {
268			title: "[tests] FolderDialog",
269			directory: None,
270			owner: None,
271		}.show()
272	);
273
274	step("Dismiss the dialog.",
275		None,
276		|| rustydialogs::FolderDialog {
277			title: "[tests] Dismiss FolderDialog",
278			directory: None,
279			owner: None,
280		}.show()
281	);
282}
283
284fn test_color_picker() {
285	println!("\n{}", Color("==== Testing ColorPicker ====", "120;190;255"));
286
287	step("Select pure RED (#FF0000) and press OK.",
288		Some(rustydialogs::ColorValue { red: 255, green: 0, blue: 0 }),
289		|| rustydialogs::ColorPicker {
290			title: "[tests] ColorPicker",
291			value: rustydialogs::ColorValue { red: 255, green: 0, blue: 0 },
292			owner: None,
293		}.show()
294	);
295
296	step("Select specific color (#4FB3A3) (79, 179, 163) and press OK.",
297		Some(rustydialogs::ColorValue { red: 79, green: 179, blue: 163 }),
298		|| rustydialogs::ColorPicker {
299			title: "[tests] ColorPicker",
300			value: rustydialogs::ColorValue { red: 255, green: 0, blue: 0 },
301			owner: None,
302		}.show()
303	);
304
305	step("Dismiss the dialog.",
306		None,
307		|| rustydialogs::ColorPicker {
308			title: "[tests] Dismiss ColorPicker",
309			value: rustydialogs::ColorValue { red: 255, green: 0, blue: 0 },
310			owner: None,
311		}.show()
312	);
313}
314
315fn test_text_input() {
316	println!("\n{}", Color("==== Testing TextInput ====", "120;190;255"));
317
318	step("Enter `Hello, Rust!` and press OK.",
319		Some("Hello, Rust!".to_string()),
320		|| rustydialogs::TextInput {
321			title: "[tests] TextInput",
322			message: "Instruction: Enter `Hello, Rust!` and press OK.",
323			value: "",
324			mode: rustydialogs::TextInputMode::SingleLine,
325			owner: None,
326		}.show()
327	);
328
329	step("Enter `Password123` and press OK.",
330		Some(String::from("Password123")),
331		|| rustydialogs::TextInput {
332			title: "[tests] TextInput",
333			message: "Instruction: Enter `Password123` and press OK.",
334			value: "",
335			mode: rustydialogs::TextInputMode::Password,
336			owner: None,
337		}.show()
338	);
339
340	step("Enter these three lines and press OK.",
341		Some(String::from("Line 1\nLine 2\nLine 3")),
342		|| rustydialogs::TextInput {
343			title: "[tests] TextInput",
344			message: "Instruction: Enter these three lines and press OK.\nLine 1\nLine 2\nLine 3",
345			value: "",
346			mode: rustydialogs::TextInputMode::MultiLine,
347			owner: None,
348		}.show()
349	);
350
351	step("Dismiss the dialog.",
352		None,
353		|| rustydialogs::TextInput {
354			title: "[tests] Dismiss TextInput",
355			message: "Instruction: Dismiss the dialog (e.g. by pressing Esc or clicking the close button).",
356			value: "",
357			mode: rustydialogs::TextInputMode::SingleLine,
358			owner: None,
359		}.show()
360	);
361}
362
363fn test_notification() {
364	println!("\n{}", Color("==== Testing Notification ====", "120;190;255"));
365
366	fn notify(p: &rustydialogs::Notification<'_>) {
367		println!("\n{} Confirm {} appeared.", Color("Step:", "255;214;102"), Color(format_args!("{:?}", p.icon), "255;214;102"));
368		loop {
369			p.show();
370			if confirm("Confirm notification? [Y/n]: ", true) {
371				println!("  Result: {}", Color("PASS", "123;201;111"));
372				break;
373			}
374			println!("  Result: {}", Color("FAIL", "255;107;107"));
375			if !confirm("  Test failed, retry? [Y/n]: ", true) {
376				break;
377			}
378		}
379	}
380
381	notify(&rustydialogs::Notification {
382		app_id: "rustydialogs-tests",
383		title: "[INFO] Notification",
384		message: "This is a test notification.\nIt should appear as a native notification on your system.",
385		icon: rustydialogs::MessageIcon::Info,
386		timeout: rustydialogs::Notification::SHORT_TIMEOUT,
387	});
388
389	notify(&rustydialogs::Notification {
390		app_id: "rustydialogs-tests",
391		title: "[WARN] Notification",
392		message: "This is a test notification.\nIt should appear as a native notification on your system.",
393		icon: rustydialogs::MessageIcon::Warning,
394		timeout: rustydialogs::Notification::SHORT_TIMEOUT,
395	});
396
397	notify(&rustydialogs::Notification {
398		app_id: "rustydialogs-tests",
399		title: "[ERROR] Notification",
400		message: "This is a test notification.\nIt should appear as a native notification on your system.",
401		icon: rustydialogs::MessageIcon::Error,
402		timeout: rustydialogs::Notification::SHORT_TIMEOUT,
403	});
404
405	notify(&rustydialogs::Notification {
406		app_id: "rustydialogs-tests",
407		title: "[QUESTION] Notification",
408		message: "This is a test notification.\nIt should appear as a native notification on your system.",
409		icon: rustydialogs::MessageIcon::Question,
410		timeout: rustydialogs::Notification::SHORT_TIMEOUT,
411	});
412}
413
414fn confirm(prompt: &str, default: bool) -> bool {
415	loop {
416		let input = prompt_input(prompt);
417		let value = input.trim().to_ascii_lowercase();
418		if value.is_empty() {
419			return default;
420		}
421		if value == "y" || value == "yes" {
422			return true;
423		}
424		if value == "n" || value == "no" {
425			return false;
426		}
427		eprintln!("{}", Color("Please answer y/yes or n/no.", "255;107;107"));
428	}
429}
430
431struct Color<'a, T> {
432	value: T,
433	color: &'a str,
434}
435#[allow(non_snake_case)]
436fn Color<'a, T>(value: T, color: &'a str) -> Color<'a, T> {
437	Color { value, color }
438}
439impl<'a, T: fmt::Display> fmt::Display for Color<'a, T> {
440	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441		if !io::stdout().is_terminal() {
442			write!(f, "{}", self.value)
443		}
444		else {
445			write!(f, "\x1b[38;2;{}m{}\x1b[0m", self.color, self.value)
446		}
447	}
448}