From d732c921c27cc2ad70c45aea6a145096d7515e93 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 1 Jun 2026 19:57:39 +0200 Subject: [PATCH] fix(techdemo): rebootstrap inserted islands Keep opaque island widgets live when slhx swaps or later DOM insertion adds a new island, and tear down the listener when an island leaves the document. req: interop/001 req: examples/001 --- examples/techdemo/templates/island.js | 22 ++++++++++++++++++++-- examples/techdemo/tests/browser_e2e.rs | 24 ++++++++++++++++++++++++ examples/techdemo/tests/e2e.rs | 2 ++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/examples/techdemo/templates/island.js b/examples/techdemo/templates/island.js index f7a2242..72c35da 100644 --- a/examples/techdemo/templates/island.js +++ b/examples/techdemo/templates/island.js @@ -80,12 +80,18 @@ const state = { frame: 0, snapshot: parseSnapshot(island.getAttribute("data-island-snapshot")) }; roots.set(island, state); - rootOf(island).addEventListener("slhx:island-orbit", (event) => { + const root = rootOf(island); + const update = (event) => { state.snapshot = parseSnapshot(event.detail); island.setAttribute("data-island-snapshot", event.detail); - }); + }; + root.addEventListener("slhx:island-orbit", update); function tick() { + if (!document.contains(island)) { + root.removeEventListener("slhx:island-orbit", update); + return; + } state.frame += 1; render(canvas, readout, state); requestAnimationFrame(tick); @@ -99,6 +105,18 @@ }); } + function bootAddedIslands(records) { + for (const record of records) { + for (const node of record.addedNodes) { + if (node.nodeType === 1) { + scan(); + return; + } + } + } + } + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", scan); else scan(); + if (typeof MutationObserver !== "undefined") new MutationObserver(bootAddedIslands).observe(document.documentElement, { childList: true, subtree: true }); })(); diff --git a/examples/techdemo/tests/browser_e2e.rs b/examples/techdemo/tests/browser_e2e.rs index 1dd2b3b..2860cc1 100644 --- a/examples/techdemo/tests/browser_e2e.rs +++ b/examples/techdemo/tests/browser_e2e.rs @@ -52,6 +52,8 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> { 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, "#probe-island [data-island-readout]", "probe live").await?; driver .find(By::Css(&handle_selector(ui::control_center::handles::simulate_push))) @@ -97,6 +99,7 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> { driver.find(By::Css("a[href='/architecture']")).await?.click().await?; wait_for_text(&driver, &slot_selector(ui::control_center::slots::inspector), "Page swap").await?; + wait_for_text(&driver, "[data-island-readout]", "activity rows").await?; assert!(driver.current_url().await?.as_str().ends_with("/architecture")); driver.goto(&format!("http://{APP_ADDR}/")).await?; @@ -133,6 +136,27 @@ fn card_button_selector(handle_id: impl std::fmt::Display, work_id: u64) -> Stri format!(r#"[data-hid="{handle_id}"][data-work-id="{work_id}"]"#) } +async fn insert_probe_island(driver: &WebDriver) -> WebDriverResult<()> { + driver + .execute( + r#" + document.querySelector('[data-slhx-root]').insertAdjacentHTML('beforeend', ` +
+ +

waiting

+
+ `); + setTimeout(() => { + document.querySelector('[data-slhx-root]').dispatchEvent(new CustomEvent('slhx:island-orbit', { bubbles: true, detail: '7|2|8|probe live' })); + }, 25); + return true; + "#, + Vec::new(), + ) + .await?; + Ok(()) +} + async fn drag_card_to_lane(driver: &WebDriver, work_id: u64, lane: &str) -> WebDriverResult<()> { driver .execute( diff --git a/examples/techdemo/tests/e2e.rs b/examples/techdemo/tests/e2e.rs index 5fd4b4c..7c6dc2e 100644 --- a/examples/techdemo/tests/e2e.rs +++ b/examples/techdemo/tests/e2e.rs @@ -71,6 +71,8 @@ fn product_is_e2e_working_over_http() { 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);