| 1 | //! Shared golden-buffer harness for the Tideline components (spec §5c). |
| 2 | //! |
| 3 | //! Every Tideline component proves itself against cell-exact golden buffers |
| 4 | //! at the four canonical blocker sizes. This module owns the one dump format |
| 5 | //! and the one bless protocol so they cannot drift between components: |
| 6 | //! |
| 7 | //! - rows are the `Buffer` cell symbols in paint order, `width` per row; |
| 8 | //! - rows are joined by `\n` with one trailing newline; |
| 9 | //! - goldens live in `crates/tui/src/tui/goldens/{name}_{w}x{h}.txt`; |
| 10 | //! - a missing golden fails the test unless `CODEWHALE_BLESS_GOLDENS=1` is |
| 11 | //! set, in which case the rendered text is written as the new contract. |
| 12 | //! |
| 13 | //! Goldens are the design contract — a visual change that cannot show as a |
| 14 | //! golden diff did not happen. |
| 15 | |
| 16 | /// The four terminal sizes the v0.8.66 modal blocker (#3732) requires every |
| 17 | /// surface to remain readable and fully operable at. Mirrors |
| 18 | /// `views/status_picker.rs::BLOCKER_SIZES` (kept private there, so the |
| 19 | /// canonical copy is restated here for the Tideline golden suites). |
| 20 | pub(crate) const BLOCKER_SIZES: [(u16, u16); 4] = [(80, 24), (100, 30), (120, 32), (160, 40)]; |
| 21 | |
| 22 | /// Render one component into a fresh buffer and dump the cell symbols as |
| 23 | /// golden text. Deterministic by contract: the caller injects every fact |
| 24 | /// (clock strings, counters, hover state), never `Instant::now`. |
| 25 | pub(crate) fn render_golden_text( |
| 26 | width: u16, |
| 27 | height: u16, |
| 28 | draw: impl FnOnce(&mut ratatui::buffer::Buffer), |
| 29 | ) -> String { |
| 30 | let mut buf = ratatui::buffer::Buffer::empty(ratatui::layout::Rect::new( |
| 31 | 0, |
| 32 | 0, |
| 33 | width.max(1), |
| 34 | height.max(1), |
| 35 | )); |
| 36 | draw(&mut buf); |
| 37 | let w = width.max(1) as usize; |
| 38 | let content = buf.content(); |
| 39 | let rows: Vec<String> = (0..height.max(1) as usize) |
| 40 | .map(|y| { |
| 41 | content[y * w..(y + 1) * w] |
| 42 | .iter() |
| 43 | .map(|cell| cell.symbol().to_string()) |
| 44 | .collect() |
| 45 | }) |
| 46 | .collect(); |
| 47 | format!("{}\n", rows.join("\n")) |
| 48 | } |
| 49 | |
| 50 | /// Assert `rendered` equals the golden `name`, blessing it when missing and |
| 51 | /// `CODEWHALE_BLESS_GOLDENS=1` is set (topbar protocol, spec §5c). |
| 52 | pub(crate) fn assert_matches_golden(name: &str, rendered: &str) { |
| 53 | let path = golden_path(name); |
| 54 | match std::fs::read_to_string(&path) { |
| 55 | // Compare against LF: a Windows checkout can hand us CRLF, and the |
| 56 | // dump side always joins rows with LF. Cell symbols never contain CR, |
| 57 | // so this can only ever cancel a line-ending difference. |
| 58 | Ok(expected) => assert_eq!( |
| 59 | rendered, |
| 60 | expected.replace("\r\n", "\n"), |
| 61 | "golden drift at {name}; re-bless only with an approved design change" |
| 62 | ), |
| 63 | Err(_) => { |
| 64 | if std::env::var("CODEWHALE_BLESS_GOLDENS").is_ok() { |
| 65 | if let Some(parent) = path.parent() { |
| 66 | std::fs::create_dir_all(parent).expect("create goldens dir"); |
| 67 | } |
| 68 | std::fs::write(&path, rendered).expect("write golden"); |
| 69 | } else { |
| 70 | panic!("missing golden {name}; run with CODEWHALE_BLESS_GOLDENS=1 to write it"); |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | pub(crate) fn golden_path(name: &str) -> std::path::PathBuf { |
| 77 | std::path::Path::new(env!("CARGO_MANIFEST_DIR")) |
| 78 | .join("src/tui/goldens") |
| 79 | .join(format!("{name}.txt")) |
| 80 | } |
| 81 | |
| 82 | // --------------------------------------------------------------------------- |
| 83 | // Ink plane — the colour half of the design contract. |
| 84 | // |
| 85 | // `render_golden_text` dumps cell *symbols*. That is only half a render: a |
| 86 | // screen can go from a gold mark over a blue gradient to uniform grey without |
| 87 | // moving a single glyph, and the symbol golden would not notice. The founder's |
| 88 | // standing complaint about the startup screen ("everything in the same dim |
| 89 | // gray") was, mechanically, invisible to this suite. |
| 90 | // |
| 91 | // The ink plane closes that hole. Each painted cell becomes one character |
| 92 | // keyed to its (fg, bg, modifier) triple, with a legend resolving those keys |
| 93 | // to concrete values, so a contrast change shows up as a golden diff. |
| 94 | // --------------------------------------------------------------------------- |
| 95 |