Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,4 @@ once_cell = "1.19"
[dev-dependencies]
insta = { version = "1", features = ["glob"] }

[features]
test-utils = []

2 changes: 1 addition & 1 deletion tui/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl From<EntryEvent> for Action {
}

impl App {
pub(super) async fn handle_action(&mut self, action: Action, input: Input) -> bool {
pub async fn handle_action(&mut self, action: Action, input: Input) -> bool {
match action {
Action::Tui(TuiAction::Quit) => {
return true;
Expand Down
63 changes: 2 additions & 61 deletions tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,66 +107,7 @@ impl App {
views::dialog::draw(frame, state, context);
}

// Test helper: render one frame on any ratatui Terminal
#[cfg(feature = "test-utils")]
pub fn draw_once_on<B: ratatui::backend::Backend>(
&mut self,
terminal: &mut ratatui::Terminal<B>,
) -> color_eyre::Result<()> {
terminal.draw(|f| self.draw(f))?;
Ok(())
}

// Test helper: inject a single input event and return whether it requested quit
#[cfg(feature = "test-utils")]
pub async fn handle_input(&mut self, input: ratatui::crossterm::event::Event) -> bool {
use ratatui::crossterm::event::{
Event as Input, KeyCode, KeyEvent as CKeyEvent, KeyEventKind, KeyModifiers,
};

if !matches!(
input,
Input::Key(CKeyEvent {
kind: KeyEventKind::Press,
..
})
) {
return false;
}

match input {
Input::Key(CKeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
}) => {
self.save_immediate().await;
true
}
_ => {
let action = self.context.consume(&input).await;
self.handle_action(action, input).await
}
}
}

// Test helper: drain queued transitions (e.g., from background tasks)
#[cfg(feature = "test-utils")]
pub async fn drain_pending_transitions(&mut self) {
let mut transitions = Vec::new();
{
let mut queue = self.glues.transition_queue.lock().log_unwrap();
while let Some(t) = queue.pop_front() {
transitions.push(t);
}
}
for t in transitions {
self.handle_transition(t).await;
}
}

#[cfg(feature = "test-utils")]
async fn save_immediate(&mut self) {
self.save().await;
pub fn context_mut(&mut self) -> &mut Context {
&mut self.context
}
}
6 changes: 0 additions & 6 deletions tui/src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ use {
pub async fn init() {
let mut glue = get_glue();

table("logs")
.drop_table_if_exists()
.execute(&mut glue)
.await
.unwrap();

table("logs")
.create_table_if_not_exists()
.add_column("timestamp TIMESTAMP DEFAULT NOW()")
Expand Down
133 changes: 21 additions & 112 deletions tui/tests/entry_inproc.rs
Original file line number Diff line number Diff line change
@@ -1,144 +1,53 @@
#![cfg(feature = "test-utils")]
mod tester;
use tester::Tester;

use color_eyre::Result;
use ratatui::{
Terminal,
backend::TestBackend,
crossterm::event::{Event as Input, KeyCode, KeyEvent, KeyModifiers},
};

use glues::{App, config, logger};

fn buffer_to_lines(term: &Terminal<TestBackend>) -> Vec<String> {
let buf = term.backend().buffer().clone();
let area = buf.area();
let mut lines = Vec::with_capacity(area.height as usize);
for y in 0..area.height {
let mut line = String::new();
for x in 0..area.width {
line.push_str(buf[(x, y)].symbol());
}
lines.push(line);
}
lines
}

async fn setup() -> Result<(App, Terminal<TestBackend>)> {
// ensure logger/config have a writable HOME directory
let cwd = std::env::current_dir()?;
unsafe {
std::env::set_var("HOME", &cwd);
}
config::init().await;
logger::init().await;

let backend = TestBackend::new(120, 40);
let term = Terminal::new(backend)?;
let app = App::new();

Ok((app, term))
}
use ratatui::crossterm::event::KeyCode;

#[tokio::test]
async fn entry_nav_enter_opens_instant_inproc() -> Result<()> {
let (mut app, mut term) = setup().await?;
let mut t = Tester::new().await?;

// initial draw (home)
app.draw_once_on(&mut term)?;
t.draw()?;

// move down then up, then Enter to open Instant via selection
app.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('j'),
KeyModifiers::NONE,
)))
.await;
app.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('k'),
KeyModifiers::NONE,
)))
.await;
app.handle_input(Input::Key(KeyEvent::new(
KeyCode::Enter,
KeyModifiers::NONE,
)))
.await;
t.press('j').await;
t.press('k').await;
t.key(KeyCode::Enter).await;

// draw and assert notebook content is visible (e.g., sample note)
app.draw_once_on(&mut term)?;
let buf = buffer_to_lines(&term).join("\n");
assert!(buf.contains("Sample Note"));
t.draw()?;
t.assert_contains("Sample Note");

Ok(())
}

#[tokio::test]
async fn entry_quit_with_q_inproc() -> Result<()> {
let (mut app, mut term) = setup().await?;
app.draw_once_on(&mut term)?;
let mut t = Tester::new().await?;
t.draw()?;

let quit = app
.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('q'),
KeyModifiers::NONE,
)))
.await;
let quit = t.press('q').await;
assert!(quit);

Ok(())
}

#[tokio::test]
async fn entry_keymap_toggle_inproc() -> Result<()> {
let (mut app, mut term) = setup().await?;
app.draw_once_on(&mut term)?;

// show keymap
app.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('?'),
KeyModifiers::NONE,
)))
.await;
app.draw_once_on(&mut term)?;
let buf = buffer_to_lines(&term).join("\n");
assert!(buf.contains(" [?] Hide keymap "));

// hide keymap
app.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('?'),
KeyModifiers::NONE,
)))
.await;
app.draw_once_on(&mut term)?;
let buf = buffer_to_lines(&term).join("\n");
assert!(!buf.contains(" [?] Hide keymap "));

Ok(())
}

#[tokio::test]
async fn entry_help_overlay_open_close_inproc() -> Result<()> {
let (mut app, mut term) = setup().await?;
app.draw_once_on(&mut term)?;
let mut t = Tester::new().await?;
t.draw()?;

// open help (currently bound to 'a')
app.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::NONE,
)))
.await;
app.draw_once_on(&mut term)?;
let buf = buffer_to_lines(&term).join("\n");
assert!(buf.contains("Press any key to close"));
t.press('a').await;
t.draw()?;
t.assert_contains("Press any key to close");

// any key closes help
app.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('x'),
KeyModifiers::NONE,
)))
.await;
app.draw_once_on(&mut term)?;
let buf = buffer_to_lines(&term).join("\n");
assert!(!buf.contains("Press any key to close"));
t.press('x').await;
t.draw()?;
t.assert_not_contains("Press any key to close");

Ok(())
}
57 changes: 8 additions & 49 deletions tui/tests/flow_inproc.rs
Original file line number Diff line number Diff line change
@@ -1,63 +1,22 @@
#![cfg(feature = "test-utils")]
mod tester;
use tester::Tester;
Comment on lines +1 to +2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Tester harness: avoid unsafe HOME mutation; prefer temp HOME per test.

In tester.rs, std::env::set_var isn’t unsafe—drop the unsafe block and consider a temp dir to isolate runs.

Example (tester.rs):

let tmp = tempfile::tempdir()?;
std::env::set_var("HOME", tmp.path()); // no unsafe
// store tmp in Tester to keep it alive for test duration
🤖 Prompt for AI Agents
In tui/tests/flow_inproc.rs around lines 1 to 2, the tester harness uses an
unsafe block to mutate HOME in tester.rs; remove the unnecessary unsafe wrapper
around std::env::set_var and instead create a temporary directory
(tempfile::tempdir()) and set HOME to that path, storing the TempDir inside the
Tester struct so it stays alive for the test duration; update tester.rs to
import tempfile, add a field like tmp_home: tempfile::TempDir to Tester, assign
it when constructing the Tester, call std::env::set_var("HOME",
tmp_home.path()), and drop the unsafe block.


use color_eyre::Result;
use insta::assert_debug_snapshot;
use ratatui::{
Terminal,
backend::TestBackend,
crossterm::event::{Event as Input, KeyCode, KeyEvent, KeyModifiers},
};

use glues::{App, config, logger};

fn buffer_to_lines(term: &Terminal<TestBackend>) -> Vec<String> {
let buf = term.backend().buffer().clone();
let area = buf.area();
let mut lines = Vec::with_capacity(area.height as usize);
for y in 0..area.height {
let mut line = String::new();
for x in 0..area.width {
line.push_str(buf[(x, y)].symbol());
}
lines.push(line);
}
lines
}

#[tokio::test]
async fn home_to_instant_quit_inproc() -> Result<()> {
// ensure logger/config have a writable HOME directory
let cwd = std::env::current_dir()?;
unsafe {
std::env::set_var("HOME", &cwd);
}
config::init().await;
logger::init().await;

let backend = TestBackend::new(120, 40);
let mut term = Terminal::new(backend)?;
let mut app = App::new();
let mut t = Tester::new().await?;

// initial home screen
app.draw_once_on(&mut term)?;
assert_debug_snapshot!("home_inproc", buffer_to_lines(&term));
t.draw()?;
t.assert_snapshot("home_inproc");

// open Instant (in-memory) notebook
app.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('1'),
KeyModifiers::NONE,
)))
.await;
app.draw_once_on(&mut term)?;
assert_debug_snapshot!("instant_inproc", buffer_to_lines(&term));
t.open_instant().await?;
t.assert_snapshot("instant_inproc");

// quit with Ctrl+C
let quit = app
.handle_input(Input::Key(KeyEvent::new(
KeyCode::Char('c'),
KeyModifiers::CONTROL,
)))
.await;
let quit = t.ctrl('c').await;
assert!(quit);

Ok(())
Expand Down
Loading