Skip to main content

app_under_test/
app_under_test.rs

1//! A Bevy app that can be used as an integration test target.
2//! It displays a button that must be clicked. The button is placed at a random position and
3//! moves every 5 seconds.
4//!
5//! Run with the `bevy_remote` feature enabled:
6//! ```bash
7//! cargo run --example app_under_test --features="bevy_remote"
8//! ```
9//! This example can be paired with the `integration_test` example, which will run an integration
10//! test on this app.
11
12use bevy::{
13    prelude::*,
14    remote::{http::RemoteHttpPlugin, RemotePlugin},
15    time::common_conditions::on_timer,
16    ui::UiGlobalTransform,
17};
18use chacha20::ChaCha8Rng;
19use rand::{RngExt, SeedableRng};
20
21fn main() {
22    App::new()
23        .add_plugins(DefaultPlugins)
24        // To make the app available for integration testing, we add these
25        // remote plugins to expose API’s for a testing framework to call.
26        .add_plugins(RemotePlugin::default())
27        .add_plugins(RemoteHttpPlugin::default())
28        .insert_resource(SeededRng(ChaCha8Rng::seed_from_u64(19878367467712)))
29        .add_systems(Startup, setup)
30        .add_systems(
31            Update,
32            (
33                move_button.run_if(on_timer(std::time::Duration::from_secs(5))),
34                log_button_position,
35            ),
36        )
37        .run();
38}
39
40#[derive(Resource)]
41struct SeededRng(ChaCha8Rng);
42
43fn on_button_click(_click: On<Pointer<Click>>, mut exit: MessageWriter<AppExit>) {
44    info!("Button pressed!");
45    exit.write(AppExit::Success);
46}
47
48fn log_button_position(
49    transform: Single<&UiGlobalTransform, (With<Button>, Changed<UiGlobalTransform>)>,
50) {
51    info!(
52        "Button at physical ({}, {})",
53        transform.translation.x, transform.translation.y
54    );
55}
56
57fn random_position(rng: &mut ChaCha8Rng) -> (f32, f32) {
58    let left_pct = rng.random_range(0.0..=60.0);
59    let top_pct = rng.random_range(0.0..=60.0);
60    (left_pct, top_pct)
61}
62
63fn move_button(mut rng: ResMut<SeededRng>, mut button_query: Query<&mut Node, With<Button>>) {
64    let (left_pct, top_pct) = random_position(&mut rng.0);
65    for mut node in &mut button_query {
66        node.left = percent(left_pct);
67        node.top = percent(top_pct);
68    }
69}
70
71fn setup(mut commands: Commands, assets: Res<AssetServer>, mut rng: ResMut<SeededRng>) {
72    let (left_pct, top_pct) = random_position(&mut rng.0);
73
74    commands.spawn(Camera2d);
75    commands
76        .spawn(Node {
77            width: percent(100),
78            height: percent(100),
79            ..default()
80        })
81        .with_children(|parent| {
82            parent
83                .spawn((
84                    Button,
85                    Node {
86                        width: px(150),
87                        height: px(65),
88                        border: UiRect::all(px(5)),
89                        justify_content: JustifyContent::Center,
90                        align_items: AlignItems::Center,
91                        border_radius: BorderRadius::MAX,
92                        left: percent(left_pct),
93                        top: percent(top_pct),
94                        ..default()
95                    },
96                    BorderColor::all(Color::WHITE),
97                    BackgroundColor(Color::BLACK),
98                ))
99                .observe(on_button_click)
100                .with_children(|parent| {
101                    parent.spawn((
102                        Text::new("Button"),
103                        TextFont {
104                            font: assets.load("fonts/FiraSans-Bold.ttf").into(),
105                            font_size: FontSize::Px(33.0),
106                            ..default()
107                        },
108                        TextColor(Color::srgb(0.9, 0.9, 0.9)),
109                        TextShadow::default(),
110                    ));
111                });
112        });
113}