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::{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(
71 app: A,
72 ecosystem: crate::storage::Ecosystem,
73 config_dir: &std::path::Path,
74 name: &str,
75 width: u16,
76 height: u16,
77 ) -> Self {
78 let mut env = Env::builtin();
79 let start = Start { theme: None, settings: None, preferences: None };
80 let follow = start.apply(&mut env, Some(Member::new(ecosystem, name, Some(config_dir.to_path_buf()))), false);
81 let mut engine = Engine::new(app, env, TaskMode::Inline);
82 if let Some(follow) = follow {
83 engine.follow(follow);
84 }
85 let mut harness =
86 Self { engine, buffer: Buffer::empty(BufferRect::new(0, 0, width, height)), now: Duration::ZERO };
87 harness.render();
88 harness
89 }
90
91 pub fn poll_preferences(&mut self) -> &mut Self {
97 self.engine.check_preferences();
98 if self.engine.dirty {
99 self.render();
100 }
101 self
102 }
103
104 pub fn render(&mut self) -> &mut Self {
106 self.settle_tasks();
107 self.engine.render(&mut self.buffer, self.now);
108 self.write_out();
109 for _ in 0..3 {
111 let due = self.engine.deadline().is_some_and(|deadline| deadline <= self.now);
112 if !self.engine.dirty && !due {
113 break;
114 }
115 self.engine.render(&mut self.buffer, self.now);
116 self.write_out();
117 }
118 self
119 }
120
121 #[cfg(all(test, feature = "image"))]
124 pub(crate) fn present_to<W: std::io::Write>(&mut self, screen: &mut super::present::Screen<W>) -> bool {
125 let now = self.now;
126 let engine = &mut self.engine;
127 screen
128 .present(|buffer| {
129 engine.render(buffer, now);
130 engine.painted()
131 })
132 .expect("a frame")
133 }
134
135 #[cfg(test)]
139 pub(crate) fn next_frame(&self) -> Option<Duration> {
140 self.engine.deadline()
141 }
142
143 fn write_out(&self) {
147 let blank = Buffer::empty(self.buffer.area);
148 let _ = blank.diff(&self.buffer);
149 }
150
151 fn settle_tasks(&mut self) {
156 self.engine.run_queued_work();
157 loop {
158 self.engine.task_clock.settle(self.now);
159 if self.engine.poll_tasks() == 0 {
160 break;
161 }
162 }
163 }
164
165 pub fn send(&mut self, message: A::Msg) -> &mut Self {
167 self.engine.update(message);
168 self.render()
169 }
170
171 pub fn press(&mut self, chord: &str) -> &mut Self {
173 self.now += KEY_INTERVAL;
174 self.engine.handle(Event::Key(KeyEvent::press(chord)), self.now);
175 self.render()
176 }
177
178 pub fn type_text(&mut self, text: &str) -> &mut Self {
180 for c in text.chars() {
181 let chord = match c {
182 ' ' => "space".to_owned(),
183 '+' => "+".to_owned(),
184 c if c.is_uppercase() => format!("shift+{}", c.to_lowercase()),
185 c => c.to_string(),
186 };
187 self.press(&chord);
188 }
189 self
190 }
191
192 pub fn events(&mut self, events: &[Event]) -> &mut Self {
199 for event in events {
200 self.engine.handle(event.clone(), self.now);
201 }
202 self.render()
203 }
204
205 #[cfg(test)]
207 pub(crate) fn inject(&mut self, event: Event, at: Duration) -> &mut Self {
208 self.engine.handle(event, at);
209 self.render()
210 }
211
212 pub fn paste(&mut self, text: &str) -> &mut Self {
214 self.engine.handle(Event::Paste(text.to_owned()), self.now);
215 self.render()
216 }
217
218 pub fn click(&mut self, x: i32, y: i32) -> &mut Self {
220 self.mouse(MouseKind::Down(MouseButton::Left), x, y);
221 self.mouse(MouseKind::Up(MouseButton::Left), x, y)
222 }
223
224 pub fn click_text(&mut self, text: &str) -> &mut Self {
230 let (x, y) = self.find(text).unwrap_or_else(|| panic!("`{text}` is not on screen:\n{}", self.screen()));
231 self.click(x, y)
232 }
233
234 pub fn drag(&mut self, from: (i32, i32), to: (i32, i32)) -> &mut Self {
236 self.mouse(MouseKind::Down(MouseButton::Left), from.0, from.1);
237 self.mouse(MouseKind::Drag(MouseButton::Left), to.0, to.1);
238 self.mouse(MouseKind::Up(MouseButton::Left), to.0, to.1)
239 }
240
241 pub fn hover(&mut self, x: i32, y: i32) -> &mut Self {
243 self.mouse(MouseKind::Moved, x, y)
244 }
245
246 pub fn mouse(&mut self, kind: MouseKind, x: i32, y: i32) -> &mut Self {
248 self.engine.handle(Event::Mouse(MouseEvent { kind, x, y, mods: Modifiers::default() }), self.now);
249 self.render()
250 }
251
252 pub fn advance(&mut self, duration: Duration) -> &mut Self {
261 self.now += duration;
262 self.engine.tick(self.now);
263 self.engine.end_when_due(self.now);
264 self.render()
265 }
266
267 pub fn terminate(&mut self, cause: Termination) -> &mut Self {
304 self.engine.terminate(cause, self.now);
305 self.render()
306 }
307
308 pub fn key(&mut self, event: KeyEvent) -> &mut Self {
313 self.engine.handle(Event::Key(event), self.now);
314 self.render()
315 }
316
317 pub fn set_theme(&mut self, id: &str) -> &mut Self {
319 self.engine.env.set_theme(id);
320 self.render()
321 }
322
323 pub fn set_locale(&mut self, code: &str) -> &mut Self {
325 self.engine.env.set_locale(code);
326 self.render()
327 }
328
329 pub fn set_region(&mut self, region: Option<&str>) -> &mut Self {
331 self.engine.env.set_region(region);
332 self.render()
333 }
334
335 pub fn set_reduced_motion(&mut self, reduced: bool) -> &mut Self {
337 self.engine.env.set_reduced_motion(reduced);
338 self.render()
339 }
340
341 pub fn set_depth(&mut self, depth: ColorDepth) -> &mut Self {
345 self.engine.env.set_depth(depth);
346 self.render()
347 }
348
349 pub fn set_graphics(&mut self, graphics: crate::graphics::Graphics) -> &mut Self {
355 self.engine.env.set_terminal_graphics(graphics);
356 self.render()
357 }
358
359 pub fn set_remote(&mut self, remote: bool) -> &mut Self {
364 self.engine.env.set_remote(remote);
365 self.render()
366 }
367
368 pub fn set_cell_pixels(&mut self, cell: Option<(u16, u16)>) -> &mut Self {
374 self.engine.env.set_cell_pixels(cell);
375 self.render()
376 }
377
378 pub fn set_glyph_mode(&mut self, mode: GlyphMode) -> &mut Self {
380 self.engine.env.set_glyph_mode(mode);
381 self.render()
382 }
383
384 pub fn resize(&mut self, width: u16, height: u16) -> &mut Self {
388 self.buffer = Buffer::empty(BufferRect::new(0, 0, width, height));
389 self.engine.dirty = true;
390 self.render()
391 }
392
393 #[must_use]
396 pub fn screen(&self) -> String {
397 let mut out = String::new();
398 for y in 0..self.buffer.area.height {
399 out.push_str(self.row(y).0.trim_end());
400 out.push('\n');
401 }
402 out
403 }
404
405 #[must_use]
408 pub fn html(&self, caption: &str) -> String {
409 let area = self.buffer.area;
410 let escape = |text: &str| text.replace('&', "&").replace('<', "<").replace('>', ">");
411 let css = |color: Color| rgb(color).map_or_else(|| "inherit".to_owned(), |c| c.to_string());
412 let mut out = format!("<figure><figcaption>{}</figcaption><div class=\"screen\">", escape(caption));
413 for y in 0..area.height {
414 out.push_str("<div class=\"row\">");
415 for x in visible_columns(&self.buffer, y) {
416 let cell = &self.buffer[(x, y)];
417 let modifier = cell.modifier;
418 let weight = if modifier.contains(Modifier::BOLD) { "font-weight:700;" } else { "" };
419 let style = if modifier.contains(Modifier::ITALIC) { "font-style:italic;" } else { "" };
420 let line = if modifier.contains(Modifier::UNDERLINED) { "text-decoration:underline;" } else { "" };
421 out.push_str(&format!(
422 "<span style=\"color:{};background:{};width:{}ch;{weight}{style}{line}\">{}</span>",
423 css(cell.fg),
424 css(cell.bg),
425 crate::text::width(cell.symbol()).max(1),
426 escape(cell.symbol())
427 ));
428 }
429 out.push_str("</div>");
430 }
431 out.push_str("</div></figure>");
432 out
433 }
434
435 #[must_use]
438 pub fn find(&self, text: &str) -> Option<(i32, i32)> {
439 (0..self.buffer.area.height).find_map(|y| {
440 let (line, columns) = self.row(y);
441 line.find(text).map(|byte| (i32::from(columns[byte]), i32::from(y)))
442 })
443 }
444
445 fn row(&self, y: u16) -> (String, Vec<u16>) {
447 let mut line = String::new();
448 let mut columns = Vec::new();
449 for x in visible_columns(&self.buffer, y) {
450 let symbol = self.buffer[(x, y)].symbol();
451 columns.extend(std::iter::repeat_n(x, symbol.len()));
452 line.push_str(symbol);
453 }
454 (line, columns)
455 }
456
457 #[must_use]
463 pub fn fg(&self, x: u16, y: u16) -> Option<Rgb> {
464 rgb(self.buffer[(x, y)].fg)
465 }
466
467 #[must_use]
473 pub fn bg(&self, x: u16, y: u16) -> Option<Rgb> {
474 rgb(self.buffer[(x, y)].bg)
475 }
476
477 #[must_use]
483 pub fn is_bold(&self, x: u16, y: u16) -> bool {
484 self.buffer[(x, y)].modifier.contains(Modifier::BOLD)
485 }
486
487 #[must_use]
489 pub fn buffer(&self) -> &Buffer {
490 &self.buffer
491 }
492
493 #[must_use]
495 pub fn app(&self) -> &A {
496 &self.engine.app
497 }
498
499 #[must_use]
501 pub fn env(&self) -> &Env {
502 &self.engine.env
503 }
504
505 #[must_use]
509 pub fn pointer_shape(&self) -> PointerShape {
510 self.engine.pointer_shape()
511 }
512
513 #[must_use]
515 pub fn copied(&self) -> &[String] {
516 &self.engine.clipboard
517 }
518
519 #[must_use]
521 pub fn clipboard(&self) -> Option<&str> {
522 self.engine.clipboard_text.as_deref()
523 }
524
525 pub fn set_system_clipboard(&mut self, text: Option<&str>) -> &mut Self {
530 let system = super::clipboard::SystemClipboard::Fixed(text.map(str::to_owned));
531 self.engine.clipboard_reader.set_system(system);
532 self
533 }
534
535 #[must_use]
540 pub fn handoffs(&self) -> &[HandoffRequest] {
541 self.engine.handoff_requests()
542 }
543
544 pub fn set_handoff_outcome(&mut self, outcome: HandoffOutcome) -> &mut Self {
547 self.engine.set_handoff_outcome(outcome);
548 self
549 }
550
551 #[must_use]
555 pub fn detached_handoffs(&self) -> &[HandoffRequest] {
556 self.engine.detached_requests()
557 }
558
559 pub fn set_detached_outcome(&mut self, outcome: DetachedOutcome) -> &mut Self {
570 self.engine.set_detached_outcome(outcome);
571 self
572 }
573
574 #[must_use]
582 pub fn opens(&self) -> &[OpenRequest] {
583 self.engine.open_requests()
584 }
585
586 pub fn set_open_outcome(&mut self, outcome: OpenOutcome) -> &mut Self {
588 self.engine.set_open_outcome(outcome);
589 self
590 }
591
592 #[cfg(feature = "updates")]
598 #[must_use]
599 pub fn update_checks(&self) -> &[super::UpdateCheckRequest] {
600 self.engine.update_checks()
601 }
602
603 #[cfg(feature = "updates")]
608 pub fn set_latest_version(&mut self, latest: Option<&str>) -> &mut Self {
609 self.engine.set_latest_version(latest.map(str::to_owned));
610 self.render()
611 }
612
613 #[must_use]
615 pub fn quit_requested(&self) -> bool {
616 self.engine.quit
617 }
618
619 #[must_use]
621 pub fn is_focused(&self, name: &str) -> bool {
622 self.engine.interaction.focused.is_some_and(|id| self.engine.frame.names.get(&id).is_some_and(|n| n == name))
623 }
624}
625
626#[must_use]
628pub fn html_page(fragments: &[String]) -> String {
629 format!(
630 "<!doctype html><meta charset=\"utf-8\"><title>Quvyta review</title><style>\
631 body{{background:#050507;margin:24px;font-family:'JetBrainsMono Nerd Font Mono','JetBrains Mono',monospace}}\
632 figure{{margin:0 0 28px}}figcaption{{color:#8a8f99;font:12px sans-serif;margin-bottom:6px}}\
633 .screen{{display:inline-block;font-size:14px;line-height:19px;white-space:pre}}\
634 .row{{display:flex;height:19px}}.row span{{display:inline-block;overflow:hidden}}</style>{}",
635 fragments.concat()
636 )
637}
638
639fn visible_columns(buffer: &Buffer, y: u16) -> impl Iterator<Item = u16> + '_ {
643 let mut covered = 0u16;
644 (0..buffer.area.width).filter(move |&x| {
645 if covered > 0 {
646 covered -= 1;
647 return false;
648 }
649 let symbol = buffer[(x, y)].symbol();
650 covered = crate::text::width(symbol).saturating_sub(1);
651 !symbol.is_empty()
652 })
653}
654
655fn rgb(color: Color) -> Option<Rgb> {
656 match color {
657 Color::Rgb(r, g, b) => Some(Rgb::new(r, g, b)),
658 _ => None,
659 }
660}
661
662#[cfg(test)]
663mod resize_tests {
664 use super::Harness;
665 use crate::runtime::{App, Command};
666 use crate::widget::View;
667 use crate::widgets::Text;
668
669 struct Greeting;
670
671 impl App for Greeting {
672 type Msg = ();
673 fn update(&mut self, (): ()) -> Command<()> {
674 Command::none()
675 }
676 fn view(&self, ui: &mut View<'_, ()>) {
677 ui.add(Text::new("container engines"));
678 }
679 }
680
681 #[test]
682 fn resize_redraws_the_whole_screen_at_the_new_size() {
683 let mut harness = Harness::new(Greeting, 30, 2);
684 assert_eq!(harness.screen(), "container engines\n\n");
685 harness.resize(9, 1);
686 assert_eq!(harness.screen(), "container\n");
687 harness.resize(0, 0);
688 assert_eq!(harness.screen(), "");
689 harness.resize(40, 3);
690 assert_eq!((harness.buffer().area.width, harness.buffer().area.height), (40, 3));
691 assert_eq!(harness.screen(), "container engines\n\n\n");
692 }
693
694 struct Raw;
695
696 impl App for Raw {
697 type Msg = ();
698 fn update(&mut self, (): ()) -> Command<()> {
699 Command::none()
700 }
701 fn view(&self, ui: &mut View<'_, ()>) {
702 ui.add(Text::new("bell\u{7} tab\t\u{1b}[1mé\r"));
703 }
704 }
705
706 #[test]
707 fn a_control_character_handed_to_any_widget_never_reaches_a_cell() {
708 let harness = Harness::new(Raw, 30, 1);
709 assert_eq!(harness.screen(), "bell tab [1mé\n", "each control character is a blank cell");
710 }
711}
712
713#[cfg(test)]
714mod handoff_tests {
715 use std::ffi::OsString;
716
717 use super::Harness;
718 use crate::runtime::{App, Command, Handoff, HandoffOutcome};
719 use crate::widget::View;
720 use crate::widgets::{Button, Text};
721
722 #[derive(Default)]
724 struct Installer {
725 outcomes: Vec<HandoffOutcome>,
726 }
727
728 #[derive(Clone)]
729 enum Msg {
730 Authorize,
731 Done(HandoffOutcome),
732 }
733
734 impl App for Installer {
735 type Msg = Msg;
736 fn update(&mut self, msg: Msg) -> Command<Msg> {
737 match msg {
738 Msg::Authorize => Command::handoff(
739 Handoff::new("sudo", Msg::Done).arg("-v").notice("Authorizing the installation").pause(false),
740 ),
741 Msg::Done(outcome) => {
742 self.outcomes.push(outcome);
743 Command::none()
744 }
745 }
746 }
747 fn view(&self, ui: &mut View<'_, Msg>) {
748 ui.add(Button::new("Authorize").on_press(Msg::Authorize)).id("authorize");
749 let text = match self.outcomes.last() {
750 None => "not asked yet".to_owned(),
751 Some(HandoffOutcome::Finished { code }) => format!("finished {code:?}"),
752 Some(HandoffOutcome::Failed(reason)) => format!("failed {reason}"),
753 };
754 ui.add(Text::new(text));
755 }
756 }
757
758 #[test]
759 fn a_handoff_is_recorded_and_answered_with_the_outcome_the_test_set() {
760 let mut harness = Harness::new(Installer::default(), 40, 3);
761 assert!(harness.handoffs().is_empty(), "nothing was asked for yet");
762 harness.send(Msg::Authorize);
763 let asked = harness.handoffs();
764 assert_eq!(asked.len(), 1);
765 assert_eq!(asked[0].program, OsString::from("sudo"));
766 assert_eq!(asked[0].args, vec![OsString::from("-v")]);
767 assert_eq!(asked[0].notice.as_deref(), Some("Authorizing the installation"));
768 assert!(!asked[0].pause);
769 assert_eq!(harness.app().outcomes, [HandoffOutcome::Finished { code: Some(0) }]);
771 assert!(harness.screen().contains("finished Some(0)"), "{}", harness.screen());
772 }
773
774 #[test]
775 fn the_outcome_a_test_sets_reaches_the_application() {
776 let mut harness = Harness::new(Installer::default(), 40, 3);
777 harness.set_handoff_outcome(HandoffOutcome::Finished { code: Some(1) });
778 harness.send(Msg::Authorize);
779 assert_eq!(harness.app().outcomes, [HandoffOutcome::Finished { code: Some(1) }]);
780 harness.set_handoff_outcome(HandoffOutcome::Failed("sudo is not installed".to_owned()));
781 harness.send(Msg::Authorize);
782 assert_eq!(harness.app().outcomes.len(), 2);
783 assert!(harness.screen().contains("failed sudo is not installed"), "{}", harness.screen());
784 assert_eq!(harness.handoffs().len(), 2, "both requests are kept, oldest first");
785 }
786
787 #[test]
788 fn several_handoffs_are_answered_one_after_another() {
789 let mut harness = Harness::new(Installer::default(), 40, 3);
790 harness.send(Msg::Authorize).send(Msg::Authorize).send(Msg::Authorize);
791 assert_eq!(harness.handoffs().len(), 3);
792 assert_eq!(harness.app().outcomes.len(), 3);
793 }
794}
795
796#[cfg(test)]
797mod wide_text_tests {
798 use super::Harness;
799 use crate::runtime::{App, Command};
800 use crate::widget::View;
801 use crate::widgets::{Button, Text};
802
803 #[derive(Default)]
805 struct Firewall {
806 presses: u32,
807 }
808
809 impl App for Firewall {
810 type Msg = ();
811 fn update(&mut self, (): ()) -> Command<()> {
812 self.presses += 1;
813 Command::none()
814 }
815 fn view(&self, ui: &mut View<'_, ()>) {
816 ui.add(Text::new("状态 防火墙 on"));
817 ui.add(Button::new("启用").on_press(()));
818 }
819 }
820
821 fn with_covered_cells_as_spaces(harness: &mut Harness<Firewall>) {
825 let area = harness.buffer.area;
826 for y in 0..area.height {
827 let mut covered = 0;
828 for x in 0..area.width {
829 let cell = &mut harness.buffer[(x, y)];
830 if covered > 0 {
831 covered -= 1;
832 cell.reset();
833 continue;
834 }
835 covered = crate::text::width(cell.symbol()).saturating_sub(1);
836 }
837 }
838 }
839
840 fn firewall() -> Harness<Firewall> {
841 let mut harness = Harness::new(Firewall::default(), 30, 3);
842 with_covered_cells_as_spaces(&mut harness);
843 harness
844 }
845
846 #[test]
847 fn the_screen_reads_wide_text_without_gaps() {
848 let harness = firewall();
849 let screen = harness.screen();
850 assert!(screen.starts_with("状态 防火墙 on\n"), "{screen}");
851 assert!(screen.contains("防火墙"), "{screen}");
852 }
853
854 #[test]
855 fn find_gives_the_column_a_wide_text_is_drawn_in() {
856 let harness = firewall();
857 assert_eq!(harness.find("防火墙"), Some((5, 0)));
858 assert_eq!(harness.find("on"), Some((12, 0)), "text after wide characters keeps its column");
859 let (x, y) = harness.find("启用").expect("the button label is on screen");
860 assert_eq!(harness.buffer()[(u16::try_from(x).unwrap(), u16::try_from(y).unwrap())].symbol(), "启");
861 }
862
863 #[test]
864 fn click_text_presses_a_wide_label() {
865 let mut harness = firewall();
866 harness.click_text("启用");
867 assert_eq!(harness.app().presses, 1);
868 }
869
870 #[test]
871 fn html_draws_a_wide_character_once() {
872 let harness = firewall();
873 let html = harness.html("wide");
874 let first_row = html.split("<div class=\"row\">").nth(1).expect("a first row");
875 assert_eq!(first_row.matches("<span").count(), 30 - 5, "five characters take two cells each: {first_row}");
876 assert!(first_row.contains(">防</span><span"), "{first_row}");
877 }
878}
879
880#[cfg(test)]
881mod graphics_tests {
882 use super::Harness;
883 use crate::color::ColorDepth;
884 use crate::graphics::Graphics;
885 use crate::icons::GlyphMode;
886 use crate::runtime::{App, Command};
887 use crate::widget::View;
888 use crate::widgets::Text;
889
890 struct Pictures;
892
893 impl App for Pictures {
894 type Msg = ();
895 fn update(&mut self, (): ()) -> Command<()> {
896 Command::none()
897 }
898 fn view(&self, ui: &mut View<'_, ()>) {
899 let graphics = ui.env().graphics();
900 ui.add(Text::new(graphics.name()));
901 }
902 }
903
904 #[test]
905 fn a_harness_answers_half_blocks_until_told_otherwise() {
906 let mut harness = Harness::new(Pictures, 20, 1);
907 assert_eq!(harness.screen(), "halfblock\n", "no terminal is asked in a test");
908 harness.set_graphics(Graphics::Kitty);
909 assert_eq!(harness.screen(), "kitty\n");
910 harness.set_graphics(Graphics::Sixel);
911 assert_eq!(harness.screen(), "sixel\n");
912 }
913
914 #[test]
915 fn the_rules_still_apply_to_what_a_harness_is_told() {
916 let mut harness = Harness::new(Pictures, 20, 1);
917 harness.set_graphics(Graphics::Kitty).set_depth(ColorDepth::Ansi16);
918 assert_eq!(harness.screen(), "none\n", "16 colours show no picture");
919 harness.set_depth(ColorDepth::TrueColor).set_glyph_mode(GlyphMode::Ascii);
920 assert_eq!(harness.screen(), "none\n", "ASCII glyphs show no picture");
921 harness.set_glyph_mode(GlyphMode::Unicode);
922 assert_eq!(harness.screen(), "kitty\n");
923 }
924
925 struct Link;
927
928 impl App for Link {
929 type Msg = ();
930 fn update(&mut self, (): ()) -> Command<()> {
931 Command::none()
932 }
933 fn view(&self, ui: &mut View<'_, ()>) {
934 ui.add(Text::new(if ui.env().remote() { "remote" } else { "local" }));
935 }
936 }
937
938 #[test]
939 fn a_harness_draws_a_remote_screen_when_told_so() {
940 let mut harness = Harness::new(Link, 20, 1);
941 assert_eq!(harness.screen(), "local\n", "a test draws the same wherever it runs");
942 harness.set_remote(true);
943 assert_eq!(harness.screen(), "remote\n");
944 harness.set_remote(false);
945 assert_eq!(harness.screen(), "local\n");
946 }
947
948 struct Cells;
950
951 impl App for Cells {
952 type Msg = ();
953 fn update(&mut self, (): ()) -> Command<()> {
954 Command::none()
955 }
956 fn view(&self, ui: &mut View<'_, ()>) {
957 let told = ui.env().cell_pixels().map_or_else(|| "none".to_owned(), |(w, h)| format!("{w}x{h}"));
958 ui.add(Text::new(told));
959 }
960 }
961
962 #[test]
963 fn a_harness_knows_no_cell_size_until_told_and_then_the_latest_one() {
964 let mut harness = Harness::new(Cells, 20, 1);
965 assert_eq!(
966 harness.screen(),
967 "none
968",
969 "no terminal is asked in a test"
970 );
971 harness.set_cell_pixels(Some((9, 19)));
972 assert_eq!(
973 harness.screen(),
974 "9x19
975"
976 );
977 harness.set_cell_pixels(Some((12, 26)));
978 assert_eq!(
979 harness.screen(),
980 "12x26
981",
982 "a larger font, the same columns and rows"
983 );
984 harness.set_cell_pixels(None);
985 assert_eq!(
986 harness.screen(),
987 "none
988"
989 );
990 }
991}