Skip to content

Commit e53692e

Browse files
authored
Add snapshot test using insta (#2411)
1 parent b6ce67d commit e53692e

File tree

9 files changed

+400
-116
lines changed

9 files changed

+400
-116
lines changed

Cargo.lock

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
8989

9090
[dev-dependencies]
9191
env_logger = "0.11"
92+
git2-testing = { path = "./git2-testing" }
93+
insta = { version = "1.41.0", features = ["filters"] }
9294
pretty_assertions = "1.4"
9395
tempfile = "3"
9496

git2-testing/src/lib.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ pub fn repo_init_empty() -> (TempDir, Repository) {
2020
(td, repo)
2121
}
2222

23-
/// initialize test repo in temp path with an empty first commit
24-
pub fn repo_init() -> (TempDir, Repository) {
23+
/// initialize test repo in temp path with given suffix and an empty first commit
24+
pub fn repo_init_suffix<T: AsRef<std::ffi::OsStr>>(
25+
suffix: Option<T>,
26+
) -> (TempDir, Repository) {
2527
init_log();
2628

2729
sandbox_config_files();
2830

29-
let td = TempDir::new().unwrap();
31+
let td = match suffix {
32+
Some(suffix) => TempDir::with_suffix(suffix).unwrap(),
33+
None => TempDir::new().unwrap(),
34+
};
3035
let repo = Repository::init(td.path()).unwrap();
3136
{
3237
let mut config = repo.config().unwrap();
@@ -45,6 +50,11 @@ pub fn repo_init() -> (TempDir, Repository) {
4550
(td, repo)
4651
}
4752

53+
/// initialize test repo in temp path with an empty first commit
54+
pub fn repo_init() -> (TempDir, Repository) {
55+
repo_init_suffix::<&std::ffi::OsStr>(None)
56+
}
57+
4858
// init log
4959
fn init_log() {
5060
let _ = env_logger::builder()

src/gitui.rs

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
use std::time::Instant;
2+
3+
use anyhow::Result;
4+
use asyncgit::{sync::utils::repo_work_dir, AsyncGitNotification};
5+
use crossbeam_channel::{never, tick, unbounded, Receiver};
6+
use scopetime::scope_time;
7+
8+
#[cfg(test)]
9+
use crossterm::event::{KeyCode, KeyModifiers};
10+
11+
use crate::{
12+
app::{App, QuitState},
13+
args::CliArgs,
14+
draw,
15+
input::{Input, InputEvent, InputState},
16+
keys::KeyConfig,
17+
select_event,
18+
spinner::Spinner,
19+
ui::style::Theme,
20+
watcher::RepoWatcher,
21+
AsyncAppNotification, AsyncNotification, QueueEvent, Updater,
22+
SPINNER_INTERVAL, TICK_INTERVAL,
23+
};
24+
25+
pub struct Gitui {
26+
app: crate::app::App,
27+
rx_input: Receiver<InputEvent>,
28+
rx_git: Receiver<AsyncGitNotification>,
29+
rx_app: Receiver<AsyncAppNotification>,
30+
rx_ticker: Receiver<Instant>,
31+
rx_watcher: Receiver<()>,
32+
}
33+
34+
impl Gitui {
35+
pub(crate) fn new(
36+
cliargs: CliArgs,
37+
theme: Theme,
38+
key_config: &KeyConfig,
39+
updater: Updater,
40+
) -> Result<Self, anyhow::Error> {
41+
let (tx_git, rx_git) = unbounded();
42+
let (tx_app, rx_app) = unbounded();
43+
44+
let input = Input::new();
45+
46+
let (rx_ticker, rx_watcher) = match updater {
47+
Updater::NotifyWatcher => {
48+
let repo_watcher = RepoWatcher::new(
49+
repo_work_dir(&cliargs.repo_path)?.as_str(),
50+
);
51+
52+
(never(), repo_watcher.receiver())
53+
}
54+
Updater::Ticker => (tick(TICK_INTERVAL), never()),
55+
};
56+
57+
let app = App::new(
58+
cliargs,
59+
tx_git,
60+
tx_app,
61+
input.clone(),
62+
theme,
63+
key_config.clone(),
64+
)?;
65+
66+
Ok(Self {
67+
app,
68+
rx_input: input.receiver(),
69+
rx_git,
70+
rx_app,
71+
rx_ticker,
72+
rx_watcher,
73+
})
74+
}
75+
76+
pub(crate) fn run_main_loop<B: ratatui::backend::Backend>(
77+
&mut self,
78+
terminal: &mut ratatui::Terminal<B>,
79+
) -> Result<QuitState, anyhow::Error> {
80+
let spinner_ticker = tick(SPINNER_INTERVAL);
81+
let mut spinner = Spinner::default();
82+
83+
self.app.update()?;
84+
85+
loop {
86+
let event = select_event(
87+
&self.rx_input,
88+
&self.rx_git,
89+
&self.rx_app,
90+
&self.rx_ticker,
91+
&self.rx_watcher,
92+
&spinner_ticker,
93+
)?;
94+
95+
{
96+
if matches!(event, QueueEvent::SpinnerUpdate) {
97+
spinner.update();
98+
spinner.draw(terminal)?;
99+
continue;
100+
}
101+
102+
scope_time!("loop");
103+
104+
match event {
105+
QueueEvent::InputEvent(ev) => {
106+
if matches!(
107+
ev,
108+
InputEvent::State(InputState::Polling)
109+
) {
110+
//Note: external ed closed, we need to re-hide cursor
111+
terminal.hide_cursor()?;
112+
}
113+
self.app.event(ev)?;
114+
}
115+
QueueEvent::Tick | QueueEvent::Notify => {
116+
self.app.update()?;
117+
}
118+
QueueEvent::AsyncEvent(ev) => {
119+
if !matches!(
120+
ev,
121+
AsyncNotification::Git(
122+
AsyncGitNotification::FinishUnchanged
123+
)
124+
) {
125+
self.app.update_async(ev)?;
126+
}
127+
}
128+
QueueEvent::SpinnerUpdate => unreachable!(),
129+
}
130+
131+
self.draw(terminal)?;
132+
133+
spinner.set_state(self.app.any_work_pending());
134+
spinner.draw(terminal)?;
135+
136+
if self.app.is_quit() {
137+
break;
138+
}
139+
}
140+
}
141+
142+
Ok(self.app.quit_state())
143+
}
144+
145+
fn draw<B: ratatui::backend::Backend>(
146+
&self,
147+
terminal: &mut ratatui::Terminal<B>,
148+
) -> std::io::Result<()> {
149+
draw(terminal, &self.app)
150+
}
151+
152+
#[cfg(test)]
153+
fn update_async(&mut self, event: crate::AsyncNotification) {
154+
self.app.update_async(event).unwrap();
155+
}
156+
157+
#[cfg(test)]
158+
fn input_event(
159+
&mut self,
160+
code: KeyCode,
161+
modifiers: KeyModifiers,
162+
) {
163+
let event = crossterm::event::KeyEvent::new(code, modifiers);
164+
self.app
165+
.event(crate::input::InputEvent::Input(
166+
crossterm::event::Event::Key(event),
167+
))
168+
.unwrap();
169+
}
170+
171+
#[cfg(test)]
172+
fn wait_for_async_git_notification(
173+
&self,
174+
expected: AsyncGitNotification,
175+
) {
176+
loop {
177+
let actual = self
178+
.rx_git
179+
.recv_timeout(std::time::Duration::from_millis(100))
180+
.unwrap();
181+
182+
if actual == expected {
183+
break;
184+
}
185+
}
186+
}
187+
188+
#[cfg(test)]
189+
fn update(&mut self) {
190+
self.app.update().unwrap();
191+
}
192+
}
193+
194+
#[cfg(test)]
195+
mod tests {
196+
use std::path::PathBuf;
197+
198+
use asyncgit::{sync::RepoPath, AsyncGitNotification};
199+
use crossterm::event::{KeyCode, KeyModifiers};
200+
use git2_testing::repo_init_suffix;
201+
use insta::assert_snapshot;
202+
use ratatui::{backend::TestBackend, Terminal};
203+
204+
use crate::{
205+
args::CliArgs, gitui::Gitui, keys::KeyConfig,
206+
ui::style::Theme, AsyncNotification, Updater,
207+
};
208+
209+
// Macro adapted from: https://insta.rs/docs/cmd/
210+
macro_rules! apply_common_filters {
211+
{} => {
212+
let mut settings = insta::Settings::clone_current();
213+
// Windows and MacOS
214+
// We don't match on the full path, but on the suffix we pass to `repo_init_suffix` below.
215+
settings.add_filter(r" *\[…\]\S+-insta/?", "[TEMP_FILE]");
216+
// Linux Temp Folder
217+
settings.add_filter(r" */tmp/\.tmp\S+-insta/", "[TEMP_FILE]");
218+
// Commit ids that follow a vertical bar
219+
settings.add_filter(r"│[a-z0-9]{7} ", "│[AAAAA] ");
220+
let _bound = settings.bind_to_scope();
221+
}
222+
}
223+
224+
#[test]
225+
fn gitui_starts() {
226+
apply_common_filters!();
227+
228+
let (temp_dir, _repo) = repo_init_suffix(Some("-insta"));
229+
let path: RepoPath = temp_dir.path().to_str().unwrap().into();
230+
let cliargs = CliArgs {
231+
theme: PathBuf::from("theme.ron"),
232+
select_file: None,
233+
repo_path: path,
234+
notify_watcher: false,
235+
key_bindings_path: None,
236+
key_symbols_path: None,
237+
};
238+
239+
let theme = Theme::init(&PathBuf::new());
240+
let key_config = KeyConfig::default();
241+
242+
let mut gitui =
243+
Gitui::new(cliargs, theme, &key_config, Updater::Ticker)
244+
.unwrap();
245+
246+
let mut terminal =
247+
Terminal::new(TestBackend::new(90, 12)).unwrap();
248+
249+
gitui.draw(&mut terminal).unwrap();
250+
251+
assert_snapshot!("app_loading", terminal.backend());
252+
253+
let event =
254+
AsyncNotification::Git(AsyncGitNotification::Status);
255+
gitui.update_async(event);
256+
257+
gitui.draw(&mut terminal).unwrap();
258+
259+
assert_snapshot!("app_loading_finished", terminal.backend());
260+
261+
gitui.input_event(KeyCode::Char('2'), KeyModifiers::empty());
262+
gitui.input_event(
263+
key_config.keys.tab_log.code,
264+
key_config.keys.tab_log.modifiers,
265+
);
266+
267+
gitui.wait_for_async_git_notification(
268+
AsyncGitNotification::Log,
269+
);
270+
271+
gitui.update();
272+
273+
gitui.draw(&mut terminal).unwrap();
274+
275+
assert_snapshot!(
276+
"app_log_tab_showing_one_commit",
277+
terminal.backend()
278+
);
279+
}
280+
}

0 commit comments

Comments
 (0)