use hemx_techdemo::ui::control_center::{self as control, launch_work, simulate_push}; use hemx_test::{ any_root_selector, document_body_selector, handle_button_selector, handle_selector, island_probe_script, island_readout_selector, keyed_selector, nav_link_selector, scoped_island_readout_selector, target_selector, }; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; use thirtyfour::prelude::*; const APP_ADDR: &str = "127.0.0.1:3012"; const WEBDRIVER_ADDR: &str = "127.0.0.1:4445"; struct ChildProcess { child: Child, } impl ChildProcess { fn spawn(mut command: Command) -> Self { let child = command .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("spawn child process"); Self { child } } } impl Drop for ChildProcess { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); } } #[tokio::test] async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> { // req: examples/001 req: dx/008 req: form/002 req: page_swap/002 req: push/003 let mut app = Command::new(env!("CARGO_BIN_EXE_hemx-techdemo")); app.env("HEMX_TECHDEMO_ADDR", APP_ADDR); let _app = ChildProcess::spawn(app); wait_for_tcp(APP_ADDR); let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg("4445"); let _webdriver = ChildProcess::spawn(webdriver); wait_for_tcp(WEBDRIVER_ADDR); let mut caps = DesiredCapabilities::firefox(); caps.set_headless()?; let driver = WebDriver::new(&format!("http://{WEBDRIVER_ADDR}"), caps).await?; let result = async { driver.goto(&format!("http://{APP_ADDR}/")).await?; assert_text( &driver, "A Linear-class work system without a frontend framework", ) .await?; assert_text(&driver, "Compile checked handles").await?; assert_text( &driver, "No selectors. Generated resources address every target.", ) .await?; assert_text(&driver, "Opaque island bridge").await?; wait_for_runtime(&driver).await?; insert_probe_island(&driver).await?; wait_for_text( &driver, &scoped_island_readout_selector("#probe-island"), "probe live", ) .await?; driver .find(By::Css(handle_selector(simulate_push))) .await? .click() .await?; wait_for_text(&driver, &target_selector(control::notice), "Push simulated").await?; wait_for_text(&driver, island_readout_selector(), "activity rows").await?; driver .find(By::Css(handle_button_selector(launch_work))) .await? .click() .await?; wait_for_text( &driver, &keyed_selector(".work-card", 4), "Ship typed updates", ) .await?; assert_text(&driver, "width:77%").await?; driver .find(By::Css(handle_selector(simulate_push))) .await? .click() .await?; wait_for_text(&driver, &target_selector(control::live_feed), "SSE tick").await?; driver .find(By::Css(nav_link_selector("/architecture"))) .await? .click() .await?; wait_for_text(&driver, &target_selector(control::inspector), "Page swap").await?; wait_for_text(&driver, island_readout_selector(), "activity rows").await?; assert!(driver .current_url() .await? .as_str() .ends_with("/architecture")); driver.goto(&format!("http://{APP_ADDR}/")).await?; wait_for_text(&driver, &target_selector(control::live_feed), "SSE tick").await?; Ok::<(), WebDriverError>(()) } .await; let quit = driver.quit().await; result.and(quit) } fn wait_for_tcp(addr: &str) { let deadline = Instant::now() + Duration::from_secs(8); while Instant::now() < deadline { if std::net::TcpStream::connect(addr).is_ok() { return; } std::thread::sleep(Duration::from_millis(25)); } panic!("timed out waiting for {addr}"); } async fn insert_probe_island(driver: &WebDriver) -> WebDriverResult<()> { let root = driver.find(By::Css(any_root_selector())).await?; let script = island_probe_script( "probe-island", "orbit", "1|1|1|probe waiting", "7|2|8|probe live", ); driver.execute(&script, vec![root.to_json()?]).await?; Ok(()) } async fn wait_for_runtime(driver: &WebDriver) -> WebDriverResult<()> { let deadline = Instant::now() + Duration::from_secs(8); loop { let loaded = driver .execute( "return !!window.hemx && window.hemx.roots().length > 0", Vec::new(), ) .await? .json() .as_bool() .unwrap_or(false); if loaded { return Ok(()); } if Instant::now() >= deadline { panic!("timed out waiting for hemx runtime"); } tokio::time::sleep(Duration::from_millis(50)).await; } } async fn assert_text(driver: &WebDriver, text: &str) -> WebDriverResult<()> { wait_for_text(driver, document_body_selector(), text).await } async fn wait_for_text(driver: &WebDriver, selector: &str, text: &str) -> WebDriverResult<()> { let deadline = Instant::now() + Duration::from_secs(8); let by = By::Css(selector); loop { if let Ok(element) = driver.find(by.clone()).await { let content = element.text().await.unwrap_or_default(); let html = element.inner_html().await.unwrap_or_default(); if content.contains(text) || html.contains(text) { return Ok(()); } } if Instant::now() >= deadline { let body = driver .find(By::Css(document_body_selector())) .await? .text() .await .unwrap_or_default(); panic!("timed out waiting for {text:?} in {selector:?}; body={body:?}"); } tokio::time::sleep(Duration::from_millis(50)).await; } }