Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/remote/integration_test.rs
30632 views
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
10
use std::{any::type_name, io::BufRead};
11
12
use anyhow::Result as AnyhowResult;
13
use 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
29
fn 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
216
fn 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
}
229
230