1use std::time::Duration;
4
5use ratatui_core::buffer::Buffer;
6use ratatui_core::layout::Rect as BufferRect;
7use ratatui_core::style::{Color, Modifier};
8
9use super::app::App;
10use super::detached::DetachedOutcome;
11use super::engine::{Engine, TaskMode};
12use super::follow::{Follow, Member, Start};
13use super::handoff::{HandoffOutcome, HandoffRequest};
14use super::open::{OpenOutcome, OpenRequest};
15use super::termination::Termination;
16use crate::color::{ColorDepth, Rgb};
17use crate::env::Env;
18use crate::event::{Event, KeyEvent, MouseButton, MouseEvent, MouseKind};
19use crate::icons::GlyphMode;
20use crate::keymap::Modifiers;
21use crate::widget::PointerShape;
22
23const KEY_INTERVAL: Duration = Duration::from_millis(150);
26
27pub struct Harness<A: App> {
34 engine: Engine<A>,
35 buffer: Buffer,
36 now: Duration,
37}
38
39impl<A: App> Harness<A> {
40 pub fn new(app: A, width: u16, height: u16) -> Self {
46 Self::with_env(app, Env::builtin(), width, height)
47 }
48
49 pub fn with_env(app: A, env: Env, width: u16, height: u16) -> Self {
51 let mut harness = Self {
52 engine: Engine::new(app, env, TaskMode::Inline),
53 buffer: Buffer::empty(BufferRect::new(0, 0, width, height)),
54 now: Duration::ZERO,
55 };
56 harness.render();
57 harness
58 }
59
60 pub fn member_in(
73 app: A,
74 ecosystem: crate::storage::Ecosystem,
75 config_dir: &std::path::Path,
76 name: &str,
77 width: u16,
78 height: u16,
79 ) -> Self {
80 Self::member_in_with_env(app, Env::builtin(), ecosystem, config_dir, name, width, height)
81 }
82
83 pub fn member_in_with_env(
95 app: A,
96 mut env: Env,
97 ecosystem: crate::storage::Ecosystem,
98 config_dir: &std::path::Path,
99 name: &str,
100 width: u16,
101 height: u16,
102 ) -> Self {
103 let start = Start { theme: None, settings: None, preferences: None };
104 let follow = start.apply(&mut env, Some(Member::new(ecosystem, name, Some(config_dir.to_path_buf()))), false);
105 Self::started(app, env, follow, width, height)
106 }
107
108 pub(crate) fn started(app: A, env: Env, follow: Option<Follow>, width: u16, height: u16) -> Self {
111 let mut engine = Engine::new(app, env, TaskMode::Inline);
112 if let Some(follow) = follow {
113 engine.follow(follow);
114 }
115 let mut harness =
116 Self { engine, buffer: Buffer::empty(BufferRect::new(0, 0, width, height)), now: Duration::ZERO };
117 harness.render();
118 harness
119 }
120
121 pub fn poll_preferences(&mut self) -> &mut Self {
128 self.engine.check_preferences();
129 if self.engine.dirty {
130 self.render();
131 }
132 self
133 }
134
135 pub fn render(&mut self) -> &mut Self {
137 self.settle_tasks();
138 self.engine.render(&mut self.buffer, self.now);
139 self.write_out();
140 for _ in 0..3 {
142 let due = self.engine.deadline().is_some_and(|deadline| deadline <= self.now);
143 if !self.engine.dirty && !due {
144 break;
145 }
146 self.engine.render(&mut self.buffer, self.now);
147 self.write_out();
148 }
149 self
150 }
151
152 #[cfg(all(test, feature = "image"))]
155 pub(crate) fn present_to<W: std::io::Write>(&mut self, screen: &mut super::present::Screen<W>) -> bool {
156 let now = self.now;
157 let engine = &mut self.engine;
158 screen
159 .present(|buffer| {
160 engine.render(buffer, now);
161 engine.painted()
162 })
163 .expect("a frame")
164 }
165
166 #[cfg(test)]
170 pub(crate) fn next_frame(&self) -> Option<Duration> {
171 self.engine.deadline()
172 }
173
174 fn write_out(&self) {
178 let blank = Buffer::empty(self.buffer.area);
179 let _ = blank.diff(&self.buffer);
180 }
181
182 fn settle_tasks(&mut self) {
187 self.engine.run_queued_work();
188 loop {
189 self.engine.task_clock.settle(self.now);
190 if self.engine.poll_tasks() == 0 {
191 break;
192 }
193 }
194 }
195
196 pub fn send(&mut self, message: A::Msg) -> &mut Self {
198 self.engine.update(message);
199 self.render()
200 }
201
202 pub fn press(&mut self, chord: &str) -> &mut Self {
204 self.now += KEY_INTERVAL;
205 self.engine.handle(Event::Key(KeyEvent::press(chord)), self.now);
206 self.render()
207 }
208
209 pub fn type_text(&mut self, text: &str) -> &mut Self {
211 for c in text.chars() {
212 let chord = match c {
213 ' ' => "space".to_owned(),
214 '+' => "+".to_owned(),
215 c if c.is_uppercase() => format!("shift+{}", c.to_lowercase()),
216 c => c.to_string(),
217 };
218 self.press(&chord);
219 }
220 self
221 }
222
223 pub fn events(&mut self, events: &[Event]) -> &mut Self {
230 for event in events {
231 self.engine.handle(event.clone(), self.now);
232 }
233 self.render()
234 }
235
236 #[cfg(test)]
238 pub(crate) fn inject(&mut self, event: Event, at: Duration) -> &mut Self {
239 self.engine.handle(event, at);
240 self.render()
241 }
242
243 pub fn paste(&mut self, text: &str) -> &mut Self {
245 self.engine.handle(Event::Paste(text.to_owned()), self.now);
246 self.render()
247 }
248
249 pub fn click(&mut self, x: i32, y: i32) -> &mut Self {
251 self.mouse(MouseKind::Down(MouseButton::Left), x, y);
252 self.mouse(MouseKind::Up(MouseButton::Left), x, y)
253 }
254
255 pub fn click_text(&mut self, text: &str) -> &mut Self {
261 let (x, y) = self.find(text).unwrap_or_else(|| panic!("`{text}` is not on screen:\n{}", self.screen()));
262 self.click(x, y)
263 }
264
265 pub fn drag(&mut self, from: (i32, i32), to: (i32, i32)) -> &mut Self {
267 self.mouse(MouseKind::Down(MouseButton::Left), from.0, from.1);
268 self.mouse(MouseKind::Drag(MouseButton::Left), to.0, to.1);
269 self.mouse(MouseKind::Up(MouseButton::Left), to.0, to.1)
270 }
271
272 pub fn hover(&mut self, x: i32, y: i32) -> &mut Self {
274 self.mouse(MouseKind::Moved, x, y)
275 }
276
277 pub fn mouse(&mut self, kind: MouseKind, x: i32, y: i32) -> &mut Self {
279 self.engine.handle(Event::Mouse(MouseEvent { kind, x, y, mods: Modifiers::default() }), self.now);
280 self.render()
281 }
282
283 pub fn advance(&mut self, duration: Duration) -> &mut Self {
292 self.now += duration;
293 self.engine.tick(self.now);
294 self.engine.end_when_due(self.now);
295 self.render()
296 }
297
298 pub fn terminate(&mut self, cause: Termination) -> &mut Self {
335 self.engine.terminate(cause, self.now);
336 self.render()
337 }
338
339 pub fn key(&mut self, event: KeyEvent) -> &mut Self {
344 self.engine.handle(Event::Key(event), self.now);
345 self.render()
346 }
347
348 pub fn set_theme(&mut self, id: &str) -> &mut Self {
350 self.engine.env.set_theme(id);
351 self.render()
352 }
353
354 pub fn set_locale(&mut self, code: &str) -> &mut Self {
356 self.engine.env.set_locale(code);
357 self.render()
358 }
359
360 pub fn set_region(&mut self, region: Option<&str>) -> &mut Self {
362 self.engine.env.set_region(region);
363 self.render()
364 }
365
366 pub fn set_reduced_motion(&mut self, reduced: bool) -> &mut Self {
368 self.engine.env.set_reduced_motion(reduced);
369 self.render()
370 }
371
372 pub fn set_depth(&mut self, depth: ColorDepth) -> &mut Self {
376 self.engine.env.set_depth(depth);
377 self.render()
378 }
379
380 pub fn set_graphics(&mut self, graphics: crate::graphics::Graphics) -> &mut Self {
386 self.engine.env.set_terminal_graphics(graphics);
387 self.render()
388 }
389
390 pub fn set_remote(&mut self, remote: bool) -> &mut Self {
395 self.engine.env.set_remote(remote);
396 self.render()
397 }
398
399 pub fn set_cell_pixels(&mut self, cell: Option<(u16, u16)>) -> &mut Self {
405 self.engine.env.set_cell_pixels(cell);
406 self.render()
407 }
408
409 pub fn set_glyph_mode(&mut self, mode: GlyphMode) -> &mut Self {
411 self.engine.env.set_glyph_mode(mode);
412 self.render()
413 }
414
415 pub fn resize(&mut self, width: u16, height: u16) -> &mut Self {
419 self.buffer = Buffer::empty(BufferRect::new(0, 0, width, height));
420 self.engine.dirty = true;
421 self.render()
422 }
423
424 #[must_use]
427 pub fn screen(&self) -> String {
428 let mut out = String::new();
429 for y in 0..self.buffer.area.height {
430 out.push_str(self.row(y).0.trim_end());
431 out.push('\n');
432 }
433 out
434 }
435
436 #[must_use]
439 pub fn html(&self, caption: &str) -> String {
440 let area = self.buffer.area;
441 let escape = |text: &str| text.replace('&', "&").replace('<', "<").replace('>', ">");
442 let css = |color: Color| rgb(color).map_or_else(|| "inherit".to_owned(), |c| c.to_string());
443 let mut out = format!("<figure><figcaption>{}</figcaption><div class=\"screen\">", escape(caption));
444 for y in 0..area.height {
445 out.push_str("<div class=\"row\">");
446 for x in visible_columns(&self.buffer, y) {
447 let cell = &self.buffer[(x, y)];
448 let modifier = cell.modifier;
449 let weight = if modifier.contains(Modifier::BOLD) { "font-weight:700;" } else { "" };
450 let style = if modifier.contains(Modifier::ITALIC) { "font-style:italic;" } else { "" };
451 let line = if modifier.contains(Modifier::UNDERLINED) { "text-decoration:underline;" } else { "" };
452 out.push_str(&format!(
453 "<span style=\"color:{};background:{};width:{}ch;{weight}{style}{line}\">{}</span>",
454 css(cell.fg),
455 css(cell.bg),
456 crate::text::width(cell.symbol()).max(1),
457 escape(cell.symbol())
458 ));
459 }
460 out.push_str("</div>");
461 }
462 out.push_str("</div></figure>");
463 out
464 }
465
466 #[must_use]
469 pub fn find(&self, text: &str) -> Option<(i32, i32)> {
470 (0..self.buffer.area.height).find_map(|y| {
471 let (line, columns) = self.row(y);
472 line.find(text).map(|byte| (i32::from(columns[byte]), i32::from(y)))
473 })
474 }
475
476 fn row(&self, y: u16) -> (String, Vec<u16>) {
478 let mut line = String::new();
479 let mut columns = Vec::new();
480 for x in visible_columns(&self.buffer, y) {
481 let symbol = self.buffer[(x, y)].symbol();
482 columns.extend(std::iter::repeat_n(x, symbol.len()));
483 line.push_str(symbol);
484 }
485 (line, columns)
486 }
487
488 #[must_use]
494 pub fn fg(&self, x: u16, y: u16) -> Option<Rgb> {
495 rgb(self.buffer[(x, y)].fg)
496 }
497
498 #[must_use]
504 pub fn bg(&self, x: u16, y: u16) -> Option<Rgb> {
505 rgb(self.buffer[(x, y)].bg)
506 }
507
508 #[must_use]
514 pub fn is_bold(&self, x: u16, y: u16) -> bool {
515 self.buffer[(x, y)].modifier.contains(Modifier::BOLD)
516 }
517
518 #[must_use]
520 pub fn buffer(&self) -> &Buffer {
521 &self.buffer
522 }
523
524 #[must_use]
526 pub fn app(&self) -> &A {
527 &self.engine.app
528 }
529
530 #[must_use]
532 pub fn env(&self) -> &Env {
533 &self.engine.env
534 }
535
536 #[must_use]
540 pub fn pointer_shape(&self) -> PointerShape {
541 self.engine.pointer_shape()
542 }
543
544 #[must_use]
546 pub fn copied(&self) -> &[String] {
547 &self.engine.clipboard
548 }
549
550 #[must_use]
552 pub fn clipboard(&self) -> Option<&str> {
553 self.engine.clipboard_text.as_deref()
554 }
555
556 pub fn set_system_clipboard(&mut self, text: Option<&str>) -> &mut Self {
561 let system = super::clipboard::SystemClipboard::Fixed(text.map(str::to_owned));
562 self.engine.clipboard_reader.set_system(system);
563 self
564 }
565
566 #[must_use]
571 pub fn handoffs(&self) -> &[HandoffRequest] {
572 self.engine.handoff_requests()
573 }
574
575 pub fn set_handoff_outcome(&mut self, outcome: HandoffOutcome) -> &mut Self {
578 self.engine.set_handoff_outcome(outcome);
579 self
580 }
581
582 #[must_use]
586 pub fn detached_handoffs(&self) -> &[HandoffRequest] {
587 self.engine.detached_requests()
588 }
589
590 pub fn set_detached_outcome(&mut self, outcome: DetachedOutcome) -> &mut Self {
601 self.engine.set_detached_outcome(outcome);
602 self
603 }
604
605 #[must_use]
613 pub fn opens(&self) -> &[OpenRequest] {
614 self.engine.open_requests()
615 }
616
617 pub fn set_open_outcome(&mut self, outcome: OpenOutcome) -> &mut Self {
619 self.engine.set_open_outcome(outcome);
620 self
621 }
622
623 #[cfg(feature = "updates")]
629 #[must_use]
630 pub fn update_checks(&self) -> &[super::UpdateCheckRequest] {
631 self.engine.update_checks()
632 }
633
634 #[cfg(feature = "updates")]
639 pub fn set_latest_version(&mut self, latest: Option<&str>) -> &mut Self {
640 self.engine.set_latest_version(latest.map(str::to_owned));
641 self.render()
642 }
643
644 #[must_use]
646 pub fn quit_requested(&self) -> bool {
647 self.engine.quit
648 }
649
650 #[must_use]
652 pub fn is_focused(&self, name: &str) -> bool {
653 self.engine.interaction.focused.is_some_and(|id| self.engine.frame.names.get(&id).is_some_and(|n| n == name))
654 }
655}
656
657#[must_use]
659pub fn html_page(fragments: &[String]) -> String {
660 format!(
661 "<!doctype html><meta charset=\"utf-8\"><title>Quvyta review</title><style>\
662 body{{background:#050507;margin:24px;font-family:'JetBrainsMono Nerd Font Mono','JetBrains Mono',monospace}}\
663 figure{{margin:0 0 28px}}figcaption{{color:#8a8f99;font:12px sans-serif;margin-bottom:6px}}\
664 .screen{{display:inline-block;font-size:14px;line-height:19px;white-space:pre}}\
665 .row{{display:flex;height:19px}}.row span{{display:inline-block;overflow:hidden}}</style>{}",
666 fragments.concat()
667 )
668}
669
670fn visible_columns(buffer: &Buffer, y: u16) -> impl Iterator<Item = u16> + '_ {
674 let mut covered = 0u16;
675 (0..buffer.area.width).filter(move |&x| {
676 if covered > 0 {
677 covered -= 1;
678 return false;
679 }
680 let symbol = buffer[(x, y)].symbol();
681 covered = crate::text::width(symbol).saturating_sub(1);
682 !symbol.is_empty()
683 })
684}
685
686fn rgb(color: Color) -> Option<Rgb> {
687 match color {
688 Color::Rgb(r, g, b) => Some(Rgb::new(r, g, b)),
689 _ => None,
690 }
691}
692
693#[cfg(test)]
694mod resize_tests {
695 use super::Harness;
696 use crate::runtime::{App, Command};
697 use crate::widget::View;
698 use crate::widgets::Text;
699
700 struct Greeting;
701
702 impl App for Greeting {
703 type Msg = ();
704 fn update(&mut self, (): ()) -> Command<()> {
705 Command::none()
706 }
707 fn view(&self, ui: &mut View<'_, ()>) {
708 ui.add(Text::new("container engines"));
709 }
710 }
711
712 #[test]
713 fn resize_redraws_the_whole_screen_at_the_new_size() {
714 let mut harness = Harness::new(Greeting, 30, 2);
715 assert_eq!(harness.screen(), "container engines\n\n");
716 harness.resize(9, 1);
717 assert_eq!(harness.screen(), "container\n");
718 harness.resize(0, 0);
719 assert_eq!(harness.screen(), "");
720 harness.resize(40, 3);
721 assert_eq!((harness.buffer().area.width, harness.buffer().area.height), (40, 3));
722 assert_eq!(harness.screen(), "container engines\n\n\n");
723 }
724
725 struct Raw;
726
727 impl App for Raw {
728 type Msg = ();
729 fn update(&mut self, (): ()) -> Command<()> {
730 Command::none()
731 }
732 fn view(&self, ui: &mut View<'_, ()>) {
733 ui.add(Text::new("bell\u{7} tab\t\u{1b}[1mé\r"));
734 }
735 }
736
737 #[test]
738 fn a_control_character_handed_to_any_widget_never_reaches_a_cell() {
739 let harness = Harness::new(Raw, 30, 1);
740 assert_eq!(harness.screen(), "bell tab [1mé\n", "each control character is a blank cell");
741 }
742}
743
744#[cfg(test)]
745mod handoff_tests {
746 use std::ffi::OsString;
747
748 use super::Harness;
749 use crate::runtime::{App, Command, Handoff, HandoffOutcome};
750 use crate::widget::View;
751 use crate::widgets::{Button, Text};
752
753 #[derive(Default)]
755 struct Installer {
756 outcomes: Vec<HandoffOutcome>,
757 }
758
759 #[derive(Clone)]
760 enum Msg {
761 Authorize,
762 Done(HandoffOutcome),
763 }
764
765 impl App for Installer {
766 type Msg = Msg;
767 fn update(&mut self, msg: Msg) -> Command<Msg> {
768 match msg {
769 Msg::Authorize => Command::handoff(
770 Handoff::new("sudo", Msg::Done).arg("-v").notice("Authorizing the installation").pause(false),
771 ),
772 Msg::Done(outcome) => {
773 self.outcomes.push(outcome);
774 Command::none()
775 }
776 }
777 }
778 fn view(&self, ui: &mut View<'_, Msg>) {
779 ui.add(Button::new("Authorize").on_press(Msg::Authorize)).id("authorize");
780 let text = match self.outcomes.last() {
781 None => "not asked yet".to_owned(),
782 Some(HandoffOutcome::Finished { code }) => format!("finished {code:?}"),
783 Some(HandoffOutcome::Failed(reason)) => format!("failed {reason}"),
784 };
785 ui.add(Text::new(text));
786 }
787 }
788
789 #[test]
790 fn a_handoff_is_recorded_and_answered_with_the_outcome_the_test_set() {
791 let mut harness = Harness::new(Installer::default(), 40, 3);
792 assert!(harness.handoffs().is_empty(), "nothing was asked for yet");
793 harness.send(Msg::Authorize);
794 let asked = harness.handoffs();
795 assert_eq!(asked.len(), 1);
796 assert_eq!(asked[0].program, OsString::from("sudo"));
797 assert_eq!(asked[0].args, vec![OsString::from("-v")]);
798 assert_eq!(asked[0].notice.as_deref(), Some("Authorizing the installation"));
799 assert!(!asked[0].pause);
800 assert_eq!(harness.app().outcomes, [HandoffOutcome::Finished { code: Some(0) }]);
802 assert!(harness.screen().contains("finished Some(0)"), "{}", harness.screen());
803 }
804
805 #[test]
806 fn the_outcome_a_test_sets_reaches_the_application() {
807 let mut harness = Harness::new(Installer::default(), 40, 3);
808 harness.set_handoff_outcome(HandoffOutcome::Finished { code: Some(1) });
809 harness.send(Msg::Authorize);
810 assert_eq!(harness.app().outcomes, [HandoffOutcome::Finished { code: Some(1) }]);
811 harness.set_handoff_outcome(HandoffOutcome::Failed("sudo is not installed".to_owned()));
812 harness.send(Msg::Authorize);
813 assert_eq!(harness.app().outcomes.len(), 2);
814 assert!(harness.screen().contains("failed sudo is not installed"), "{}", harness.screen());
815 assert_eq!(harness.handoffs().len(), 2, "both requests are kept, oldest first");
816 }
817
818 #[test]
819 fn several_handoffs_are_answered_one_after_another() {
820 let mut harness = Harness::new(Installer::default(), 40, 3);
821 harness.send(Msg::Authorize).send(Msg::Authorize).send(Msg::Authorize);
822 assert_eq!(harness.handoffs().len(), 3);
823 assert_eq!(harness.app().outcomes.len(), 3);
824 }
825}
826
827#[cfg(test)]
828mod wide_text_tests {
829 use super::Harness;
830 use crate::runtime::{App, Command};
831 use crate::widget::View;
832 use crate::widgets::{Button, Text};
833
834 #[derive(Default)]
836 struct Firewall {
837 presses: u32,
838 }
839
840 impl App for Firewall {
841 type Msg = ();
842 fn update(&mut self, (): ()) -> Command<()> {
843 self.presses += 1;
844 Command::none()
845 }
846 fn view(&self, ui: &mut View<'_, ()>) {
847 ui.add(Text::new("状态 防火墙 on"));
848 ui.add(Button::new("启用").on_press(()));
849 }
850 }
851
852 fn with_covered_cells_as_spaces(harness: &mut Harness<Firewall>) {
856 let area = harness.buffer.area;
857 for y in 0..area.height {
858 let mut covered = 0;
859 for x in 0..area.width {
860 let cell = &mut harness.buffer[(x, y)];
861 if covered > 0 {
862 covered -= 1;
863 cell.reset();
864 continue;
865 }
866 covered = crate::text::width(cell.symbol()).saturating_sub(1);
867 }
868 }
869 }
870
871 fn firewall() -> Harness<Firewall> {
872 let mut harness = Harness::new(Firewall::default(), 30, 3);
873 with_covered_cells_as_spaces(&mut harness);
874 harness
875 }
876
877 #[test]
878 fn the_screen_reads_wide_text_without_gaps() {
879 let harness = firewall();
880 let screen = harness.screen();
881 assert!(screen.starts_with("状态 防火墙 on\n"), "{screen}");
882 assert!(screen.contains("防火墙"), "{screen}");
883 }
884
885 #[test]
886 fn find_gives_the_column_a_wide_text_is_drawn_in() {
887 let harness = firewall();
888 assert_eq!(harness.find("防火墙"), Some((5, 0)));
889 assert_eq!(harness.find("on"), Some((12, 0)), "text after wide characters keeps its column");
890 let (x, y) = harness.find("启用").expect("the button label is on screen");
891 assert_eq!(harness.buffer()[(u16::try_from(x).unwrap(), u16::try_from(y).unwrap())].symbol(), "启");
892 }
893
894 #[test]
895 fn click_text_presses_a_wide_label() {
896 let mut harness = firewall();
897 harness.click_text("启用");
898 assert_eq!(harness.app().presses, 1);
899 }
900
901 #[test]
902 fn html_draws_a_wide_character_once() {
903 let harness = firewall();
904 let html = harness.html("wide");
905 let first_row = html.split("<div class=\"row\">").nth(1).expect("a first row");
906 assert_eq!(first_row.matches("<span").count(), 30 - 5, "five characters take two cells each: {first_row}");
907 assert!(first_row.contains(">防</span><span"), "{first_row}");
908 }
909}
910
911#[cfg(test)]
912mod graphics_tests {
913 use super::Harness;
914 use crate::color::ColorDepth;
915 use crate::graphics::Graphics;
916 use crate::icons::GlyphMode;
917 use crate::runtime::{App, Command};
918 use crate::widget::View;
919 use crate::widgets::Text;
920
921 struct Pictures;
923
924 impl App for Pictures {
925 type Msg = ();
926 fn update(&mut self, (): ()) -> Command<()> {
927 Command::none()
928 }
929 fn view(&self, ui: &mut View<'_, ()>) {
930 let graphics = ui.env().graphics();
931 ui.add(Text::new(graphics.name()));
932 }
933 }
934
935 #[test]
936 fn a_harness_answers_half_blocks_until_told_otherwise() {
937 let mut harness = Harness::new(Pictures, 20, 1);
938 assert_eq!(harness.screen(), "halfblock\n", "no terminal is asked in a test");
939 harness.set_graphics(Graphics::Kitty);
940 assert_eq!(harness.screen(), "kitty\n");
941 harness.set_graphics(Graphics::Sixel);
942 assert_eq!(harness.screen(), "sixel\n");
943 }
944
945 #[test]
946 fn the_rules_still_apply_to_what_a_harness_is_told() {
947 let mut harness = Harness::new(Pictures, 20, 1);
948 harness.set_graphics(Graphics::Kitty).set_depth(ColorDepth::Ansi16);
949 assert_eq!(harness.screen(), "none\n", "16 colours show no picture");
950 harness.set_depth(ColorDepth::TrueColor).set_glyph_mode(GlyphMode::Ascii);
951 assert_eq!(harness.screen(), "none\n", "ASCII glyphs show no picture");
952 harness.set_glyph_mode(GlyphMode::Unicode);
953 assert_eq!(harness.screen(), "kitty\n");
954 }
955
956 struct Link;
958
959 impl App for Link {
960 type Msg = ();
961 fn update(&mut self, (): ()) -> Command<()> {
962 Command::none()
963 }
964 fn view(&self, ui: &mut View<'_, ()>) {
965 ui.add(Text::new(if ui.env().remote() { "remote" } else { "local" }));
966 }
967 }
968
969 #[test]
970 fn a_harness_draws_a_remote_screen_when_told_so() {
971 let mut harness = Harness::new(Link, 20, 1);
972 assert_eq!(harness.screen(), "local\n", "a test draws the same wherever it runs");
973 harness.set_remote(true);
974 assert_eq!(harness.screen(), "remote\n");
975 harness.set_remote(false);
976 assert_eq!(harness.screen(), "local\n");
977 }
978
979 struct Cells;
981
982 impl App for Cells {
983 type Msg = ();
984 fn update(&mut self, (): ()) -> Command<()> {
985 Command::none()
986 }
987 fn view(&self, ui: &mut View<'_, ()>) {
988 let told = ui.env().cell_pixels().map_or_else(|| "none".to_owned(), |(w, h)| format!("{w}x{h}"));
989 ui.add(Text::new(told));
990 }
991 }
992
993 #[test]
994 fn a_harness_knows_no_cell_size_until_told_and_then_the_latest_one() {
995 let mut harness = Harness::new(Cells, 20, 1);
996 assert_eq!(
997 harness.screen(),
998 "none
999",
1000 "no terminal is asked in a test"
1001 );
1002 harness.set_cell_pixels(Some((9, 19)));
1003 assert_eq!(
1004 harness.screen(),
1005 "9x19
1006"
1007 );
1008 harness.set_cell_pixels(Some((12, 26)));
1009 assert_eq!(
1010 harness.screen(),
1011 "12x26
1012",
1013 "a larger font, the same columns and rows"
1014 );
1015 harness.set_cell_pixels(None);
1016 assert_eq!(
1017 harness.screen(),
1018 "none
1019"
1020 );
1021 }
1022}