feat(api): streamline generated app authoring
Move the canonical examples toward generated component-root helpers, typed form decoding, async/state handler registration, and derive-driven app/component registry wiring. Tighten requirements and diagnostics for the server-first, selectorless authoring path. Verified with cargo run -p slhx-xtask -- test, cargo check --workspace, redgate list, redgate refs, redgate health --strict, and git diff --check. req: canonical/001 req: canonical/003 req: canonical/004 req: dx/002 req: derive_app/001 req: component/003 req: form/004 req: axum_integration/003
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
use slhx_techdemo::ui::control_center::{handles, slots};
|
||||
use slhx_techdemo::ui::issue_card::handles as card_handles;
|
||||
use slhx_techdemo::ui::control_center::{self as control, launch_work, simulate_push};
|
||||
use slhx_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::*;
|
||||
@@ -48,63 +52,70 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
|
||||
|
||||
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,
|
||||
"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,
|
||||
"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?;
|
||||
wait_for_text(
|
||||
&driver,
|
||||
&scoped_island_readout_selector("#probe-island"),
|
||||
"probe live",
|
||||
)
|
||||
.await?;
|
||||
|
||||
driver
|
||||
.find(By::Css(&handle_selector(handles::simulate_push)))
|
||||
.find(By::Css(&handle_selector(simulate_push)))
|
||||
.await?
|
||||
.click()
|
||||
.await?;
|
||||
wait_for_text(&driver, &slot_selector(slots::notice), "Push simulated").await?;
|
||||
wait_for_text(&driver, "[data-island-readout]", "activity rows").await?;
|
||||
|
||||
driver.find(By::Css("button.primary-action")).await?.click().await?;
|
||||
wait_for_text(&driver, ".work-card[data-key='4']", "Ship typed effects").await?;
|
||||
assert_text(&driver, "width:77%").await?;
|
||||
|
||||
drag_card_to_lane(&driver, 2, "product").await?;
|
||||
wait_for_text(&driver, ".lane[data-lane='product'] .work-card[data-key='2']", "Shipped").await?;
|
||||
wait_for_text(&driver, &slot_selector(slots::notice), "Drag-and-drop move persisted").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(&card_button_selector(card_handles::spotlight_work, 2)))
|
||||
.find(By::Css(&handle_button_selector(launch_work)))
|
||||
.await?
|
||||
.click()
|
||||
.await?;
|
||||
wait_for_text(
|
||||
&driver,
|
||||
&slot_selector(slots::inspector),
|
||||
"Stream typed presence · lane=Product · stage=Shipped · impact=7",
|
||||
&keyed_selector(".work-card", 4),
|
||||
"Ship typed updates",
|
||||
)
|
||||
.await?;
|
||||
assert_text(&driver, "width:77%").await?;
|
||||
|
||||
driver
|
||||
.find(By::Css(&card_button_selector(card_handles::advance_work, 3)))
|
||||
.find(By::Css(&handle_selector(simulate_push)))
|
||||
.await?
|
||||
.click()
|
||||
.await?;
|
||||
wait_for_text(&driver, ".lane[data-lane='product'] .work-card[data-key='3']", "Active").await?;
|
||||
wait_for_text(&driver, &target_selector(control::live_feed), "SSE tick").await?;
|
||||
|
||||
driver
|
||||
.find(By::Css(&handle_selector(handles::simulate_push)))
|
||||
.find(By::Css(&nav_link_selector("/architecture")))
|
||||
.await?
|
||||
.click()
|
||||
.await?;
|
||||
wait_for_text(&driver, &slot_selector(slots::live_feed), "SSE tick").await?;
|
||||
|
||||
driver.find(By::Css("a[href='/architecture']")).await?.click().await?;
|
||||
wait_for_text(&driver, &slot_selector(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"));
|
||||
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, &slot_selector(slots::live_feed), "SSE tick").await?;
|
||||
wait_for_text(&driver, &target_selector(control::live_feed), "SSE tick").await?;
|
||||
|
||||
Ok::<(), WebDriverError>(())
|
||||
}
|
||||
@@ -125,68 +136,15 @@ fn wait_for_tcp(addr: &str) {
|
||||
panic!("timed out waiting for {addr}");
|
||||
}
|
||||
|
||||
fn handle_selector(handle: impl std::fmt::Display) -> String {
|
||||
format!(r#"[data-hid="{handle}"]"#)
|
||||
}
|
||||
|
||||
fn slot_selector(slot: impl std::fmt::Display) -> String {
|
||||
format!(r#"[data-sid="{slot}"]"#)
|
||||
}
|
||||
|
||||
fn card_button_selector(handle_id: impl std::fmt::Display, work_id: u64) -> String {
|
||||
format!(r#"[data-hid="{handle_id}"][data-work-id="{work_id}"]"#)
|
||||
}
|
||||
|
||||
async fn insert_probe_island(driver: &WebDriver) -> WebDriverResult<()> {
|
||||
let root = driver.find(By::Css("[data-slhx-root]")).await?;
|
||||
driver
|
||||
.execute(
|
||||
r#"
|
||||
const root = arguments[0];
|
||||
const island = document.createElement('article');
|
||||
island.id = 'probe-island';
|
||||
island.setAttribute('data-slhx-island', 'orbit');
|
||||
island.setAttribute('data-island-snapshot', '1|1|1|probe waiting');
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 32;
|
||||
canvas.height = 16;
|
||||
island.appendChild(canvas);
|
||||
|
||||
const readout = document.createElement('p');
|
||||
readout.setAttribute('data-island-readout', '');
|
||||
readout.textContent = 'waiting';
|
||||
island.appendChild(readout);
|
||||
|
||||
root.appendChild(island);
|
||||
setTimeout(() => {
|
||||
root.dispatchEvent(new CustomEvent('slhx:island-orbit', { bubbles: true, detail: '7|2|8|probe live' }));
|
||||
}, 25);
|
||||
return true;
|
||||
"#,
|
||||
vec![root.to_json()?],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn drag_card_to_lane(driver: &WebDriver, work_id: u64, lane: &str) -> WebDriverResult<()> {
|
||||
let card = driver.find(By::Css(format!(".work-card[data-key='{work_id}']"))).await?;
|
||||
let lane = driver.find(By::Css(format!(".lane[data-lane='{lane}']"))).await?;
|
||||
driver
|
||||
.execute(
|
||||
r#"
|
||||
const card = arguments[0];
|
||||
const lane = arguments[1];
|
||||
const data = new DataTransfer();
|
||||
card.dispatchEvent(new DragEvent('dragstart', { bubbles: true, dataTransfer: data }));
|
||||
lane.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: data }));
|
||||
lane.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: data }));
|
||||
return true;
|
||||
"#,
|
||||
vec![card.to_json()?, lane.to_json()?],
|
||||
)
|
||||
.await?;
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -194,7 +152,10 @@ async fn wait_for_runtime(driver: &WebDriver) -> WebDriverResult<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(8);
|
||||
loop {
|
||||
let loaded = driver
|
||||
.execute("return !!window.slhx && window.slhx.roots().length > 0", Vec::new())
|
||||
.execute(
|
||||
"return !!window.slhx && window.slhx.roots().length > 0",
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.as_bool()
|
||||
@@ -210,7 +171,7 @@ async fn wait_for_runtime(driver: &WebDriver) -> WebDriverResult<()> {
|
||||
}
|
||||
|
||||
async fn assert_text(driver: &WebDriver, text: &str) -> WebDriverResult<()> {
|
||||
wait_for_text(driver, "body", text).await
|
||||
wait_for_text(driver, document_body_selector(), text).await
|
||||
}
|
||||
|
||||
async fn wait_for_text(driver: &WebDriver, selector: &str, text: &str) -> WebDriverResult<()> {
|
||||
@@ -225,7 +186,12 @@ async fn wait_for_text(driver: &WebDriver, selector: &str, text: &str) -> WebDri
|
||||
}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
let body = driver.find(By::Css("body")).await?.text().await.unwrap_or_default();
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user