use scraper::{Html, Selector}; use slhx::{Effect, EffectBatch, Payload, EFFECT_BATCH_ABI_VERSION}; use slhx_techdemo::ui::BUILD_FINGERPRINT; use slhx_techdemo::ui::control_center::handles; use slhx_techdemo::ui::issue_card::handles as card_handles; use slhx_techdemo::ui::issue_lane::handles as lane_handles; 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_slhx-techdemo"); let child = Command::new(bin) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("start slhx-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!("slhx-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, "[data-slhx-root=techdemo]", 1); assert_selector_count_at_least(&document, "[data-hid]", 8); assert_selector_count_at_least(&document, "[data-sid]", 7); assert_selector_count_at_least(&document, ".work-card", 3); assert_selector_count_at_least(&document, "[data-slhx-island=orbit]", 1); assert_text(&document, "Opaque island bridge"); assert!(home.text().contains("data-island-snapshot=")); assert!(home.text().contains("data-slhx-sse=\"/events\"")); assert!(home.text().contains("/island.js")); let runtime = get("/slhx.js"); assert_eq!(runtime.status, 200); assert!(runtime.header("content-type").contains("javascript")); assert!(runtime.text().contains("const HID = \"data-hid\"")); let island = get("/island.js"); assert_eq!(island.status, 200); assert!(island.header("content-type").contains("javascript")); assert!(island.text().contains("slhx:island-orbit")); assert!(island.text().contains("data-slhx-island")); assert!(island.text().contains("MutationObserver")); assert!(island.text().contains("removeEventListener")); let architecture = request("GET", "/architecture", &[("X-SLHX-Partial", "1")], ""); assert_eq!(architecture.status, 200); assert!(architecture.header("x-slhx-partial").contains("true")); let architecture_doc = Html::parse_document(architecture.text()); assert_text(&architecture_doc, "Page swap"); assert_text(&architecture_doc, "slhx-build"); let launch = post( "/", &format!( "__h={}&title=Design+hero+moment&lane=product&impact=9", handles::launch_work ), ); assert_effect_response(&launch); let launch_batch = launch.batch(); 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, "slhx:island-orbit", "activity rows"); assert!( launch_batch.ops.len() >= 6, "launch should update generated targets and notify the island" ); let default_impact = post( "/", &format!( "__h={}&title=Default+impact&lane=compiler", handles::launch_work ), ); assert_effect_response(&default_impact); let default_impact_batch = default_impact.batch(); assert_payload_contains(&default_impact_batch, "Default impact"); assert_card(&default_impact_batch, "Default impact", "Compiler", "Draft", "width:55%"); let low_impact = post( "/", &format!( "__h={}&title=Low+impact&lane=runtime&impact=0", handles::launch_work ), ); assert_effect_response(&low_impact); let low_impact_batch = low_impact.batch(); assert_card(&low_impact_batch, "Low impact", "Runtime", "Draft", "width:11%"); let high_impact = post( "/", &format!( "__h={}&title=High+impact&lane=runtime&impact=99", handles::launch_work ), ); assert_effect_response(&high_impact); let high_impact_batch = high_impact.batch(); assert_card(&high_impact_batch, "High impact", "Runtime", "Draft", "width:99%"); let missing_title = post( "/", &format!("__h={}&lane=runtime&impact=8", handles::launch_work), ); assert_effect_response(&missing_title); let missing_title_batch = missing_title.batch(); assert_payload_contains(&missing_title_batch, "Launch accepted"); assert_payload_not_contains(&missing_title_batch, "data-key=\"8\""); assert_payload_not_contains(&missing_title_batch, "MUTATED"); let move_to_lane = post( "/", &format!("__h={}&work_id=4&lane=runtime", lane_handles::move_to_lane), ); assert_effect_response(&move_to_lane); let move_to_lane_batch = move_to_lane.batch(); 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("/", &format!("__h={}&work_id=4", card_handles::spotlight_work)); assert_effect_response(&inspect); let inspect_batch = inspect.batch(); assert_payload_contains(&inspect_batch, "Design hero moment · lane=Runtime"); assert_payload_contains(&inspect_batch, "Inspector focused"); let advance = post("/", &format!("__h={}&work_id=4", card_handles::advance_work)); assert_effect_response(&advance); let advance_batch = advance.batch(); assert_payload_contains(&advance_batch, "Pipeline advanced"); assert_payload_contains(&advance_batch, "Active"); let advance_default = post("/", &format!("__h={}&work_id=5", card_handles::advance_work)); assert_effect_response(&advance_default); let advance_default_batch = advance_default.batch(); assert_payload_contains(&advance_default_batch, "Default impact"); assert_card(&advance_default_batch, "Default impact", "Compiler", "Active", "width:55%"); let ship_default = post("/", &format!("__h={}&work_id=5", card_handles::advance_work)); assert_effect_response(&ship_default); let ship_default_batch = ship_default.batch(); assert_card(&ship_default_batch, "Default impact", "Product", "Shipped", "width:55%"); let simulated_push = post("/", &format!("__h={}", handles::simulate_push)); assert_effect_response(&simulated_push); let push_batch = simulated_push.batch(); 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 EffectBatch shape"); assert_emit(&push_batch, "slhx:island-orbit", "activity rows"); let delete_missing = post("/", &format!("__h={}&work_id=999", card_handles::delete_work)); assert_effect_response(&delete_missing); let delete_missing_batch = delete_missing.batch(); assert_payload_not_contains(&delete_missing_batch, "Deleted card #999"); let delete = post("/", &format!("__h={}&work_id=4", card_handles::delete_work)); assert_effect_response(&delete); let delete_batch = delete.batch(); assert_payload_contains(&delete_batch, "Card removed"); assert_payload_contains(&delete_batch, "Deleted card #4"); assert_payload_not_contains(&delete_batch, "data-key=\"4\""); assert_payload_contains(&delete_batch, "Default impact"); let reset = post("/", &format!("__h={}", handles::reset_demo)); assert_effect_response(&reset); let reset_batch = reset.batch(); 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: slhx")); assert!(sse.text().contains("data: ")); let unknown = post("/", "__h=999999"); assert_eq!(unknown.status, 404); assert!(unknown.text().contains("unknown slhx handle id 999999")); } fn assert_effect_response(response: &Response) { assert_eq!(response.status, 200); assert!(response.header("content-type").contains("application/slhx")); assert_eq!(response.header("x-slhx-fingerprint"), BUILD_FINGERPRINT.0.to_string()); let batch = response.batch(); assert_eq!(batch.abi_version, EFFECT_BATCH_ABI_VERSION); assert!(!batch.ops.is_empty()); } fn assert_payload_contains(batch: &EffectBatch, needle: &str) { assert!( batch.ops.iter().any(|op| match op { Effect::Put { payload, .. } | Effect::Insert { payload, .. } | Effect::Prepend { payload, .. } => payload_value(payload).contains(needle), Effect::Emit { payload, .. } => payload.contains(needle), Effect::Navigate { url, .. } => url.contains(needle), Effect::Remove { .. } | Effect::Move { .. } | Effect::Focus { .. } => false, }), "missing payload {needle:?} in {batch:#?}" ); } fn assert_emit(batch: &EffectBatch, name: &str, needle: &str) { assert!( batch.ops.iter().any(|op| match op { Effect::Emit { name: actual, payload } => actual == name && payload.contains(needle), _ => false, }), "missing emit {name:?} containing {needle:?} in {batch:#?}" ); } fn assert_card(batch: &EffectBatch, title: &str, lane: &str, stage: &str, impact_style: &str) { let board = board_html(batch); let document = Html::parse_fragment(&board); let lane_selector = Selector::parse(".lane").unwrap(); let card_selector = Selector::parse(".work-card").unwrap(); let strong_selector = Selector::parse("strong").unwrap(); let stage_selector = Selector::parse(".pill").unwrap(); let impact_selector = Selector::parse(".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 board_html(batch: &EffectBatch) -> String { batch .ops .iter() .find_map(|op| match op { Effect::Put { payload: Payload::Html(value), .. } if value.contains("class=\"lanes\"") => Some(value.clone()), _ => None, }) .expect("board html payload") } fn assert_payload_not_contains(batch: &EffectBatch, needle: &str) { assert!( batch.ops.iter().all(|op| match op { Effect::Put { payload, .. } | Effect::Insert { payload, .. } | Effect::Prepend { payload, .. } => !payload_value(payload).contains(needle), Effect::Emit { payload, .. } => !payload.contains(needle), Effect::Navigate { url, .. } => !url.contains(needle), Effect::Remove { .. } | Effect::Move { .. } | Effect::Focus { .. } => true, }), "unexpected payload {needle:?} in {batch:#?}" ); } fn payload_value(payload: &Payload) -> &str { match payload { Payload::Text(value) | Payload::Html(value) => value, } } 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_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 batch(&self) -> EffectBatch { EffectBatch::from_wire(&self.body).unwrap() } }