use std::time::Duration;
use bevy_react::js_thread::spawn_js_thread;
use bevy_react::protocol::{Op, Outbound, UiEvent};
use bevy_react::{RawRequest, ReactMessage};
const GOOD_APP: &[u8] = br#"
(function () {
if (globalThis.__started) {
Deno.core.ops.op_emit("phase", "update:" + globalThis.__n);
globalThis.__loop();
return;
}
globalThis.__started = true;
globalThis.__n = 41;
globalThis.__loop = () => (async () => {
for (;;) {
const m = await Deno.core.ops.op_next_event();
if (m == null) return; // shutdown
if (m.t === "reload") return; // yield to Rust so it can re-exec the app
if (m.t === "uiEvent") {
globalThis.__n++;
Deno.core.ops.op_emit("phase", "click:" + globalThis.__n);
}
}
})();
Deno.core.ops.op_emit("phase", "init:" + globalThis.__n);
globalThis.__loop();
})();
"#;
const BROKEN_APP: &[u8] = b"aa16;\n";
fn click(tx: &tokio::sync::mpsc::UnboundedSender<Outbound>) {
tx.send(Outbound::UiEvent {
event: UiEvent {
id: 1,
kind: "click".into(),
..Default::default()
},
})
.expect("JS thread gone");
}
#[test]
fn broken_reload_is_rejected_and_recovers() {
let dir = std::env::temp_dir().join("bevy_react_broken_reload_test");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("vendor.js"), b"// no-op vendor\n").expect("write vendor");
let app = dir.join("app.js");
std::fs::write(&app, GOOD_APP).expect("write app");
let (ops_tx, _ops_rx) = crossbeam_channel::unbounded::<Vec<Op>>();
let (emit_tx, emit_rx) = crossbeam_channel::unbounded::<ReactMessage>();
let (request_tx, _request_rx) = crossbeam_channel::unbounded::<RawRequest>();
let (anim_tx, _anim_rx) = crossbeam_channel::unbounded();
let (outbound_tx, outbound_rx) = tokio::sync::mpsc::unbounded_channel::<Outbound>();
let (reload_tx, reload_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
spawn_js_thread(
dir.join("vendor.js"),
app.clone(),
ops_tx,
emit_tx,
request_tx,
anim_tx,
outbound_rx,
reload_rx,
);
let recv_prefix = |prefix: &str| -> u32 {
loop {
let phase = emit_rx
.recv_timeout(Duration::from_secs(10))
.expect("no emit — UI wedged?")
.value
.as_str()
.unwrap()
.to_string();
if let Some(n) = phase.strip_prefix(prefix) {
return n.parse().expect("counter");
}
}
};
assert_eq!(recv_prefix("init:"), 41);
click(&outbound_tx);
assert_eq!(recv_prefix("click:"), 42);
std::fs::write(&app, BROKEN_APP).expect("write broken app");
reload_tx.send(()).expect("send reload");
assert_eq!(
recv_prefix("update:"),
42,
"a broken reload tore down the working app instead of being rejected"
);
click(&outbound_tx);
assert!(
recv_prefix("click:") > 42,
"event loop did not survive a rejected (broken) hot reload"
);
std::fs::write(&app, GOOD_APP).expect("rewrite good app");
reload_tx.send(()).expect("send reload");
recv_prefix("update:"); click(&outbound_tx);
assert!(
recv_prefix("click:") > 42,
"event loop broken after recovery"
);
eprintln!("OK broken reload rejected; working UI preserved and recovered on next good edit");
}