use scraper::{Html, Selector}; use hemx_techdemo::ui::control_center::{self as control, launch_work, reset_demo, simulate_push}; use hemx_techdemo::ui::issue_card::{advance_work, delete_work, spotlight_work}; use hemx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle; use hemx_techdemo::ui::BUILD_FINGERPRINT; use hemx_test::{ class_descendant_selector, class_selector, handle_form_body, inspect_wire, island_attribute_name, island_event_name, island_selector, island_snapshot_marker, root_selector, sse_endpoint_marker, strong_text_selector, unknown_handle_form_body, EffectInspector, }; use std::io::{Read, Write}; use std::net::TcpStream; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; const ADDR: &str = "127.0.0.1:3002"; struct Server { child: Child, } impl Server { fn start() -> Self { let bin = env!("CARGO_BIN_EXE_hemx-techdemo"); let child = Command::new(bin) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("start hemx-techdemo"); let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { if TcpStream::connect(ADDR).is_ok() { return Self { child }; } std::thread::sleep(Duration::from_millis(25)); } panic!("hemx-techdemo did not listen on {ADDR}"); } } impl Drop for Server { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); } } #[test] fn product_is_e2e_working_over_http() { // req: examples/001 req: dx/008 req: form/002 req: page_swap/002 req: push/003 let _server = Server::start(); let home = get("/"); assert_eq!(home.status, 200); assert!(home.header("content-type").contains("text/html")); let document = Html::parse_document(home.text()); assert_text( &document, "A Linear-class work system without a frontend framework", ); assert_text(&document, "Compile checked handles"); assert_text(&document, "Stream typed presence"); assert_selector_count_at_least(&document, &root_selector("techdemo"), 1); assert_work_card_count_at_least(&document, 3); assert_selector_count_at_least(&document, &island_selector("orbit"), 1); assert_text(&document, "Opaque island bridge"); assert!(home.text().contains(island_snapshot_marker())); assert!(home.text().contains(&sse_endpoint_marker("/events"))); assert!(home.text().contains("/island.js")); let favicon = get("/favicon.ico"); assert_eq!(favicon.status, 204); let runtime = get("/hemx.js"); assert_eq!(runtime.status, 200); assert!(runtime.header("content-type").contains("javascript")); let island = get("/island.js"); assert_eq!(island.status, 200); assert!(island.header("content-type").contains("javascript")); assert!(island.text().contains(&island_event_name("orbit"))); assert!(island.text().contains(island_attribute_name())); assert!(island.text().contains("MutationObserver")); assert!(island.text().contains("removeEventListener")); let architecture = request("GET", "/architecture", &[("X-HEMX-Partial", "1")], ""); assert_eq!(architecture.status, 200); assert!(architecture.header("x-hemx-partial").contains("true")); let architecture_doc = Html::parse_document(architecture.text()); assert_text(&architecture_doc, "Page swap"); assert_text(&architecture_doc, "hemx-build"); let launch = post( "/", &handle_form_body( launch_work, &[ ("title", "Design hero moment"), ("lane", "product"), ("impact", "9"), ], ), ); assert_effect_response(&launch); let launch_batch = launch.effects(); assert_payload_contains(&launch_batch, "Design hero moment"); assert_card( &launch_batch, "Design hero moment", "Product", "Draft", "width:99%", ); assert_payload_contains(&launch_batch, "Launch accepted"); assert_payload_contains(&launch_batch, "Launched card #4"); assert_emit(&launch_batch, &island_event_name("orbit"), "activity rows"); assert!( launch_batch.op_count() >= 6, "launch should update generated targets and notify the island" ); let default_impact = post( "/", &handle_form_body( launch_work, &[("title", "Default impact"), ("lane", "compiler")], ), ); assert_effect_response(&default_impact); let default_impact_batch = default_impact.effects(); assert_payload_contains(&default_impact_batch, "Default impact"); assert_card( &default_impact_batch, "Default impact", "Compiler", "Draft", "width:55%", ); let low_impact = post( "/", &handle_form_body( launch_work, &[ ("title", "Low impact"), ("lane", "runtime"), ("impact", "0"), ], ), ); assert_effect_response(&low_impact); let low_impact_batch = low_impact.effects(); assert_card( &low_impact_batch, "Low impact", "Runtime", "Draft", "width:11%", ); let high_impact = post( "/", &handle_form_body( launch_work, &[ ("title", "High impact"), ("lane", "runtime"), ("impact", "99"), ], ), ); assert_effect_response(&high_impact); let high_impact_batch = high_impact.effects(); assert_card( &high_impact_batch, "High impact", "Runtime", "Draft", "width:99%", ); let missing_title = post( "/", &handle_form_body(launch_work, &[("lane", "runtime"), ("impact", "8")]), ); assert_effect_response(&missing_title); let missing_title_batch = missing_title.effects(); assert_payload_contains(&missing_title_batch, "Launch accepted"); assert!(missing_title_batch.payload_excludes_key(8)); assert_payload_not_contains(&missing_title_batch, "MUTATED"); let move_to_lane = post( "/", &handle_form_body( move_to_lane_handle, &[("work_id", "4"), ("lane", "runtime")], ), ); assert_effect_response(&move_to_lane); let move_to_lane_batch = move_to_lane.effects(); assert_card( &move_to_lane_batch, "Design hero moment", "Runtime", "Active", "width:99%", ); assert_payload_contains(&move_to_lane_batch, "Drag-and-drop move persisted"); let inspect = post("/", &handle_form_body(spotlight_work, &[("work_id", "4")])); assert_effect_response(&inspect); let inspect_batch = inspect.effects(); assert_payload_contains(&inspect_batch, "Design hero moment · lane=Runtime"); assert_payload_contains(&inspect_batch, "Inspector focused"); let advance = post("/", &handle_form_body(advance_work, &[("work_id", "4")])); assert_effect_response(&advance); let advance_batch = advance.effects(); assert_payload_contains(&advance_batch, "Pipeline advanced"); assert_payload_contains(&advance_batch, "Active"); let advance_default = post("/", &handle_form_body(advance_work, &[("work_id", "5")])); assert_effect_response(&advance_default); let advance_default_batch = advance_default.effects(); assert_payload_contains(&advance_default_batch, "Default impact"); assert_card( &advance_default_batch, "Default impact", "Compiler", "Active", "width:55%", ); let ship_default = post("/", &handle_form_body(advance_work, &[("work_id", "5")])); assert_effect_response(&ship_default); let ship_default_batch = ship_default.effects(); assert_card( &ship_default_batch, "Default impact", "Product", "Shipped", "width:55%", ); let simulated_push = post("/", &handle_form_body(simulate_push, &[])); assert_effect_response(&simulated_push); let push_batch = simulated_push.effects(); assert_payload_contains(&push_batch, "SSE tick"); assert_payload_contains(&push_batch, "Push simulated · no client app code"); assert_payload_contains( &push_batch, "Simulated push event produced the same generated update shape", ); assert_emit(&push_batch, &island_event_name("orbit"), "activity rows"); let delete_missing = post("/", &handle_form_body(delete_work, &[("work_id", "999")])); assert_effect_response(&delete_missing); let delete_missing_batch = delete_missing.effects(); assert_payload_not_contains(&delete_missing_batch, "Deleted card #999"); let delete = post("/", &handle_form_body(delete_work, &[("work_id", "4")])); assert_effect_response(&delete); let delete_batch = delete.effects(); assert_payload_contains(&delete_batch, "Card removed"); assert_payload_contains(&delete_batch, "Deleted card #4"); assert!(delete_batch.payload_excludes_key(4)); assert_payload_contains(&delete_batch, "Default impact"); let reset = post("/", &handle_form_body(reset_demo, &[])); assert_effect_response(&reset); let reset_batch = reset.effects(); assert_payload_contains(&reset_batch, "Demo reset from Rust state"); assert_payload_contains(&reset_batch, "Compile checked handles"); let sse = get("/events?once=1"); assert_eq!(sse.status, 200); assert!(sse.header("content-type").contains("text/event-stream")); assert!(sse.text().contains("event: hemx")); assert!(sse.text().contains("data: ")); let unknown = post("/", &unknown_handle_form_body(999999)); assert_eq!(unknown.status, 404); assert!(unknown.text().contains("unknown hemx handle id 999999")); } fn assert_effect_response(response: &Response) { assert_eq!(response.status, 200); 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 assert_emit(batch: &EffectInspector, name: &str, needle: &str) { assert!( batch.emits_containing(name, needle), "missing emit {name:?} containing {needle:?} in {batch:#?}" ); } fn assert_card(batch: &EffectInspector, title: &str, lane: &str, stage: &str, impact_style: &str) { let board = batch .target_html_containing(control::board, "class=\"lanes\"") .expect("board html payload"); let document = Html::parse_fragment(&board); let lane_selector = Selector::parse(&class_selector("lane")).unwrap(); let card_selector = Selector::parse(&class_selector("work-card")).unwrap(); let strong_selector = Selector::parse(strong_text_selector()).unwrap(); let stage_selector = Selector::parse(&class_selector("pill")).unwrap(); let impact_selector = Selector::parse(&class_descendant_selector("impact", "i")).unwrap(); for lane_node in document.select(&lane_selector) { let lane_text = lane_node.text().collect::>().join(" "); if !lane_text.contains(lane) { continue; } for card in lane_node.select(&card_selector) { let card_title = card .select(&strong_selector) .next() .map(|node| node.text().collect::()) .unwrap_or_default(); if card_title != title { continue; } let card_stage = card .select(&stage_selector) .next() .map(|node| node.text().collect::()) .unwrap_or_default(); let style = card .select(&impact_selector) .next() .and_then(|node| node.value().attr("style")) .unwrap_or(""); assert_eq!(card_stage, stage); assert!( style.contains(impact_style), "style {style:?} missing {impact_style:?}" ); return; } } panic!("missing card title={title:?} lane={lane:?} in {board}"); } fn assert_payload_not_contains(batch: &EffectInspector, needle: &str) { assert!( batch.payload_excludes(needle), "unexpected payload {needle:?} in {batch:#?}" ); } fn assert_text(document: &Html, text: &str) { let body = document.root_element().text().collect::>().join(" "); assert!(body.contains(text), "missing text {text:?} in {body:?}"); } fn assert_work_card_count_at_least(document: &Html, expected: usize) { assert_selector_count_at_least(document, &class_selector("work-card"), expected); } fn assert_selector_count_at_least(document: &Html, selector: &str, expected: usize) { let selector = Selector::parse(selector).unwrap(); let count = document.select(&selector).count(); assert!(count >= expected, "selector count {count} < {expected}"); } fn get(path: &str) -> Response { request("GET", path, &[], "") } fn post(path: &str, body: &str) -> Response { request( "POST", path, &[("Content-Type", "application/x-www-form-urlencoded")], body, ) } fn request(method: &str, path: &str, headers: &[(&str, &str)], body: &str) -> Response { let mut stream = TcpStream::connect(ADDR).expect("connect to server"); stream .set_read_timeout(Some(Duration::from_secs(5))) .unwrap(); write!( stream, "{method} {path} HTTP/1.1\r\nHost: {ADDR}\r\nConnection: close\r\nContent-Length: {}\r\n", body.len() ) .unwrap(); for (name, value) in headers { write!(stream, "{name}: {value}\r\n").unwrap(); } write!(stream, "\r\n{body}").unwrap(); let mut raw = Vec::new(); stream.read_to_end(&mut raw).unwrap(); Response::parse(raw) } 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("response header terminator"); let head = String::from_utf8(raw[..split].to_vec()).unwrap(); 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.trim().to_ascii_lowercase(), value.trim().to_string())) .collect(); Self { status, headers, body, } } fn header(&self, name: &str) -> &str { self.headers .iter() .find(|(key, _)| key == &name.to_ascii_lowercase()) .map(|(_, value)| value.as_str()) .unwrap_or("") } fn text(&self) -> &str { std::str::from_utf8(&self.body).unwrap() } fn effects(&self) -> EffectInspector { inspect_wire(&self.body) } }