use hemx_axum::runtime_js_path; use hemx_test::{inspect_wire, EffectInspector}; use hemx_workout_example::ui::BUILD_FINGERPRINT; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; struct Server { child: Child, addr: String, } impl Server { fn start() -> Self { let listener = TcpListener::bind("127.0.0.1:0").expect("reserve test port"); let addr = listener.local_addr().unwrap().to_string(); drop(listener); let bin = env!("CARGO_BIN_EXE_hemx-workout-example"); let child = Command::new(bin) .env("HEMX_WORKOUT_ADDR", &addr) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("start hemx-workout-example"); let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { if TcpStream::connect(&addr).is_ok() { return Self { child, addr }; } std::thread::sleep(Duration::from_millis(25)); } panic!("hemx-workout-example did not listen on {addr}"); } } impl Drop for Server { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); } } #[test] fn workout_is_e2e_working_over_http() { // req: examples/001 req: local/001 req: local/003 req: local/004 req: host/001 let server = Server::start(); let home = get(&server, "/"); assert_eq!(home.status, 200); assert!(home.header("content-type").contains("text/html")); assert!(home.text().contains("Now-first Workout Copilot")); assert!(home.text().contains("Private event log")); assert!(home.text().contains("Replay export")); assert!(home .text() .contains(" String { format!("__h={}", handle_id_from_html(html, label)) } fn handle_id_from_html(html: &str, label: &str) -> String { let label_at = html.find(label).expect("label in html"); let prefix = &html[..label_at]; let hid_at = prefix.rfind("data-hid=\"").expect("handle before label") + "data-hid=\"".len(); let end = prefix[hid_at..].find('"').expect("handle quote"); prefix[hid_at..hid_at + end].to_owned() } fn assert_effect_response(response: &Response) { assert_eq!(response.status, 200, "response: {response:?}"); assert!(response.header("content-type").contains("application/hemx")); assert_eq!( response.header("x-hemx-fingerprint"), BUILD_FINGERPRINT.0.to_string() ); assert!(!response.effects().is_empty()); } fn assert_payload_contains(batch: &EffectInspector, needle: &str) { assert!( batch.payload_contains(needle), "missing payload {needle:?} in {batch:#?}" ); } fn get(server: &Server, path: &str) -> Response { request(server, "GET", path, "") } fn post(server: &Server, path: &str, body: &str) -> Response { request(server, "POST", path, body) } fn request(server: &Server, method: &str, path: &str, body: &str) -> Response { let mut stream = TcpStream::connect(&server.addr).expect("connect workout example"); let request = format!( "{method} {path} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\n\r\n{body}", server.addr, body.len() ); stream.write_all(request.as_bytes()).unwrap(); let mut raw = Vec::new(); stream.read_to_end(&mut raw).unwrap(); Response::parse(raw) } #[derive(Debug)] struct Response { status: u16, headers: Vec<(String, String)>, body: Vec, } impl Response { fn parse(raw: Vec) -> Self { let split = raw .windows(4) .position(|window| window == b"\r\n\r\n") .expect("http response"); let head = String::from_utf8(raw[..split].to_vec()).expect("utf8 headers"); let body = raw[(split + 4)..].to_vec(); let mut lines = head.lines(); let status = lines .next() .and_then(|line| line.split_whitespace().nth(1)) .and_then(|status| status.parse().ok()) .expect("status code"); let headers = lines .filter_map(|line| line.split_once(':')) .map(|(name, value)| (name.to_ascii_lowercase(), value.trim().to_owned())) .collect(); Self { status, headers, body, } } fn header(&self, name: &str) -> String { let name = name.to_ascii_lowercase(); self.headers .iter() .find_map(|(key, value)| (key == &name).then(|| value.clone())) .unwrap_or_default() } fn text(&self) -> String { String::from_utf8(self.body.clone()).expect("utf8 body") } fn effects(&self) -> EffectInspector { inspect_wire(&self.body) } }