use super::*;
use std::collections::HashSet;
use crate::api::Polled;
use crate::api::browse::{CheckOutcome, JobLog};
use crate::live::Ask;
use crate::live::page::Probe;
fn next_ask(s: &mut State) -> (Vec<Ask<Probe>>, 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");
let cmds = update(s, Msg::Timer(Timer::Watch(id)));
match &cmds[..] {
[Cmd::Api(Api::Watch { asks, .. })] => (asks.clone(), ms),
_ => panic!("{cmds:?}"),
}
}
fn answer_with(s: &mut State, answers: Vec<(Probe, Result<Polled, ApiError>)>) -> Vec<Cmd> {
let what = s.page_watch.as_ref().expect("a watch").what.clone();
update(s, Msg::Watched(what, answers))
}
fn changed(etag: &str, body: &str) -> Result<Polled, ApiError> {
Ok(Polled::Changed {
etag: etag.into(),
body: body.into(),
})
}
fn rest_issue(comments: u64) -> String {
let issue = crate::fixtures::issue();
serde_json::json!({
"title": issue.title,
"body": issue.body,
"state": "open",
"state_reason": null,
"comments": comments,
"labels": [{"name": "ui"}, {"name": "bug"}],
"assignees": [{"login": "hubot"}],
})
.to_string()
}
fn on_issue() -> (State, DataKey) {
(at(with_repo(), issue(14)), DataKey::Issue(ghtui(), 14))
}
#[test]
fn an_issue_is_watched_and_another_authors_comment_is_new() {
let (mut s, key) = on_issue();
let (asks, wait) = next_ask(&mut s);
assert_eq!(wait, crate::live::FIRST_MS);
assert!(matches!(
&asks[..],
[
Ask {
probe: Probe::Issue(_, 14),
etag: None
},
Ask {
probe: Probe::IssueComments(_, 14, _),
etag: None
},
]
));
let comments = asks[1].probe.clone();
let first = vec![
(Probe::Issue(ghtui(), 14), changed("\"i1\"", &rest_issue(1))),
(comments.clone(), changed("\"c1\"", "[]")),
];
assert_eq!(
crate::fixtures::asked(&answer_with(&mut s, first)),
Vec::new()
);
let (asks, wait) = next_ask(&mut s);
assert!(wait > crate::live::FIRST_MS);
assert_eq!(asks[0].etag.as_deref(), Some("\"i1\""));
let came = vec![
(Probe::Issue(ghtui(), 14), changed("\"i2\"", &rest_issue(3))),
(comments, Ok(Polled::Same)),
];
assert_eq!(
crate::fixtures::asked(&answer_with(&mut s, came)),
std::slice::from_ref(&key)
);
let mut issue = crate::fixtures::issue();
let comment = |id, author: &str| crate::api::browse::Comment {
id: Some(id),
author: author.into(),
body: format!("from {author}"),
created_at: "2026-10-03T12:00:00Z".into(),
editable: None,
hiding: crate::api::manage::Hiding::default(),
};
let first = issue.comments().next().cloned().expect("a comment");
issue.timeline = vec![
crate::api::timeline::Item::Comment(first),
crate::api::timeline::Item::Comment(comment(2, "monalisa")),
crate::api::timeline::Item::Comment(comment(3, "octocat")),
]
.into();
issue.comment_count = 3;
fetched(&mut s, key, Data::Issue(Some(Box::new(issue))));
says(&s, "New on gold-silver-copper/ghtui#14: 1 comment");
let fresh = crate::live::page::fresh(&s, &route(&s)).cloned();
assert_eq!(fresh, Some(HashSet::from([2])));
s.sync_page();
let lines = &page(&s).page().lines;
let badged: Vec<String> = (lines.iter())
.filter(|l| l.right.iter().any(|r| r.text().contains("New")))
.map(crate::ui::page::PageLine::text)
.collect();
assert_eq!(badged, ["monalisa commented just now"]);
}
#[test]
fn a_first_answer_newer_than_the_issue_is_fetched() {
let (mut s, key) = on_issue();
let (asks, _) = next_ask(&mut s);
let comments = asks[1].probe.clone();
let first = vec![
(Probe::Issue(ghtui(), 14), changed("\"i1\"", &rest_issue(2))),
(comments, changed("\"c1\"", "[]")),
];
assert_eq!(crate::fixtures::asked(&answer_with(&mut s, first)), [key]);
}
#[test]
fn a_running_job_is_followed_to_its_end() {
let mut s = with_job(None);
let (done, log) = crate::fixtures::job();
let mut running = done.clone();
running.outcome = CheckOutcome::Pending;
running.run.outcome = CheckOutcome::Pending;
for step in &mut running.steps[2..] {
step.outcome = CheckOutcome::Pending;
step.completed_at = None;
}
running.steps[3].started_at = None;
fetched(
&mut s,
DataKey::Job(ghtui(), 2),
Data::Job(Box::new(running)),
);
let waiting = JobLog {
running: true,
..JobLog::default()
};
fetched(
&mut s,
DataKey::JobLog(ghtui(), 2),
Data::Log(Arc::new(waiting)),
);
let (asks, _) = next_ask(&mut s);
let shown = |s: &State| {
let p = page(s).page();
p.jump
.and_then(|j| p.lines.get(j))
.map(crate::ui::page::PageLine::text)
.unwrap_or_default()
};
assert!(shown(&s).contains("cargo test"), "{}", shown(&s));
assert_eq!(asks[0].probe, Probe::Job(ghtui(), 2));
let body = serde_json::json!({
"id": 2, "run_id": 7, "name": "test (ubuntu)", "status": "completed",
"conclusion": "failure", "started_at": null, "completed_at": null, "steps": [],
});
let cmds = answer_with(
&mut s,
vec![(asks[0].probe.clone(), changed("\"j\"", &body.to_string()))],
);
let keys = crate::fixtures::asked(&cmds);
assert!(keys.contains(&DataKey::Job(ghtui(), 2)), "{keys:?}");
assert!(keys.contains(&DataKey::JobLog(ghtui(), 2)), "{keys:?}");
fetched(&mut s, DataKey::Job(ghtui(), 2), Data::Job(Box::new(done)));
says(&s, "test (ubuntu) failed");
fetched(
&mut s,
DataKey::JobLog(ghtui(), 2),
Data::Log(Arc::new(log)),
);
s.sync_page();
assert!(shown(&s).contains("exit code 101"), "{}", shown(&s));
let asking = timers(&mut s)
.into_iter()
.any(|cmd| matches!(cmd, Cmd::Timer(Timer::Watch(_), _)));
assert!(!asking);
}
#[test]
fn new_runs_on_the_actions_tab_are_said() {
let mut s = at(with_repo(), Route::actions(ghtui()));
let query = String::new();
let key = DataKey::Runs(ghtui(), query.clone());
let (_, runs) = crate::fixtures::workflow_runs();
let (asks, _) = next_ask(&mut s);
assert_eq!(asks[0].probe, Probe::Runs(ghtui(), query));
let wire = serde_json::json!({ "total_count": 1, "workflow_runs": [{
"id": 999, "name": "CI", "display_title": "New", "run_number": 99, "event": "push",
"head_branch": "main", "head_sha": "abc", "status": "queued", "conclusion": null,
"actor": {"login": "monalisa"}, "run_started_at": null, "created_at": null,
"updated_at": null,
}]});
let cmds = answer_with(
&mut s,
vec![(asks[0].probe.clone(), changed("\"r\"", &wire.to_string()))],
);
assert_eq!(crate::fixtures::asked(&cmds), std::slice::from_ref(&key));
let mut more = runs;
let mut new = more.items[0].clone();
(new.id, new.actor) = (999, Some("monalisa".into()));
more.items.insert(0, new);
fetched(&mut s, key, Data::Runs(Box::new(more)));
says(&s, "New on the Actions tab: 1 run");
}