Skip to main content

studio_worker/ui/
tab.rs

1//! The five tabs the UI exposes.  Pure data + tiny enum impl so the
2//! contract is testable without egui in scope.
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
5pub enum Tab {
6    #[default]
7    Status,
8    Jobs,
9    Config,
10    Logs,
11    About,
12}
13
14impl Tab {
15    pub const ALL: [Tab; 5] = [Tab::Status, Tab::Jobs, Tab::Config, Tab::Logs, Tab::About];
16
17    pub fn label(self) -> &'static str {
18        match self {
19            Tab::Status => "Status",
20            Tab::Jobs => "Jobs",
21            Tab::Config => "Config",
22            Tab::Logs => "Logs",
23            Tab::About => "About",
24        }
25    }
26
27    /// Parse a tab name (case-insensitive).  Used by the
28    /// `STUDIO_WORKER_UI_TAB` debug env var to seed the initial tab
29    /// during screenshot capture and headless UI inspection.
30    pub fn parse(name: &str) -> Option<Self> {
31        match name.trim().to_ascii_lowercase().as_str() {
32            "status" => Some(Self::Status),
33            "jobs" => Some(Self::Jobs),
34            "config" => Some(Self::Config),
35            "logs" => Some(Self::Logs),
36            "about" => Some(Self::About),
37            _ => None,
38        }
39    }
40
41    /// Resolve the initial tab on app launch: env override or default.
42    pub fn initial() -> Self {
43        std::env::var("STUDIO_WORKER_UI_TAB")
44            .ok()
45            .and_then(|s| Self::parse(&s))
46            .unwrap_or_default()
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn all_returns_five_tabs_in_render_order() {
56        let labels: Vec<&str> = Tab::ALL.iter().map(|t| t.label()).collect();
57        assert_eq!(
58            labels,
59            ["Status", "Jobs", "Config", "Logs", "About"],
60            "tab labels + order are part of the UI contract"
61        );
62    }
63
64    #[test]
65    fn default_is_status() {
66        assert_eq!(Tab::default(), Tab::Status);
67    }
68
69    #[test]
70    fn parse_round_trips_with_label_case_insensitively() {
71        for tab in Tab::ALL {
72            assert_eq!(Tab::parse(tab.label()), Some(tab));
73            assert_eq!(Tab::parse(&tab.label().to_uppercase()), Some(tab));
74        }
75        assert!(Tab::parse("").is_none());
76        assert!(Tab::parse("nope").is_none());
77    }
78
79    #[test]
80    fn initial_falls_back_to_default_when_env_unset() {
81        // SAFETY: the Rust test harness runs tests concurrently, so this
82        // mutates the process-global STUDIO_WORKER_UI_TAB. Safe because
83        // this is the only test that touches that var; we snapshot it and
84        // restore it afterwards. (A panic here would skip the restore, but
85        // it also fails the run and no other test reads the var.)
86        let prev = std::env::var("STUDIO_WORKER_UI_TAB").ok();
87        std::env::remove_var("STUDIO_WORKER_UI_TAB");
88        assert_eq!(Tab::initial(), Tab::default());
89        if let Some(v) = prev {
90            std::env::set_var("STUDIO_WORKER_UI_TAB", v);
91        }
92    }
93
94    #[test]
95    fn labels_are_unique() {
96        use std::collections::HashSet;
97        let unique: HashSet<&str> = Tab::ALL.iter().map(|t| t.label()).collect();
98        assert_eq!(unique.len(), Tab::ALL.len());
99    }
100}