Some checks failed
ci / test (pull_request) Failing after 54s
Reject non-32-byte spk, non-v3 onion, and invites over 4KiB before hex alloc. Sign onionwire-invite-v1 with 0x00 separators; verify that first. Well-formed v1 concat still verifies after field checks.
198 lines
6.2 KiB
Rust
198 lines
6.2 KiB
Rust
//! Main-screen chrome helpers: ASCII wordmark, compact fallback, help text.
|
|
|
|
use onionwire::qr;
|
|
use onionwire::tui::{
|
|
banner_for_width, compact_banner, draw_share, help_overlay_text, main_footer_hints,
|
|
onion_glyph, status_footer, wordmark_banner, Pane,
|
|
};
|
|
|
|
#[test]
|
|
fn wordmark_banner_is_nonempty_and_fits_80() {
|
|
let banner = wordmark_banner();
|
|
assert!(!banner.is_empty());
|
|
for line in banner.lines() {
|
|
assert!(
|
|
line.chars().count() <= 80,
|
|
"wordmark line exceeds 80 chars: {line:?} ({})",
|
|
line.chars().count()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn compact_banner_is_nonempty_and_fits_40() {
|
|
let compact = compact_banner();
|
|
assert!(!compact.is_empty());
|
|
assert!(
|
|
compact.chars().count() <= 40,
|
|
"compact banner exceeds 40 chars ({})",
|
|
compact.chars().count()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn onion_glyph_is_nonempty() {
|
|
assert!(!onion_glyph().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn help_overlay_lists_core_bindings() {
|
|
let help = help_overlay_text();
|
|
assert!(!help.is_empty());
|
|
for needle in [
|
|
"Tab", "F2", "F3", "F4", "F5", "/profile", "/pay", "/tip", "/backup", "/restore", "Ctrl-Q",
|
|
"?",
|
|
] {
|
|
assert!(help.contains(needle), "help overlay missing {needle:?}");
|
|
}
|
|
for line in help.lines() {
|
|
assert!(
|
|
line.chars().count() <= 80,
|
|
"help line exceeds 80 chars: {line:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn banner_for_width_collapses_when_narrow() {
|
|
let full = wordmark_banner();
|
|
let max_line = full.lines().map(|l| l.chars().count()).max().unwrap();
|
|
assert_eq!(banner_for_width(max_line as u16), full);
|
|
assert_eq!(banner_for_width(max_line as u16 - 1), compact_banner());
|
|
assert_eq!(banner_for_width(80), full);
|
|
assert_eq!(banner_for_width(120), full);
|
|
}
|
|
|
|
#[test]
|
|
fn chrome_renders_at_80x24_and_120x40() {
|
|
use ratatui::backend::TestBackend;
|
|
use ratatui::layout::{Constraint, Layout};
|
|
use ratatui::widgets::Paragraph;
|
|
use ratatui::Terminal;
|
|
|
|
let fp = "abcdef0123456789";
|
|
let onion = "abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion";
|
|
for (w, h) in [(80u16, 24u16), (120, 40)] {
|
|
let backend = TestBackend::new(w, h);
|
|
let mut terminal = Terminal::new(backend).expect("terminal");
|
|
terminal
|
|
.draw(|f| {
|
|
let banner = banner_for_width(f.area().width);
|
|
let banner_h = banner.lines().count() as u16;
|
|
let chunks = Layout::vertical([
|
|
Constraint::Length(banner_h),
|
|
Constraint::Min(3),
|
|
Constraint::Length(1),
|
|
])
|
|
.split(f.area());
|
|
f.render_widget(Paragraph::new(banner), chunks[0]);
|
|
let footer = status_footer(f.area().width, fp, onion, None, main_footer_hints());
|
|
f.render_widget(Paragraph::new(footer), chunks[2]);
|
|
})
|
|
.expect("draw");
|
|
let buf = terminal.backend().buffer();
|
|
let row0: String = (0..w).map(|x| buf[(x, 0)].symbol().to_string()).collect();
|
|
assert!(
|
|
row0.contains("ONIONWIRE"),
|
|
"banner missing on row 0 at {w}x{h}: {row0:?}"
|
|
);
|
|
let footer: String = (0..w)
|
|
.map(|x| buf[(x, h - 1)].symbol().to_string())
|
|
.collect();
|
|
assert!(
|
|
footer.contains("? help"),
|
|
"footer missing ? help at {w}x{h}: {footer:?}"
|
|
);
|
|
assert!(
|
|
footer.contains("TOR"),
|
|
"footer missing TOR at {w}x{h}: {footer:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn main_footer_keeps_help_at_80_and_120() {
|
|
let fp = "abcdef0123456789deadbeef";
|
|
let onion = "abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion";
|
|
let hints = main_footer_hints();
|
|
assert!(hints.contains("? help"));
|
|
for w in [80u16, 120] {
|
|
let line = status_footer(w, fp, onion, None, hints);
|
|
assert!(
|
|
line.chars().count() <= w as usize,
|
|
"footer wider than {w}: {} {line:?}",
|
|
line.chars().count()
|
|
);
|
|
assert!(
|
|
line.contains("? help"),
|
|
"footer missing ? help at {w}: {line:?}"
|
|
);
|
|
assert!(line.contains("TOR"), "footer missing TOR at {w}: {line:?}");
|
|
assert!(
|
|
line.contains("me:"),
|
|
"footer missing identity at {w}: {line:?}"
|
|
);
|
|
}
|
|
let crowded = status_footer(80, fp, onion, Some("rotated · notified 1/2 friends"), hints);
|
|
assert!(
|
|
crowded.chars().count() <= 80,
|
|
"crowded footer wider than 80: {} {crowded:?}",
|
|
crowded.chars().count()
|
|
);
|
|
assert!(
|
|
crowded.contains("? help"),
|
|
"status note crowded out ? help: {crowded:?}"
|
|
);
|
|
assert!(
|
|
crowded.contains("TOR"),
|
|
"crowded footer missing TOR: {crowded:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pane_focus_cycles_roster_chat_composer() {
|
|
assert_eq!(Pane::Roster.next(), Pane::Chat);
|
|
assert_eq!(Pane::Chat.next(), Pane::Composer);
|
|
assert_eq!(Pane::Composer.next(), Pane::Roster);
|
|
assert_eq!(Pane::Roster.prev(), Pane::Composer);
|
|
assert_eq!(Pane::Chat.prev(), Pane::Roster);
|
|
assert_eq!(Pane::Composer.prev(), Pane::Chat);
|
|
}
|
|
|
|
#[test]
|
|
fn restore_terminal_is_callable_without_panic() {
|
|
onionwire::tui::restore_terminal();
|
|
}
|
|
|
|
#[test]
|
|
fn share_screen_shows_invite_not_qr_at_80x24() {
|
|
use ratatui::backend::TestBackend;
|
|
use ratatui::Terminal;
|
|
|
|
let sk = [7u8; 32];
|
|
let onion = "abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion";
|
|
let payload = qr::encode(&sk, onion, &[1u8; 32]).expect("encode");
|
|
assert!(payload.starts_with("onionwire:v1:"));
|
|
|
|
let backend = TestBackend::new(80, 24);
|
|
let mut terminal = Terminal::new(backend).expect("terminal");
|
|
terminal.draw(|f| draw_share(f, &payload)).expect("draw");
|
|
let buf = terminal.backend().buffer();
|
|
let mut screen = String::new();
|
|
for y in 0..24u16 {
|
|
for x in 0..80u16 {
|
|
screen.push_str(buf[(x, y)].symbol());
|
|
}
|
|
screen.push('\n');
|
|
}
|
|
assert!(
|
|
screen.contains("onionwire:v1:"),
|
|
"share screen missing invite prefix:\n{screen}"
|
|
);
|
|
for ch in ['█', '▀', '▄'] {
|
|
assert!(
|
|
!screen.contains(ch),
|
|
"share screen still has QR block {ch:?}:\n{screen}"
|
|
);
|
|
}
|
|
}
|