Skip to main content

integration_test/
integration_test.rs

1//! An integration test that connects to a running Bevy app via the BRP,
2//! finds a button's position, and sends a mouse click to press it.
3//!
4//! Run with the `bevy_remote` feature enabled:
5//! ```bash
6//! cargo run --example integration_test --features="bevy_remote"
7//! ```
8//! This example assumes that the `app_under_test` example is running on the same machine.
9
10use std::{any::type_name, io::BufRead};
11
12use anyhow::Result as AnyhowResult;
13use bevy::{
14    platform::collections::HashMap,
15    remote::{
16        builtin_methods::{
17            BrpObserveParams, BrpQuery, BrpQueryFilter, BrpQueryParams, BrpSpawnEntityParams,
18            BrpWriteMessageParams, ComponentSelector, BRP_OBSERVE_METHOD, BRP_QUERY_METHOD,
19            BRP_SPAWN_ENTITY_METHOD, BRP_WRITE_MESSAGE_METHOD,
20        },
21        http::{DEFAULT_ADDR, DEFAULT_PORT},
22        BrpRequest,
23    },
24    render::view::screenshot::{Screenshot, ScreenshotCaptured},
25    ui::{widget::Button, UiGlobalTransform},
26    window::{Window, WindowEvent},
27};
28
29fn main() -> AnyhowResult<()> {
30    let url = format!("http://{DEFAULT_ADDR}:{DEFAULT_PORT}/");
31
32    // Step 1: Take a screenshot via BRP
33    // The window must be visible (not fully occluded) for the GPU to render content
34    // If the window is hidden, the screenshot will be black
35    println!("Spawning Screenshot entity...");
36    let spawn_response = brp_request(
37        &url,
38        BRP_SPAWN_ENTITY_METHOD,
39        1,
40        &BrpSpawnEntityParams {
41            components: HashMap::from([(
42                type_name::<Screenshot>().to_string(),
43                serde_json::json!({"Window": "Primary"}),
44            )]),
45        },
46    )?;
47    let screenshot_entity = &spawn_response["result"]["entity"];
48
49    println!("Observing ScreenshotCaptured on entity {screenshot_entity}...");
50    let observe_response = ureq::post(&url).send_json(BrpRequest {
51        method: BRP_OBSERVE_METHOD.to_string(),
52        id: Some(serde_json::to_value(2)?),
53        params: Some(serde_json::to_value(BrpObserveParams {
54            event: type_name::<ScreenshotCaptured>().to_string(),
55            entity: Some(serde_json::from_value(screenshot_entity.clone())?),
56        })?),
57    })?;
58
59    println!("Waiting for screenshot capture...");
60    let reader = std::io::BufReader::new(observe_response.into_body().into_reader());
61    for line in reader.lines() {
62        let line = line?;
63        if let Some(json_str) = line.strip_prefix("data: ") {
64            let response: serde_json::Value = serde_json::from_str(json_str)?;
65            if let Some(error) = response.get("error") {
66                anyhow::bail!("Observe error: {error}");
67            }
68            if let Some(result) = response.get("result") {
69                let events = result.as_array().expect("Expected events array");
70                let event = &events[0];
71
72                let image_data = &event["image"];
73                let width = image_data["texture_descriptor"]["size"]["width"]
74                    .as_u64()
75                    .unwrap();
76                let height = image_data["texture_descriptor"]["size"]["height"]
77                    .as_u64()
78                    .unwrap();
79                println!("Screenshot captured! Image size: {width}x{height}");
80
81                let image: bevy::image::Image = serde_json::from_value(image_data.clone())?;
82                let dyn_img = image
83                    .try_into_dynamic()
84                    .expect("Failed to convert screenshot to dynamic image");
85                let path = "screenshot.png";
86                dyn_img.to_rgb8().save(path)?;
87                println!("Screenshot saved to {path}");
88                break;
89            }
90        }
91    }
92
93    // Step 2: Find the button entity, and its global transform
94    println!("Querying for button entity...");
95    let button_query = brp_request(
96        &url,
97        BRP_QUERY_METHOD,
98        3,
99        &BrpQueryParams {
100            data: BrpQuery {
101                components: vec![type_name::<UiGlobalTransform>().to_string()],
102                option: ComponentSelector::default(),
103                has: Vec::default(),
104            },
105            strict: false,
106            filter: BrpQueryFilter {
107                with: vec![type_name::<Button>().to_string()],
108                without: Vec::default(),
109            },
110        },
111    )?;
112
113    let button_result = button_query["result"]
114        .as_array()
115        .expect("Expected result array");
116    let button = &button_result[0];
117
118    // UiGlobalTransform wraps an Affine2, serialized as a flat array:
119    // [_, _, _, _, translation_x, translation_y]
120    // The translation gives the node's center in physical pixels.
121    let transform = &button["components"][type_name::<UiGlobalTransform>()];
122    let transform_arr = transform.as_array().expect("Expected transform array");
123    let phys_x = transform_arr[4].as_f64().unwrap();
124    let phys_y = transform_arr[5].as_f64().unwrap();
125    println!("Found button at physical ({phys_x}, {phys_y})");
126
127    // Step 3: Find the window entity and scale factor
128    println!("Querying for window entity...");
129    let window_query = brp_request(
130        &url,
131        BRP_QUERY_METHOD,
132        4,
133        &BrpQueryParams {
134            data: BrpQuery {
135                components: vec![type_name::<Window>().to_string()],
136                option: ComponentSelector::default(),
137                has: Vec::default(),
138            },
139            strict: false,
140            filter: BrpQueryFilter::default(),
141        },
142    )?;
143
144    let window_result = window_query["result"]
145        .as_array()
146        .expect("Expected result array");
147    let window = &window_result[0];
148    let window_entity = &window["entity"];
149    let window_data = &window["components"][type_name::<Window>()];
150    let scale_factor = window_data["resolution"]["scale_factor"].as_f64().unwrap();
151    println!("Found window entity: {window_entity}, scale_factor: {scale_factor}");
152
153    // Step 4: Convert button center from physical to logical pixels
154    let logical_x = phys_x / scale_factor;
155    let logical_y = phys_y / scale_factor;
156    println!("Clicking at logical position: ({logical_x}, {logical_y})");
157
158    // Step 5: Send CursorMoved via WindowEvent message
159    // This lets the picking system know where the pointer is.
160    println!("Sending CursorMoved message...");
161    brp_request(
162        &url,
163        BRP_WRITE_MESSAGE_METHOD,
164        5,
165        &BrpWriteMessageParams {
166            message: type_name::<WindowEvent>().to_string(),
167            value: Some(serde_json::json!({
168                "CursorMoved": {
169                    "window": window_entity,
170                    "position": [logical_x, logical_y],
171                    "delta": null
172                }
173            })),
174        },
175    )?;
176
177    // Step 6: Send MouseButtonInput Pressed + Released via WindowEvent messages.
178    // The picking system needs both press and release to generate a Pointer<Click>.
179    println!("Sending mouse press...");
180    brp_request(
181        &url,
182        BRP_WRITE_MESSAGE_METHOD,
183        6,
184        &BrpWriteMessageParams {
185            message: type_name::<WindowEvent>().to_string(),
186            value: Some(serde_json::json!({
187                "MouseButtonInput": {
188                    "button": "Left",
189                    "state": "Pressed",
190                    "window": window_entity,
191                }
192            })),
193        },
194    )?;
195
196    println!("Sending mouse release...");
197    brp_request(
198        &url,
199        BRP_WRITE_MESSAGE_METHOD,
200        7,
201        &BrpWriteMessageParams {
202            message: type_name::<WindowEvent>().to_string(),
203            value: Some(serde_json::json!({
204                "MouseButtonInput": {
205                    "button": "Left",
206                    "state": "Released",
207                    "window": window_entity,
208                }
209            })),
210        },
211    )?;
212
213    Ok(())
214}
215
216fn brp_request(
217    url: &str,
218    method: &str,
219    id: u32,
220    params: &impl serde::Serialize,
221) -> AnyhowResult<serde_json::Value> {
222    let req = BrpRequest {
223        method: method.to_string(),
224        id: Some(serde_json::to_value(id)?),
225        params: Some(serde_json::to_value(params)?),
226    };
227    Ok(ureq::post(url).send_json(req)?.body_mut().read_json()?)
228}