use std::sync::Arc;
use waydriver::{CompositorRuntime, Error, InputBackend, Session, SessionConfig};
use waydriver_capture_mutter::MutterCapture;
use waydriver_compositor_mutter::{MutterCompositor, MutterState};
use waydriver_input_mutter::MutterInput;
fn extract_png(raw: &[u8]) -> Vec<u8> {
let png_start = raw
.windows(4)
.position(|w| w == [0x89, b'P', b'N', b'G'])
.expect("no PNG magic found in screenshot data");
raw[png_start..].to_vec()
}
async fn start_calculator_session() -> anyhow::Result<(Session, Arc<MutterState>)> {
let mut compositor = MutterCompositor::new();
compositor.start().await?;
let state = compositor.state();
let input = MutterInput::new(state.clone());
let capture = MutterCapture::new(state.clone());
let session = Session::start(
Box::new(compositor),
Box::new(input),
Box::new(capture),
SessionConfig {
command: "gnome-calculator".into(),
args: vec![],
cwd: None,
app_name: "gnome-calculator".into(),
},
)
.await?;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
session.press_keysym(0xff1b).await?; tokio::time::sleep(std::time::Duration::from_millis(200)).await;
Ok((session, state))
}
#[tokio::test]
#[ignore = "flaky: shared gnome-calculator instance on host a11y bus"]
async fn calculator_screenshots_change() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.try_init()
.ok();
let (session, _state) = start_calculator_session().await?;
let baseline = extract_png(&session.take_screenshot().await?);
assert!(baseline.len() > 1000, "baseline screenshot too small");
for keysym in [0x36 , 0x3d ] {
session.press_keysym(keysym).await?;
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
let after_input = extract_png(&session.take_screenshot().await?);
let img1 = image::load_from_memory(&baseline)?.to_rgba8();
let img2 = image::load_from_memory(&after_input)?.to_rgba8();
let diff_pixels = img1
.pixels()
.zip(img2.pixels())
.filter(|(a, b)| a != b)
.count();
eprintln!("pixel diff: {diff_pixels} / {} pixels", img1.pixels().len());
session.kill().await?;
assert!(
diff_pixels > 100,
"screenshot should change after typing 6 = (only {diff_pixels} pixels differ)"
);
Ok(())
}
#[tokio::test]
#[ignore = "flaky: shared gnome-calculator instance on host a11y bus"]
async fn accessibility_tree_inspection() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.try_init()
.ok();
let (session, _state) = start_calculator_session().await?;
let tree = waydriver::atspi::dump_app_tree(
session.a11y_connection.as_ref().unwrap(),
&session.app_bus_name,
&session.app_path,
)
.await?;
assert!(!tree.is_empty(), "accessibility tree should not be empty");
assert!(
tree.contains("[button]"),
"tree should contain buttons, got:\n{tree}"
);
let (bus, path, role) = waydriver::atspi::find_element_by_name(
session.a11y_connection.as_ref().unwrap(),
&session.app_bus_name,
&session.app_path,
"1",
)
.await?;
assert!(!bus.is_empty());
assert!(!path.is_empty());
eprintln!("found '1' button: {bus}:{path} [{role}]");
let err = waydriver::atspi::find_element_by_name(
session.a11y_connection.as_ref().unwrap(),
&session.app_bus_name,
&session.app_path,
"nonexistent_element_xyz_12345",
)
.await
.unwrap_err();
assert!(
matches!(err, Error::ElementNotFound(_)),
"expected ElementNotFound, got: {err}"
);
session.kill().await?;
Ok(())
}
#[tokio::test]
#[ignore = "flaky: shared gnome-calculator instance on host a11y bus"]
async fn click_element_changes_display() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.try_init()
.ok();
let (session, _state) = start_calculator_session().await?;
let baseline = extract_png(&session.take_screenshot().await?);
let result = waydriver::atspi::click_element(
session.a11y_connection.as_ref().unwrap(),
&session.app_bus_name,
&session.app_path,
"5",
)
.await?;
eprintln!("click result: {result}");
session.press_keysym(0xffe1).await?; tokio::time::sleep(std::time::Duration::from_millis(300)).await;
session.press_keysym(0x2b).await?; tokio::time::sleep(std::time::Duration::from_millis(200)).await;
waydriver::atspi::click_element(
session.a11y_connection.as_ref().unwrap(),
&session.app_bus_name,
&session.app_path,
"3",
)
.await?;
session.press_keysym(0xffe1).await?; tokio::time::sleep(std::time::Duration::from_millis(300)).await;
session.press_keysym(0x3d).await?;
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let after_click = extract_png(&session.take_screenshot().await?);
let img1 = image::load_from_memory(&baseline)?.to_rgba8();
let img2 = image::load_from_memory(&after_click)?.to_rgba8();
let diff_pixels = img1
.pixels()
.zip(img2.pixels())
.filter(|(a, b)| a != b)
.count();
eprintln!("pixel diff after click: {diff_pixels}");
session.kill().await?;
assert!(
diff_pixels > 100,
"display should change after clicking 5 + 3 = (only {diff_pixels} pixels differ)"
);
Ok(())
}
#[tokio::test]
#[ignore = "flaky: shared gnome-calculator instance on host a11y bus"]
async fn pointer_input_operations() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.try_init()
.ok();
let (session, state) = start_calculator_session().await?;
assert!(
session.wayland_display().starts_with("wayland-wd-"),
"unexpected display name: {}",
session.wayland_display()
);
let pointer = MutterInput::new(state);
pointer.pointer_motion_relative(100.0, 100.0).await?;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
pointer.pointer_button(0x110).await?;
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
pointer.pointer_motion_relative(-50.0, -50.0).await?;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let screenshot = session.take_screenshot().await?;
let png = extract_png(&screenshot);
assert!(png.len() > 1000, "screenshot after pointer ops too small");
session.kill().await?;
Ok(())
}