pub mod fixtures;
pub mod mysql;
pub mod postgres;
use std::sync::Arc;
use std::time::Instant;
use ratatui::Terminal;
use ratatui::backend::Backend;
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
use ratatui::layout::Position;
use sabiql_app::model::app_state::AppState;
use sabiql_app::model::connection::setup::ConnectionField;
use sabiql_app::model::shared::text_input::TextInputState;
use sabiql_app::services::AppServices;
use sabiql_domain::{ConnectionId, DatabaseType};
use sabiql_ui::shell::layout::MainLayout;
use sabiql_ui::theme::{ThemePalette, palette_for};
pub const TEST_WIDTH: u16 = 165;
pub const TEST_HEIGHT: u16 = 51;
pub fn test_instant() -> Instant {
Instant::now()
}
pub fn create_test_state() -> AppState {
let mut state = AppState::new("test_project".to_string());
state.session.activate_connection_with_dsn(
&ConnectionId::from_string("test-connection"),
"localhost:5432/test",
DatabaseType::PostgreSQL,
"localhost:5432/test",
);
state
}
pub fn focus_connection_field(state: &mut AppState, field: ConnectionField) {
let fields = state.connection_setup.visible_fields();
let target_idx = fields
.iter()
.position(|candidate| *candidate == field)
.unwrap_or_else(|| panic!("field {field:?} is not visible: {fields:?}"));
loop {
let current = state.connection_setup.focused_field();
if current == field {
return;
}
let current_idx = fields
.iter()
.position(|candidate| *candidate == current)
.expect("focused field must be visible");
if target_idx > current_idx {
state.connection_setup.focus_next_field();
} else {
state.connection_setup.focus_prev_field();
}
}
}
pub fn set_connection_input(state: &mut AppState, field: ConnectionField, input: TextInputState) {
*state
.connection_setup
.input_mut(field)
.expect("expected text input field") = input;
}
pub fn create_test_terminal() -> Terminal<TestBackend> {
let backend = TestBackend::new(TEST_WIDTH, TEST_HEIGHT);
Terminal::new(backend).unwrap()
}
pub fn create_test_terminal_sized(width: u16, height: u16) -> Terminal<TestBackend> {
let backend = TestBackend::new(width, height);
Terminal::new(backend).unwrap()
}
const FIXED_TIME_MS: u128 = 0;
pub fn render_and_get_buffer(terminal: &mut Terminal<TestBackend>, state: &mut AppState) -> Buffer {
render_and_get_buffer_at(terminal, state, test_instant())
}
pub fn render_and_get_buffer_at(
terminal: &mut Terminal<TestBackend>,
state: &mut AppState,
now: Instant,
) -> Buffer {
render_and_get_buffer_at_with_theme(terminal, state, now, palette_for(state.ui.theme_id()))
}
pub fn render_and_get_buffer_at_with_theme(
terminal: &mut Terminal<TestBackend>,
state: &mut AppState,
now: Instant,
theme: &ThemePalette,
) -> Buffer {
render_and_get_buffer_at_with_theme_and_services(
terminal,
state,
now,
theme,
&AppServices::stub(),
)
}
pub fn render_and_get_buffer_at_with_theme_and_services(
terminal: &mut Terminal<TestBackend>,
state: &mut AppState,
now: Instant,
theme: &ThemePalette,
services: &AppServices,
) -> Buffer {
terminal
.draw(|frame| {
let output = MainLayout::render_with_theme(
frame,
state,
Some(FIXED_TIME_MS),
services,
now,
theme,
);
state.apply_render_output(output);
})
.unwrap();
terminal.backend().buffer().clone()
}
pub fn render_to_string(terminal: &mut Terminal<TestBackend>, state: &mut AppState) -> String {
let buffer = render_and_get_buffer(terminal, state);
buffer_to_string(&buffer)
}
pub fn render_to_string_with_services(
terminal: &mut Terminal<TestBackend>,
state: &mut AppState,
services: &AppServices,
) -> String {
let buffer = render_and_get_buffer_at_with_theme_and_services(
terminal,
state,
test_instant(),
palette_for(state.ui.theme_id()),
services,
);
buffer_to_string(&buffer)
}
pub fn render_and_get_cursor_position(
terminal: &mut Terminal<TestBackend>,
state: &mut AppState,
) -> Position {
let _ = render_and_get_buffer(terminal, state);
terminal.backend_mut().get_cursor_position().unwrap()
}
fn buffer_to_string(buffer: &Buffer) -> String {
let mut result = String::new();
for y in 0..buffer.area.height {
for x in 0..buffer.area.width {
let cell = buffer.cell((x, y)).unwrap();
result.push_str(cell.symbol());
}
if y < buffer.area.height - 1 {
result.push('\n');
}
}
result
}
pub fn postgres_connected_state() -> AppState {
let mut state = create_test_state();
state
.session
.mark_connected(Arc::new(fixtures::sample_metadata()));
state
}
pub fn explorer_selected_state() -> AppState {
let mut state = postgres_connected_state();
state.ui.set_explorer_selection(Some(0));
state
}
pub fn table_detail_loaded_state() -> AppState {
let mut state = explorer_selected_state();
let _ = state
.session
.set_table_detail(fixtures::sample_postgres_table_detail(), 0);
state
}
pub fn with_current_result(state: &mut AppState) {
state
.query
.set_current_result(Arc::new(fixtures::sample_query_result()));
}