#![allow(
unused_must_use,
reason = "tests drive the state, and check only the commands they care about"
)]
use super::*;
use crate::api::browse::{IssueKind, IssueState, PrState, SearchKind, SearchResults, Status};
use crate::api::change::Change;
use crate::api::model::{ChecksState, PrDetail, RepoId, ReviewDecision};
use crate::api::rate_limit::{Bucket, RateLimits};
use crate::fixtures;
use crate::fixtures::{
answer, diff_msg, fetched, pr_detail, press, says, select_comment, thread, threads_loaded,
warns,
};
use crate::picker::{Choice, fuzzy_score};
use crate::route::{Dest, OPEN, issues};
use crate::theme::{ColorDepth, DEFAULT_SEED, Mode, Theme};
use crate::ui::annotations::{AnnotationKey, Place};
use crate::ui::overlays::PALETTE_ROWS;
use crate::ui::pages::{PrTab, ProfileTab};
macro_rules! overlay {
($state:expr, $kind:ident) => {
match &$state.overlay {
Some(Overlay::$kind(overlay)) => overlay,
_ => panic!("no {} overlay", stringify!($kind)),
}
};
}
fn mine(n: u64, total: u64) -> SearchResults {
let row = |n| crate::fixtures::found_pr(&format!("o/r#{n}"), &format!("PR {n}"), PrState::Open);
crate::fixtures::found_prs((1..=n).map(row).collect(), total)
}
fn mine_key(state: &State) -> DataKey {
state.home[1].search.clone().unwrap().key()
}
fn mine_failed(state: &mut State, err: ApiError) {
let key = mine_key(state);
update(state, Msg::fetched(key, Err(err)));
}
fn with_prs(n: u64) -> State {
let mut state = state();
crate::fixtures::section(&mut state, 1, mine(n, n));
state
}
pub(super) fn act(state: &mut State, action: Action) -> Cmds {
let mut cmds = apply(state, action);
state.sync_page();
cmds.extend(state.settle_diff());
cmds
}
fn page(state: &State) -> &PageScreen {
state.screen().page().expect("on a page")
}
fn word_here(s: &State) -> (Option<u32>, Option<String>) {
let p = page(s);
let word = (p.word_cursor()).map(|(_, code, w)| code.get(w.bytes).unwrap().to_owned());
(p.selected.and_then(|i| p.line_of(i)), word)
}
fn on_diff(state: &State) -> &DiffScreen {
state.screen().diff().expect("on the diff")
}
fn picked(state: &State) -> Vec<(String, String)> {
let rows = state.picker_rows(overlay!(state, Picker));
(rows.into_iter()).map(|(r, _)| (r.label, r.hint)).collect()
}
fn picked_labels(state: &State) -> Vec<String> {
picked(state).into_iter().map(|(label, _)| label).collect()
}
fn ask_labels(state: &State) -> Vec<&str> {
let ask = overlay!(state, Ask);
ask.choices.iter().map(|(l, _)| l.as_str()).collect()
}
fn route(state: &State) -> Route {
page(state).route.clone()
}
fn selected_text(state: &State) -> String {
let p = page(state);
let item = p.page().items[p.selected.expect("a selection")];
p.page().lines[item.start].text()
}
fn next_watch(s: &mut State) -> (Cmds, u64) {
let timer = timers(s).into_iter().find_map(|cmd| match cmd {
Cmd::Timer(Timer::Watch(id), ms) => Some((id, ms)),
_ => None,
});
let (id, ms) = timer.expect("an ask scheduled");
(update(s, Msg::Timer(Timer::Watch(id))), ms)
}
fn changed(etag: &str, body: &str) -> Result<crate::api::Polled, ApiError> {
Ok(crate::api::Polled::Changed {
etag: etag.into(),
body: body.into(),
})
}
fn fetch(key: DataKey) -> Cmd {
Cmd::Api(Api::Fetch { key, cached: true })
}
fn refetch(key: DataKey) -> Cmd {
Cmd::Api(Api::Fetch { key, cached: false })
}
fn more(key: &DataKey, after: &str) -> Cmd {
let (key, after) = (key.clone(), after.to_owned());
Cmd::Api(Api::FetchMore { key, after })
}
fn fetches(cmds: Cmds) -> Cmds {
cmds.into_iter()
.filter(|c| !matches!(c, Cmd::Api(Api::SaveVisits(_))))
.collect()
}
fn issues(query: &str) -> Route {
let (repo, query) = (ghtui(), query.to_owned());
Route::Issues(issues::Issues { repo, query })
}
fn issue(number: u64) -> Route {
let repo = ghtui();
Route::Issues(issues::Issue { repo, number })
}
fn blob(rev: &str, path: &str) -> Route {
Route::blob(ghtui(), rev.to_owned(), path.to_owned())
}
fn click(state: &mut State, x: u16, y: u16) -> Cmds {
update(
state,
Msg::Mouse(MouseEvent {
kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
column: x,
row: y,
modifiers: KeyModifiers::NONE,
}),
)
}
fn find(state: &State, text: &str) -> (u16, u16) {
let p = page(state);
let area = state.page_area();
let cols = crate::ui::page::columns(p.page(), area);
for (row, line) in p.page().lines.iter().enumerate().skip(p.scroll) {
let at = line.text().find(text);
if let Some(at) = at {
let x = cols.main_x + 2 + line.indent + u16::try_from(at).unwrap();
return (x, area.y + u16::try_from(row - p.scroll).unwrap());
}
}
panic!("{text} isn't on screen")
}
pub(crate) const NOW: u64 = 1_791_028_800;
pub(crate) fn page_text(state: &State) -> String {
let lines = page(state).page().lines.iter();
let lines = lines.map(crate::ui::page::PageLine::text);
lines.collect::<Vec<_>>().join("\n")
}
pub(crate) fn draw(state: &State) -> ratatui::buffer::Buffer {
let (w, h) = state.size;
let backend = ratatui::backend::TestBackend::new(w, h);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal
.draw(|frame| crate::view::view(state, frame, NOW))
.unwrap();
terminal.backend().buffer().clone()
}
pub(crate) fn screen_text(state: &State) -> String {
let buf = draw(state);
let area = buf.area;
let row = |y| {
let mut skip = 0;
let mut text = String::new();
for x in area.left()..area.right() {
let symbol = buf[(x, y)].symbol();
if skip == 0 {
text.push_str(symbol);
skip = crate::ui::text::width(symbol).saturating_sub(1);
} else {
skip -= 1;
}
}
text
};
let rows = (area.top()..area.bottom()).map(row);
rows.collect::<Vec<_>>().join("\n")
}
mod screens;
use diff::{commit_of, fixture_diff, screen_state};
pub(crate) use screens::*;
mod changes;
mod code_nav;
mod create;
mod discussions;
mod edits;
mod failures;
mod features;
mod housekeeping;
mod manage;
mod merging;
mod pages;
mod projects;
mod releases;
mod reviews;
mod security;
mod settings;
mod site;
mod smoke;
mod workflows;
mod writes;
mod tabs;
pub(super) mod diff;
mod fetch;
mod home;
mod jobs;
mod keys;
mod links;
mod lists;
mod offline;
mod pr_diff;
mod rebuilds;
mod repo;
mod search;
mod watch;