144 lines
4.7 KiB
Rust
144 lines
4.7 KiB
Rust
//! App 级会话持久化的测试(临时数据目录,零网络)。
|
||
//! App-level session persistence tests (temp data dir, zero network).
|
||
|
||
use crossterm::event::{KeyModifiers, MouseEvent, MouseEventKind};
|
||
use focus_core::model::Message;
|
||
use focus_harness::session::SessionStore;
|
||
use focus_tui::app::App;
|
||
use std::path::PathBuf;
|
||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||
|
||
static DIR_SEQ: AtomicUsize = AtomicUsize::new(0);
|
||
|
||
fn temp_data_dir(tag: &str) -> PathBuf {
|
||
let seq = DIR_SEQ.fetch_add(1, Ordering::SeqCst);
|
||
let d = std::env::temp_dir().join(format!(
|
||
"focus-tui-app-{}-{}-{}",
|
||
tag,
|
||
std::process::id(),
|
||
seq
|
||
));
|
||
let _ = std::fs::remove_dir_all(&d);
|
||
std::fs::create_dir_all(&d).unwrap();
|
||
d
|
||
}
|
||
|
||
/// 构造一个使用临时数据目录的 App(避免环境变量在并行测试间竞争)。
|
||
/// Build an App on a temp data dir (avoids env-var races in parallel tests).
|
||
fn app_on(dir: &PathBuf) -> App {
|
||
let mut app = App::new();
|
||
app.store = SessionStore::new(dir);
|
||
app
|
||
}
|
||
|
||
#[test]
|
||
fn persist_and_reload_session() {
|
||
let dir = temp_data_dir("persist");
|
||
|
||
let mut app = app_on(&dir);
|
||
assert_eq!(app.session_id, None);
|
||
app.ensure_session();
|
||
let sid = app.session_id.clone().unwrap();
|
||
|
||
app.persist_entries(&[Message::user_text("first")]);
|
||
app.persist_entries(&[Message::user_text("second")]);
|
||
assert_eq!(app.persisted, 2);
|
||
assert_eq!(app.last_entry_id.as_deref(), Some("e2"));
|
||
|
||
// 另一个 App 实例加载同一会话。
|
||
// Another App instance loads the same session.
|
||
let mut app2 = app_on(&dir);
|
||
app2.load_session(&sid);
|
||
assert_eq!(app2.transcript.len(), 2);
|
||
assert_eq!(app2.persisted, 2);
|
||
let text = match &app2.transcript[1] {
|
||
Message::User(u) => u.content[0].as_text().map(|t| t.text.clone()),
|
||
_ => None,
|
||
};
|
||
assert_eq!(text.as_deref(), Some("second"));
|
||
|
||
// 续写:追加消息(parentId 链延续到 e3)。
|
||
// Continue: appending continues the parentId chain at e3.
|
||
app2.persist_entries(&[Message::user_text("third")]);
|
||
assert_eq!(app2.last_entry_id.as_deref(), Some("e3"));
|
||
let sessions = app2.store.list_sessions().unwrap();
|
||
assert_eq!(sessions, vec![sid.clone()]);
|
||
}
|
||
|
||
#[test]
|
||
fn new_session_resets_state() {
|
||
let dir = temp_data_dir("new");
|
||
|
||
let mut app = app_on(&dir);
|
||
app.ensure_session();
|
||
let sid1 = app.session_id.clone().unwrap();
|
||
app.persist_entries(&[Message::user_text("hi")]);
|
||
assert_eq!(app.persisted, 1);
|
||
|
||
app.new_session();
|
||
assert_eq!(app.session_id, None);
|
||
assert_eq!(app.transcript.len(), 0);
|
||
assert_eq!(app.persisted, 0);
|
||
assert_eq!(app.last_entry_id, None);
|
||
|
||
app.ensure_session();
|
||
let sid2 = app.session_id.clone().unwrap();
|
||
assert_ne!(sid1, sid2);
|
||
}
|
||
|
||
/// 滚轮方向与逐行滚动(回归:此前 ScrollUp/ScrollDown 方向写反,且贴底时
|
||
/// 上滚被钳回底部导致滚轮失灵)。
|
||
/// Wheel direction and line-by-line scrolling (regression: both wheel
|
||
/// directions used to scroll down, and scrolling up at the bottom was clamped
|
||
/// back, killing the wheel).
|
||
#[test]
|
||
fn wheel_scrolls_line_by_line_in_correct_direction() {
|
||
use focus_tui::app::BlockGeometry;
|
||
use focus_tui::state::BlockId;
|
||
|
||
let mut app = app_on(&temp_data_dir("wheel"));
|
||
// 模拟内容 100 行、视口 10 行的几何。
|
||
// Simulate geometry: 100 content lines, 10-line viewport.
|
||
app.geometry = vec![BlockGeometry {
|
||
id: BlockId("x".into()),
|
||
start: 0,
|
||
end: 100,
|
||
}];
|
||
app.view_height = 10;
|
||
|
||
// 贴底时向上滚:偏移减 1、取消 follow。
|
||
// At the bottom, scrolling up: offset -1, follow cleared.
|
||
app.scroll = 90;
|
||
app.follow = true;
|
||
app.handle_mouse(scroll_event(MouseEventKind::ScrollUp));
|
||
assert_eq!(app.scroll, 89, "wheel up must move up one line");
|
||
assert!(!app.follow);
|
||
|
||
// 向下滚:偏移 +1,逐行。
|
||
// Wheel down: offset +1, line by line.
|
||
app.handle_mouse(scroll_event(MouseEventKind::ScrollDown));
|
||
assert_eq!(app.scroll, 90);
|
||
assert!(app.follow, "reaching the bottom restores follow");
|
||
|
||
// 从顶部向下滚一次只动一行(不是直接跳到底部)。
|
||
// From the top, one notch moves exactly one line (no jump to the bottom).
|
||
app.scroll = 0;
|
||
app.follow = false;
|
||
app.handle_mouse(scroll_event(MouseEventKind::ScrollDown));
|
||
assert_eq!(app.scroll, 1);
|
||
|
||
// 顶部再向上滚不会下溢。
|
||
// Scrolling up at the top does not underflow.
|
||
app.handle_mouse(scroll_event(MouseEventKind::ScrollUp));
|
||
assert_eq!(app.scroll, 0);
|
||
}
|
||
|
||
fn scroll_event(kind: MouseEventKind) -> MouseEvent {
|
||
MouseEvent {
|
||
kind,
|
||
column: 0,
|
||
row: 0,
|
||
modifiers: KeyModifiers::NONE,
|
||
}
|
||
}
|