#[cfg(test)]
mod screenshot_annotate_tests {
use crate::computer::screenshot::{screenshot, screenshot_tool_result};
use crate::computer::types::{
ComputerScreenshot, LegendEntryOutput, OmissionOutput, ScreenshotOutput,
};
fn input() -> ComputerScreenshot {
ComputerScreenshot {
annotate: true,
app: Some("TestApp".into()),
..ComputerScreenshot::default()
}
}
#[tokio::test]
async fn annotate_requires_app_or_pid() {
let input = ComputerScreenshot {
annotate: true,
..ComputerScreenshot::default()
};
let err = screenshot(&input).await.unwrap_err();
assert!(err.contains("requires `app`, `pid` or `surface`"), "{err}");
}
#[tokio::test]
async fn annotate_rejects_nth() {
let mut input = input();
input.nth = Some(2);
let err = screenshot(&input).await.unwrap_err();
assert!(err.contains("does not apply to annotated"), "{err}");
}
#[tokio::test]
async fn annotate_rejects_region() {
let mut input = input();
input.region = Some(vec![0, 0, 100, 100]);
let err = screenshot(&input).await.unwrap_err();
assert!(err.contains("mutually exclusive"), "{err}");
}
#[tokio::test]
async fn plain_app_without_selector_still_rejected() {
let input = ComputerScreenshot {
app: Some("TestApp".into()),
..ComputerScreenshot::default()
};
let err = screenshot(&input).await.unwrap_err();
assert!(err.contains("requires `selector`"), "{err}");
}
#[tokio::test]
async fn annotate_rejects_alternation_selector() {
let mut input = input();
input.selector = Some("button, link".into());
let err = screenshot(&input).await.unwrap_err();
assert!(err.contains("alternation"), "unexpected error: {err}");
}
#[tokio::test]
async fn comma_inside_quoted_name_is_not_an_alternation() {
let mut input = input();
input.selector = Some("button[name='Save, As']".into());
let err = screenshot(&input).await.unwrap_err();
assert!(
!err.contains("alternation"),
"quoted comma must not trip the alternation check: {err}"
);
}
#[test]
fn quoted_spans_are_blanked_for_clause_detection() {
let blanked =
crate::computer::screenshot::without_quoted_spans("button[name='Save, As'], link");
assert!(blanked.contains(", link"), "{blanked}");
assert!(!blanked.contains(", As"), "{blanked}");
let blanked = crate::computer::screenshot::without_quoted_spans("button[name='a");
assert!(!blanked.contains(','), "{blanked}");
}
fn output(legend: Vec<LegendEntryOutput>) -> ScreenshotOutput {
ScreenshotOutput {
width: 800,
height: 600,
desktop_origin: (0, 0),
desktop_scale: (1.0, 1.0),
bytes: 42,
legend,
omitted: Vec::new(),
truncated: 0,
images: Vec::new(),
}
}
#[test]
fn plain_result_has_no_legend_text() {
let msg = screenshot_tool_result("t1", &output(Vec::new()));
let text = msg.content.as_deref().unwrap_or_default();
assert!(!text.contains("Annotated elements"));
assert!(text.contains("screenshot 800x600"));
}
#[test]
fn annotated_result_renders_legend_lines() {
let msg = screenshot_tool_result(
"t1",
&output(vec![LegendEntryOutput {
tag: "B7".into(),
selector: "button[name='Export']:nth(7)".into(),
role: "button".into(),
name: Some("Export".into()),
color: [255, 0, 0],
}]),
);
let text = msg.content.as_deref().unwrap_or_default();
assert!(text.contains("Annotated elements"), "{text}");
assert!(
text.contains("B7 button[name='Export']:nth(7) button 'Export'"),
"{text}"
);
}
#[test]
fn annotated_result_renders_omission_and_truncation_lines() {
let mut out = output(vec![LegendEntryOutput {
tag: "A1".into(),
selector: "button:nth(1)".into(),
role: "button".into(),
name: None,
color: [0, 255, 0],
}]);
out.omitted.push(OmissionOutput {
selector: "list_item:nth(3)".into(),
role: "list_item".into(),
name: Some("Hidden".into()),
reason: "outside_capture".into(),
});
out.truncated = 12;
let text = screenshot_tool_result("t1", &out)
.content
.as_deref()
.unwrap_or_default()
.to_string();
assert!(text.contains("A1 button:nth(1) button"), "{text}");
assert!(
text.contains("not drawn: list_item:nth(3) list_item (outside_capture)"),
"{text}"
);
assert!(text.contains("legend truncated: 12"), "{text}");
}
#[test]
fn empty_legend_still_reports_omissions_and_truncation() {
let mut out = output(Vec::new());
out.omitted.push(OmissionOutput {
selector: "group:nth(1)".into(),
role: "group".into(),
name: None,
reason: "zero_area".into(),
});
out.truncated = 4;
let text = screenshot_tool_result("t1", &out)
.content
.as_deref()
.unwrap_or_default()
.to_string();
assert!(!text.contains("Annotated elements"), "{text}");
assert!(
text.contains("not drawn: group:nth(1) group (zero_area)"),
"{text}"
);
assert!(text.contains("legend truncated: 4"), "{text}");
}
}