Skip to content

Commit cf4652e

Browse files
test(prompt): pin prompt color output across nu-ansi-term switch
Covers the SGR bytes emitted for prompt, indicator and right prompt on both buffer paths: the trait defaults, Color::Default emitting an explicit SGR 39 (#1046), the use_ansi_coloring guards, and that crossterm's Green/Cyan were the bright palette entries.
1 parent e6c8935 commit cf4652e

1 file changed

Lines changed: 193 additions & 1 deletion

File tree

src/painting/painter.rs

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ impl Painter {
12201220
mod tests {
12211221
use super::*;
12221222
use crate::menu::MenuEvent;
1223-
use crate::{Completer, Editor, PromptHistorySearch, Suggestion};
1223+
use crate::{Color, Completer, Editor, PromptHistorySearch, Suggestion};
12241224
use pretty_assertions::assert_eq;
12251225
use std::borrow::Cow;
12261226
use std::sync::{Arc, Mutex};
@@ -1741,4 +1741,196 @@ mod tests {
17411741
]
17421742
);
17431743
}
1744+
1745+
const SGR_GREEN: &str = "\x1b[92m"; // DEFAULT_PROMPT_COLOR (LightGreen, palette 10)
1746+
const SGR_CYAN: &str = "\x1b[96m"; // DEFAULT_INDICATOR_COLOR (LightCyan, palette 14)
1747+
const SGR_PURPLE: &str = "\x1b[35m"; // DEFAULT_PROMPT_RIGHT_COLOR (Purple, palette 5)
1748+
const SGR_DEFAULT_FG: &str = "\x1b[39m"; // Color::Default — "terminal foreground"
1749+
1750+
/// A prompt whose color methods are set per-test. Rendering is inherited from
1751+
/// `TestPrompt` since these tests only care about the emitted escapes.
1752+
struct ColoredPrompt {
1753+
prompt: Color,
1754+
indicator: Color,
1755+
right: Color,
1756+
}
1757+
1758+
impl Prompt for ColoredPrompt {
1759+
fn render_prompt_left(&self) -> Cow<'_, str> {
1760+
TestPrompt.render_prompt_left()
1761+
}
1762+
fn render_prompt_right(&self) -> Cow<'_, str> {
1763+
TestPrompt.render_prompt_right()
1764+
}
1765+
fn render_prompt_indicator(&self, mode: PromptEditMode) -> Cow<'_, str> {
1766+
TestPrompt.render_prompt_indicator(mode)
1767+
}
1768+
fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
1769+
TestPrompt.render_prompt_multiline_indicator()
1770+
}
1771+
fn render_prompt_history_search_indicator(
1772+
&self,
1773+
search: PromptHistorySearch,
1774+
) -> Cow<'_, str> {
1775+
TestPrompt.render_prompt_history_search_indicator(search)
1776+
}
1777+
1778+
fn get_prompt_color(&self) -> Color {
1779+
self.prompt
1780+
}
1781+
fn get_indicator_color(&self) -> Color {
1782+
self.indicator
1783+
}
1784+
fn get_prompt_right_color(&self) -> Color {
1785+
self.right
1786+
}
1787+
}
1788+
/// Like `capture_repaint`, but with configurable ANSI coloring. Also returns
1789+
/// whether the paint took the large-buffer branch, since `repaint_buffer`
1790+
/// recomputes that field and a caller cannot force it.
1791+
fn capture_repaint_ansi(
1792+
prompt: &dyn Prompt,
1793+
lines: &PromptLines,
1794+
use_ansi_coloring: bool,
1795+
) -> (String, bool) {
1796+
let mut p = Painter::new(W::capture());
1797+
p.terminal_size = (20, 10);
1798+
p.prompt_start_row.mark_verified(0);
1799+
p.prompt_height = 1;
1800+
p.repaint_buffer(
1801+
prompt,
1802+
lines,
1803+
PromptEditMode::Default,
1804+
None,
1805+
use_ansi_coloring,
1806+
&None,
1807+
)
1808+
.expect("repaint_buffer failed");
1809+
let large = p.large_buffer;
1810+
(
1811+
String::from_utf8_lossy(p.stdout.captured()).into_owned(),
1812+
large,
1813+
)
1814+
}
1815+
1816+
/// Records which palette entries crossterm's `SetForegroundColor` selected
1817+
/// for the pre-migration defaults. crossterm's `Green`/`Cyan` are the
1818+
/// *bright* entries (10/14) — `DarkGreen`/`DarkCyan` are 2/6 — so the
1819+
/// nu-ansi-term replacements have to be the `Light*` variants to keep the
1820+
/// prompt looking the same.
1821+
///
1822+
/// Green and cyan intentionally re-encode from the 256-color form
1823+
/// (`38;5;10`) to the aixterm form (`92`); both select palette entry 10.
1824+
#[test]
1825+
fn crossterm_defaults_were_the_bright_palette_entries() {
1826+
use crossterm::{
1827+
style::{Color as CtColor, SetForegroundColor},
1828+
Command,
1829+
};
1830+
1831+
fn crossterm_sgr(color: CtColor) -> String {
1832+
let mut buf = String::new();
1833+
SetForegroundColor(color)
1834+
.write_ansi(&mut buf)
1835+
.expect("write_ansi failed");
1836+
buf
1837+
}
1838+
1839+
for (name, crossterm, palette) in [
1840+
("prompt", CtColor::Green, 10),
1841+
("indicator", CtColor::Cyan, 14),
1842+
("right prompt", CtColor::AnsiValue(5), 5),
1843+
] {
1844+
assert_eq!(
1845+
crossterm_sgr(crossterm),
1846+
Color::Fixed(palette).prefix().to_string(),
1847+
"{name} default selected a different palette entry than assumed"
1848+
);
1849+
}
1850+
1851+
// "Unstyled" has no palette entry; both spellings are SGR 39.
1852+
assert_eq!(
1853+
Color::Default.prefix().to_string(),
1854+
crossterm_sgr(CtColor::Reset)
1855+
);
1856+
}
1857+
1858+
/// The trait's default colors reach the terminal as the expected SGR
1859+
/// sequences, on the small-buffer path.
1860+
#[test]
1861+
fn default_prompt_colors_emit_expected_sgr() {
1862+
let (out, _) =
1863+
capture_repaint_ansi(&TestPrompt, &make_lines("> ", "", "RP", "hi", ""), true);
1864+
1865+
assert!(
1866+
out.contains(SGR_GREEN),
1867+
"left prompt color missing: {out:?}"
1868+
);
1869+
assert!(out.contains(SGR_CYAN), "indicator color missing: {out:?}");
1870+
assert!(
1871+
out.contains(SGR_PURPLE),
1872+
"right prompt color missing: {out:?}"
1873+
);
1874+
}
1875+
1876+
/// `Color::Default` must emit an explicit SGR 39, never an empty prefix —
1877+
/// an empty one would let the active color bleed into an unstyled prompt,
1878+
/// which is the starship bug (#1046).
1879+
#[test]
1880+
fn default_color_emits_explicit_foreground_reset() {
1881+
let prompt = ColoredPrompt {
1882+
prompt: Color::Default,
1883+
indicator: Color::Default,
1884+
right: Color::Default,
1885+
};
1886+
let (out, _) = capture_repaint_ansi(&prompt, &make_lines("> ", "", "RP", "hi", ""), true);
1887+
1888+
assert!(
1889+
out.contains(SGR_DEFAULT_FG),
1890+
"Color::Default must emit an explicit SGR 39, not nothing: {out:?}"
1891+
);
1892+
assert!(
1893+
!out.contains(SGR_GREEN),
1894+
"no default color should leak through: {out:?}"
1895+
);
1896+
}
1897+
1898+
/// `print_large_buffer` has its own three color call sites that the other
1899+
/// capture tests never reach.
1900+
///
1901+
/// The bulk sits in `after_cursor` so `extra_rows` stays 0 and the right
1902+
/// prompt is still drawn; a tall `before_cursor` would suppress it (see
1903+
/// `test_layout_right_prompt_suppressed_in_large_buffer`).
1904+
#[test]
1905+
fn large_buffer_path_emits_prompt_colors() {
1906+
let tall = "line\n".repeat(15);
1907+
let (out, large) =
1908+
capture_repaint_ansi(&TestPrompt, &make_lines("> ", "", "RP", "hi", &tall), true);
1909+
1910+
assert!(large, "expected the large-buffer path to be taken");
1911+
assert!(
1912+
out.contains(SGR_GREEN),
1913+
"left prompt color missing: {out:?}"
1914+
);
1915+
assert!(out.contains(SGR_CYAN), "indicator color missing: {out:?}");
1916+
assert!(
1917+
out.contains(SGR_PURPLE),
1918+
"right prompt color missing: {out:?}"
1919+
);
1920+
}
1921+
1922+
/// Every color write stays inside its `use_ansi_coloring` guard. Checks the
1923+
/// color sequences only — `repaint_buffer` always emits a leading `\x1b[0m`.
1924+
#[test]
1925+
fn no_prompt_colors_when_ansi_coloring_disabled() {
1926+
let (out, _) =
1927+
capture_repaint_ansi(&TestPrompt, &make_lines("> ", "", "RP", "hi", ""), false);
1928+
1929+
for sgr in [SGR_GREEN, SGR_CYAN, SGR_PURPLE] {
1930+
assert!(
1931+
!out.contains(sgr),
1932+
"emitted {sgr:?} with coloring disabled: {out:?}"
1933+
);
1934+
}
1935+
}
17441936
}

0 commit comments

Comments
 (0)