use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command as Shell;
use std::time::Duration;
use super::*;
use crate::i18n::I18n;
use crate::icons::nerd_font::{Archive, FONT_FILE};
use crate::runtime::{App, Harness};
use crate::storage::{Ecosystem, Settings, Shared};
use crate::widgets::{Select, Text};
const APP: &str = "code";
fn folder(name: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!("quvyta-setup-{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&path);
path
}
#[derive(Clone, Debug, PartialEq)]
enum Msg {
Setup(SetupMsg),
Done,
Engine(usize),
}
struct Demo {
setup: Setup<Msg>,
settings: Settings,
engine: usize,
done: bool,
}
impl Demo {
fn new(folder: &Path) -> Self {
let ecosystem = Ecosystem::QUVYTA;
let setup = Setup::new_in(folder, ecosystem, APP, &I18n::builtin(), Msg::Setup)
.on_finish(Msg::Done)
.install(Install::new().target(folder.join("fonts").join("QuvytaNerdFont")).register(false))
.font_dirs(vec![folder.join("fonts")]);
let settings = Settings::open(folder.join("code.conf")).member_of(&ecosystem);
Self { setup, settings, engine: 0, done: false }
}
}
impl App for Demo {
type Msg = Msg;
fn update(&mut self, msg: Msg) -> Command<Msg> {
match msg {
Msg::Setup(message) => self.setup.update(message, &mut self.settings),
Msg::Done => {
self.done = true;
Command::none()
}
Msg::Engine(engine) => {
self.engine = engine;
Command::none()
}
}
}
fn view(&self, ui: &mut View<'_, Msg>) {
if !self.setup.needed() {
ui.add(Text::new("Everything is set"));
return;
}
SetupWizard::new(&self.setup)
.step("Containers", |ui| {
ui.add(Select::new(["podman", "docker"]).selected(Some(self.engine)).on_select(Msg::Engine))
.id("engine");
})
.show(ui);
}
}
fn wizard(name: &str) -> (Harness<Demo>, PathBuf) {
let folder = folder(name);
(Harness::new(Demo::new(&folder), 60, 30), folder)
}
fn file(folder: &Path, name: &str) -> String {
fs::read_to_string(folder.join(name)).unwrap_or_default()
}
#[test]
fn it_opens_only_while_the_application_has_no_file_of_its_own() {
let folder = folder("needed");
fs::create_dir_all(&folder).expect("the folder");
let first = Demo::new(&folder);
assert!(first.setup.needed(), "no code.conf yet");
fs::write(folder.join("code.conf"), "theme = \"iris\"\n").expect("code.conf");
assert!(!Demo::new(&folder).setup.needed(), "the application has been set up");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn closing_it_half_way_writes_nothing_at_all() {
let (mut h, folder) = wizard("half-way");
h.send(Msg::Setup(SetupMsg::Appearance(AppearanceChange::Theme("nordic".to_owned()))));
h.send(Msg::Setup(SetupMsg::Next));
assert_eq!(h.app().setup.step(), 1);
assert!(!folder.join("quvyta.conf").exists(), "no shared file");
assert!(!folder.join("code.conf").exists(), "no application file");
assert!(Demo::new(&folder).setup.needed(), "it comes again next start");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn starting_with_the_defaults_writes_both_files_and_never_asks_again() {
let (mut h, folder) = wizard("defaults");
h.click_text("Start with the defaults");
h.advance(Duration::from_millis(0));
assert!(h.app().done, "the application is told to write its own keys");
assert!(file(&folder, "quvyta.conf").contains("theme = \"monochrome\""), "{}", file(&folder, "quvyta.conf"));
assert!(file(&folder, "code.conf").contains("theme = \"quvyta\""), "{}", file(&folder, "code.conf"));
assert!(!h.app().setup.needed());
assert!(h.screen().contains("Everything is set"), "{}", h.screen());
assert!(!Demo::new(&folder).setup.needed(), "and never again");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn the_box_decides_whether_a_choice_goes_to_the_ecosystem_or_to_the_application() {
let (mut h, folder) = wizard("scope");
h.send(Msg::Setup(SetupMsg::Appearance(AppearanceChange::Language("tr".to_owned()))));
h.send(Msg::Setup(SetupMsg::Appearance(AppearanceChange::Everywhere(Shared::Theme, false))));
h.send(Msg::Setup(SetupMsg::Appearance(AppearanceChange::Theme("nordic".to_owned()))));
h.send(Msg::Setup(SetupMsg::Next));
h.send(Msg::Setup(SetupMsg::Finish));
h.advance(Duration::from_millis(0));
let shared = file(&folder, "quvyta.conf");
let own = file(&folder, "code.conf");
assert!(shared.contains("language = \"tr\""), "the ecosystem takes the language: {shared}");
assert!(own.contains("language = \"quvyta\""), "and the application follows it: {own}");
assert!(own.contains("theme = \"nordic\""), "the theme stays here: {own}");
assert!(!shared.contains("theme"), "a choice made here alone never reaches the ecosystem: {shared}");
assert!(shared.contains("icons = "), "what the boxes keep does reach it: {shared}");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn back_works_on_every_step_and_keeps_the_choices() {
let (mut h, folder) = wizard("back");
h.send(Msg::Setup(SetupMsg::Appearance(AppearanceChange::Theme("iris".to_owned()))));
h.send(Msg::Setup(SetupMsg::Appearance(AppearanceChange::Everywhere(Shared::Icons, false))));
h.click_text("Next");
assert_eq!(h.app().setup.step(), 1);
h.click_text("Back");
assert_eq!(h.app().setup.step(), 0);
assert_eq!(h.env().theme().id(), "iris", "the theme is still the chosen one");
assert_eq!(h.app().setup.preferences().source(Shared::Icons), Source::App, "the cleared box is still cleared");
assert!(h.screen().contains("Iris"), "{}", h.screen());
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn a_theme_chosen_on_the_first_step_draws_the_wizard_in_that_theme_at_once() {
let (mut h, folder) = wizard("theme");
let before = h.bg(0, 0);
h.send(Msg::Setup(SetupMsg::Appearance(AppearanceChange::Theme("nordic".to_owned()))));
assert_eq!(h.env().theme().id(), "nordic");
assert_ne!(h.bg(0, 0), before, "the wizard is drawn in the new theme");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn every_language_names_the_rows_of_the_first_step() {
for (code, appearance, language, defaults) in [
("en", "Appearance", "Language", "Start with the defaults"),
("tr", "Görünüm", "Dil", "Varsayılanlarla başla"),
("de", "Darstellung", "Sprache", "Mit den Vorgaben beginnen"),
] {
let (mut h, folder) = wizard(&format!("language-{code}"));
h.set_locale(code);
let screen = h.screen();
assert!(screen.contains(appearance), "{code}: {screen}");
assert!(screen.contains(language), "{code}: {screen}");
assert!(screen.contains(defaults), "{code}: {screen}");
let _ = fs::remove_dir_all(&folder);
}
}
#[test]
fn forty_columns_cuts_no_word() {
let folder = folder("narrow");
let mut h = Harness::new(Demo::new(&folder), 40, 40);
let screen = h.screen();
for word in ["Appearance", "Language", "In every Quvyta application", "Start with the defaults"] {
assert!(screen.contains(word), "“{word}” is cut: {screen}");
}
assert!(screen.lines().all(|line| line.chars().count() <= 40), "{screen}");
h.click_text("Start with the defaults");
h.advance(Duration::from_millis(0));
assert!(h.app().done, "the narrow layout is clickable too");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn the_three_glyph_modes_show_their_own_icons_beside_their_names() {
let (mut h, folder) = wizard("glyphs");
h.set_glyph_mode(GlyphMode::Ascii);
let screen = h.screen();
assert!(screen.contains("\u{f07b} \u{f00c} \u{f002} \u{f013}"), "the Nerd glyphs: {screen}");
assert!(screen.contains("■ ✓ ⌕ ▤"), "the Unicode glyphs: {screen}");
assert!(screen.contains("# v / *"), "the ASCII glyphs: {screen}");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn the_install_offer_comes_only_without_a_nerd_font_and_says_what_to_look_at() {
let folder = folder("install");
fs::create_dir_all(folder.join("content")).expect("the archive content");
fs::write(folder.join("content").join(FONT_FILE), b"glyphs").expect("the font");
let archive = folder.join("symbols.tar");
let packed = Shell::new("tar")
.arg("-cf")
.arg(&archive)
.arg("-C")
.arg(folder.join("content"))
.arg(FONT_FILE)
.status()
.expect("tar");
assert!(packed.success());
let digest = Shell::new("sha256sum").arg(&archive).output().expect("sha256sum");
let digest = String::from_utf8_lossy(&digest.stdout).split_whitespace().next().expect("a digest").to_owned();
let mut demo = Demo::new(&folder);
demo.setup = demo
.setup
.install(
Install::new()
.archive(Archive::new(format!("file://{}", archive.display()), &digest))
.target(folder.join("fonts").join("QuvytaNerdFont"))
.register(false),
)
.font_dirs(vec![folder.join("fonts")]);
let mut h = Harness::new(demo, 60, 30);
assert!(h.screen().contains("No Nerd Font was found on this machine"), "{}", h.screen());
h.click_text("Install the font");
h.advance(Duration::from_millis(0));
let screen = h.screen();
assert!(folder.join("fonts/QuvytaNerdFont").join(FONT_FILE).is_file(), "{screen}");
assert!(screen.contains("JetBrainsMono Nerd Font"), "the honest next step: {screen}");
assert!(!screen.contains("Install the font"), "nothing left to install: {screen}");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn a_finish_that_cannot_be_saved_says_so_and_the_wizard_comes_again() {
let folder = folder("failure");
fs::create_dir_all(folder.parent().expect("a parent")).expect("the parent folder");
fs::write(&folder, b"not a folder").expect("the file in the way");
let mut h = Harness::new(Demo::new(&folder), 60, 30);
h.click_text("Start with the defaults");
h.advance(Duration::from_millis(0));
assert!(!h.app().done, "the application is not told it is over");
assert!(h.app().setup.needed(), "the wizard stays");
assert!(h.screen().contains("could not be saved"), "{}", h.screen());
let _ = fs::remove_file(&folder);
}
const ANSWERED: &str = "language = \"tr\"\ntheme = \"nordic\"\nicons = \"unicode\"\n";
fn shared_folder(name: &str, shared: &str) -> PathBuf {
let folder = folder(name);
fs::create_dir_all(&folder).expect("the folder");
fs::write(folder.join("quvyta.conf"), shared).expect("quvyta.conf");
folder
}
#[test]
fn an_answered_shared_file_and_no_steps_of_its_own_need_no_wizard() {
let folder = shared_folder("appearance-only", ANSWERED);
let setup = Setup::new_in(&folder, Ecosystem::QUVYTA, APP, &I18n::builtin(), Msg::Setup).appearance_only();
assert!(!setup.needed(), "nothing is left to ask");
let own = file(&folder, "code.conf");
for key in ["language", "theme", "icons"] {
assert!(own.contains(&format!("{key} = \"quvyta\"")), "{key} follows the ecosystem: {own}");
}
assert_eq!(file(&folder, "quvyta.conf"), ANSWERED, "the shared file is as it was");
let again = Setup::new_in(&folder, Ecosystem::QUVYTA, APP, &I18n::builtin(), Msg::Setup);
assert!(!again.needed(), "and the question is not asked next start");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn an_answered_shared_file_opens_the_wizard_on_the_application_s_own_step() {
let folder = shared_folder("own-step", ANSWERED);
let mut h = Harness::new(Demo::new(&folder), 60, 30);
assert!(h.app().setup.needed());
assert!(!h.app().setup.asks_appearance());
assert_eq!(h.app().setup.step(), 1);
let screen = h.screen();
assert!(screen.contains("podman"), "the application's step is shown: {screen}");
assert!(!screen.contains("In every Quvyta application"), "the appearance step is not: {screen}");
assert!(!screen.contains("Back"), "there is nothing before it: {screen}");
h.click_text("Containers");
assert_eq!(h.app().setup.step(), 1, "nothing reaches the step left out");
h.click_text("Finish");
h.advance(Duration::from_millis(0));
assert!(h.app().done);
let own = file(&folder, "code.conf");
for key in ["language", "theme", "icons"] {
assert!(own.contains(&format!("{key} = \"quvyta\"")), "{key} follows the ecosystem: {own}");
}
assert_eq!(file(&folder, "quvyta.conf"), ANSWERED);
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn without_a_whole_shared_file_the_appearance_step_comes_first() {
for (name, shared) in [("no-shared", None), ("half-shared", Some("language = \"tr\"\ntheme = \"nordic\"\n"))] {
let folder = folder(name);
fs::create_dir_all(&folder).expect("the folder");
if let Some(shared) = shared {
fs::write(folder.join("quvyta.conf"), shared).expect("quvyta.conf");
}
let h = Harness::new(Demo::new(&folder), 60, 30);
assert!(h.app().setup.asks_appearance(), "{name}");
assert_eq!(h.app().setup.step(), 0, "{name}");
assert!(h.screen().contains("In every Quvyta application"), "{name}: {}", h.screen());
let alone = Setup::new_in(&folder, Ecosystem::QUVYTA, APP, &I18n::builtin(), Msg::Setup).appearance_only();
assert!(alone.needed(), "{name}: the appearance step is still asked");
assert!(!folder.join("code.conf").exists(), "{name}: and nothing is written before it is");
let _ = fs::remove_dir_all(&folder);
}
}
#[test]
fn a_file_holding_only_the_settle_mark_is_no_settings_yet() {
let folder = shared_folder("marked", ANSWERED);
fs::write(folder.join("code.conf"), "shared-checked = true\n").expect("code.conf");
assert!(Demo::new(&folder).setup.needed(), "the mark alone is not a setup");
fs::write(folder.join("code.conf"), "shared-checked = true\nengine = 1\n").expect("code.conf");
assert!(!Demo::new(&folder).setup.needed(), "a key of its own is");
fs::write(folder.join("code.conf"), "").expect("code.conf");
assert!(!Demo::new(&folder).setup.needed(), "an empty file the application wrote is one too");
let _ = fs::remove_dir_all(&folder);
}
#[test]
fn a_finish_that_cannot_be_saved_on_the_application_s_step_says_so_there() {
let folder = shared_folder("own-step-failure", ANSWERED);
fs::create_dir_all(folder.join("code.conf").join("in-the-way")).expect("the folder in the way");
let mut h = Harness::new(Demo::new(&folder), 60, 30);
assert_eq!(h.app().setup.step(), 1);
h.click_text("Finish");
h.advance(Duration::from_millis(0));
assert!(!h.app().done);
assert!(h.screen().contains("could not be saved"), "{}", h.screen());
let _ = fs::remove_dir_all(&folder);
}