use super::{BackendStats, CountingBackend, buffer_row_text, buffer_text, key};
use crate::KeyCode;
use crate::{DiffReviewState, DiffReviewWidget, ReviewInput};
use clankerdiff_core::DiffDocument;
use clankerdiff_syntax::HighlightStats;
use ratatui::{Terminal, backend::TestBackend, buffer::Buffer};
use std::sync::Arc;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct FrameStats {
pub backend: BackendStats,
pub highlight_calls: u64,
pub highlight_hits: u64,
pub highlight_misses: u64,
pub highlighted_bytes: usize,
}
pub struct ReviewHarnessBuilder {
pub document: Arc<DiffDocument>,
pub width: u16,
pub height: u16,
}
impl Default for ReviewHarnessBuilder {
fn default() -> Self {
Self {
document: Arc::new(DiffDocument::empty()),
width: 80,
height: 24,
}
}
}
impl ReviewHarnessBuilder {
#[must_use]
pub fn dimensions(mut self, width: u16, height: u16) -> Self {
self.width = width;
self.height = height;
self
}
#[must_use]
pub fn build(self) -> ReviewHarness {
let state = DiffReviewState::new(self.document);
ReviewHarness {
terminal: Terminal::new(CountingBackend::new(self.width, self.height))
.expect("infallible test terminal"),
state,
}
}
}
pub struct ReviewHarness {
terminal: Terminal<CountingBackend>,
state: DiffReviewState,
}
impl ReviewHarness {
#[must_use]
pub fn new(document: Arc<DiffDocument>, width: u16, height: u16) -> Self {
ReviewHarnessBuilder {
document,
width,
height,
}
.build()
}
pub fn draw(&mut self) -> FrameStats {
let before = self.state.highlight_stats();
self.terminal
.draw(|frame| {
frame.render_stateful_widget(
DiffReviewWidget::new(),
frame.area(),
&mut self.state,
);
if let Some(position) = self.state.cursor_position() {
frame.set_cursor_position(position);
}
})
.expect("infallible test draw");
let after = self.state.highlight_stats();
FrameStats::new(self.terminal.backend_mut().take_stats(), before, after)
}
pub fn input(&mut self, input: ReviewInput) {
let _ = self.state.handle_input(input);
}
pub fn press(&mut self, code: KeyCode) {
self.input(key(code));
}
pub fn type_text(&mut self, text: &str) {
for character in text.chars() {
self.press(KeyCode::Char(character));
}
}
pub fn input_and_draw(&mut self, input: ReviewInput) -> FrameStats {
self.input(input);
self.draw()
}
#[must_use]
pub const fn state(&self) -> &DiffReviewState {
&self.state
}
pub const fn state_mut(&mut self) -> &mut DiffReviewState {
&mut self.state
}
#[must_use]
pub fn buffer(&self) -> &Buffer {
self.terminal.backend().buffer()
}
#[must_use]
pub fn text(&self) -> String {
buffer_text(self.buffer())
}
#[must_use]
pub fn row_text(&self, row: u16) -> String {
buffer_row_text(self.buffer(), row)
}
pub fn assert_contains(&self, expected: &str) {
let rendered = self.text();
assert!(
rendered.contains(expected),
"rendered output did not contain {expected:?}:\n{rendered}"
);
}
pub fn assert_row(&self, row: u16, expected: &str) {
let actual = self.row_text(row);
assert_eq!(
actual.trim_end(),
expected.trim_end(),
"rendered row {row} differed; full buffer:\n{}",
self.text()
);
}
}
impl FrameStats {
fn new(backend: BackendStats, before: HighlightStats, after: HighlightStats) -> Self {
Self {
backend,
highlight_calls: after.calls.saturating_sub(before.calls),
highlight_hits: after.hits.saturating_sub(before.hits),
highlight_misses: after.misses.saturating_sub(before.misses),
highlighted_bytes: after.bytes.saturating_sub(before.bytes),
}
}
}
pub fn render_review(state: &mut DiffReviewState, width: u16, height: u16) -> String {
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("test terminal");
terminal
.draw(|frame| {
frame.render_stateful_widget(DiffReviewWidget::new(), frame.area(), state);
if let Some(position) = state.cursor_position() {
frame.set_cursor_position(position);
}
})
.expect("draw review widget");
buffer_text(terminal.backend().buffer())
}
pub fn type_review_text(state: &mut DiffReviewState, text: &str) {
for character in text.chars() {
let _ = state.handle_input(key(KeyCode::Char(character)));
}
}