feat(techdemo): render issues with hemplate partials
req: examples/001 req: dx/008 req: list/003 req: form/002
This commit is contained in:
Generated
+19
@@ -389,6 +389,14 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "hemplate"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hemplate-core",
|
||||
"hemplate-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hemplate-core"
|
||||
version = "0.1.0"
|
||||
@@ -399,6 +407,16 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hemplate-derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hemplate-core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hemplate-parser"
|
||||
version = "0.1.0"
|
||||
@@ -1404,6 +1422,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures-util",
|
||||
"hemplate",
|
||||
"scraper",
|
||||
"slhx",
|
||||
"slhx-axum",
|
||||
|
||||
@@ -10,6 +10,7 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
axum = "0.7"
|
||||
futures-util = "0.3"
|
||||
hemplate = { path = "../../../hemplate/hemplate" }
|
||||
slhx = { path = "../../slhx" }
|
||||
slhx-axum = { path = "../../slhx-axum" }
|
||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
||||
|
||||
@@ -10,13 +10,14 @@ This is a polished Linear-style product demo for planning typed work across lane
|
||||
|
||||
- modern SSR-first UI
|
||||
- generated resource modules from `.heml`
|
||||
- hemplate partials for issue lanes, cards, and inspector panels
|
||||
- native form posts carrying `__h` numeric handle ids
|
||||
- multi-target `application/slhx` EffectBatch responses
|
||||
- generated slot updates instead of selectors
|
||||
- root-scoped runtime lowering (`data-hid`, `data-sid`)
|
||||
- page-enhancer navigation with native link fallback
|
||||
- SSE server push into a generated slot
|
||||
- one tiny launch glue function plus slhx runtime-driven interactions
|
||||
- drag-and-drop lane moves persisted by typed server handlers
|
||||
|
||||
Verification:
|
||||
|
||||
|
||||
+171
-38
@@ -3,6 +3,7 @@ use axum::response::IntoResponse;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use futures_util::{stream, StreamExt};
|
||||
use hemplate::Hemplate;
|
||||
use slhx::{IntoEffect, SafeHtml};
|
||||
use slhx_axum::{runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm, PageRequest};
|
||||
use slhx_techdemo::ui;
|
||||
@@ -58,6 +59,7 @@ struct DemoState {
|
||||
work: Vec<WorkItem>,
|
||||
activity: VecDeque<String>,
|
||||
spotlight: String,
|
||||
selected_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for DemoState {
|
||||
@@ -71,6 +73,7 @@ impl Default for DemoState {
|
||||
],
|
||||
activity: VecDeque::new(),
|
||||
spotlight: "No selectors. Generated resources address every target.".into(),
|
||||
selected_id: Some(2),
|
||||
};
|
||||
state.log("Demo booted from server-rendered HTML");
|
||||
state.log("Runtime attached one delegated listener per root");
|
||||
@@ -91,6 +94,50 @@ struct Shared {
|
||||
demo: Mutex<DemoState>,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct IssueLane {
|
||||
class: String,
|
||||
lane_id: &'static str,
|
||||
move_handle: u32,
|
||||
title: &'static str,
|
||||
description: &'static str,
|
||||
cards: Vec<IssueCard>,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct IssueCard {
|
||||
class: String,
|
||||
id: u64,
|
||||
title: String,
|
||||
stage: &'static str,
|
||||
impact_style: String,
|
||||
spotlight_handle: u32,
|
||||
advance_handle: u32,
|
||||
delete_handle: u32,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct InspectorPanel {
|
||||
selected: String,
|
||||
spotlight: String,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct InspectorSelected {
|
||||
title: String,
|
||||
lane: &'static str,
|
||||
stage: &'static str,
|
||||
impact: u8,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct InspectorEmpty;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let state = Arc::new(Shared { demo: Mutex::new(DemoState::default()) });
|
||||
@@ -173,6 +220,7 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
||||
let id = demo.next_id;
|
||||
demo.next_id += 1;
|
||||
demo.work.push(WorkItem { id, title: title.into(), lane, impact, stage: Stage::Draft });
|
||||
demo.selected_id = Some(id);
|
||||
demo.spotlight = format!("Form data became typed Rust state; card #{id} was rendered by a generated slot.");
|
||||
demo.log(format!("Launched card #{id}: {title}"));
|
||||
}
|
||||
@@ -197,6 +245,28 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
||||
demo_effects(&demo, "Pipeline advanced")
|
||||
}
|
||||
})
|
||||
.register(ui::control_center::handles::move_to_lane.id().id, {
|
||||
let shared = shared.clone();
|
||||
move |form| {
|
||||
// req: list/003 req: examples/001
|
||||
let mut demo = shared.demo.lock().unwrap();
|
||||
let lane = parse_lane(form.value("lane"));
|
||||
let title = update_work(&mut demo, form.value("work_id"), |item| {
|
||||
item.lane = lane;
|
||||
item.stage = match lane {
|
||||
0 => Stage::Draft,
|
||||
1 => Stage::Active,
|
||||
_ => Stage::Shipped,
|
||||
};
|
||||
});
|
||||
if let Some(title) = title {
|
||||
demo.selected_id = form.value("work_id").and_then(|value| value.parse().ok());
|
||||
demo.spotlight = format!("{title} moved to {} by drag-and-drop; Rust re-rendered the board slot.", LANES[lane].1);
|
||||
demo.log(format!("Dragged {title} to {}", LANES[lane].1));
|
||||
}
|
||||
demo_effects(&demo, "Drag-and-drop move persisted")
|
||||
}
|
||||
})
|
||||
.register(ui::control_center::handles::delete_work.id().id, {
|
||||
let shared = shared.clone();
|
||||
move |form| {
|
||||
@@ -206,6 +276,9 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
||||
let before = demo.work.len();
|
||||
demo.work.retain(|item| item.id != id);
|
||||
if demo.work.len() < before {
|
||||
if demo.selected_id == Some(id) {
|
||||
demo.selected_id = demo.work.first().map(|item| item.id);
|
||||
}
|
||||
demo.spotlight = format!("Card #{id} removed; the board, metrics, activity, and inspector updated together.");
|
||||
demo.log(format!("Deleted card #{id}"));
|
||||
}
|
||||
@@ -219,7 +292,8 @@ fn registry(shared: Arc<Shared>) -> HandlerRegistry {
|
||||
// req: examples/001
|
||||
let mut demo = shared.demo.lock().unwrap();
|
||||
if let Some(id) = form.value("work_id").and_then(|value| value.parse::<u64>().ok()) {
|
||||
if let Some(item) = demo.work.iter().find(|item| item.id == id) {
|
||||
if let Some(item) = demo.work.iter().find(|item| item.id == id).cloned() {
|
||||
demo.selected_id = Some(id);
|
||||
demo.spotlight = format!("{} · lane={} · stage={} · impact={}", item.title, LANES[item.lane].1, item.stage.label(), item.impact);
|
||||
demo.log(format!("Inspected card #{id}"));
|
||||
}
|
||||
@@ -293,11 +367,7 @@ fn shell(body: String) -> String {
|
||||
<title>slhx Techdemo</title>
|
||||
<script src="/slhx.js" defer></script>
|
||||
<script>
|
||||
function slhxTechdemoLaunch(button) {{
|
||||
const form = button.closest('form');
|
||||
const root = button.closest('[data-slhx-root]');
|
||||
const data = new URLSearchParams(new FormData(form));
|
||||
data.set('__h', button.getAttribute('data-hid'));
|
||||
function slhxTechdemoEffect(root, data) {{
|
||||
fetch('/', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Accept': 'application/slhx' }},
|
||||
@@ -305,6 +375,36 @@ fn shell(body: String) -> String {
|
||||
}})
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((buffer) => window.slhx.applyBatch(buffer, root));
|
||||
}}
|
||||
function slhxTechdemoLaunch(button) {{
|
||||
const form = button.closest('form');
|
||||
const data = new URLSearchParams(new FormData(form));
|
||||
data.set('__h', button.getAttribute('data-hid'));
|
||||
slhxTechdemoEffect(button.closest('[data-slhx-root]'), data);
|
||||
return false;
|
||||
}}
|
||||
function slhxTechdemoDrag(event) {{
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', event.currentTarget.getAttribute('data-key'));
|
||||
}}
|
||||
function slhxTechdemoDragOver(event) {{
|
||||
event.preventDefault();
|
||||
event.currentTarget.classList.add('drop-ready');
|
||||
}}
|
||||
function slhxTechdemoDragLeave(event) {{
|
||||
event.currentTarget.classList.remove('drop-ready');
|
||||
}}
|
||||
function slhxTechdemoDrop(event) {{
|
||||
event.preventDefault();
|
||||
const lane = event.currentTarget;
|
||||
lane.classList.remove('drop-ready');
|
||||
const id = event.dataTransfer.getData('text/plain');
|
||||
if (!id) return false;
|
||||
const data = new URLSearchParams();
|
||||
data.set('__h', lane.getAttribute('data-move-hid'));
|
||||
data.set('work_id', id);
|
||||
data.set('lane', lane.getAttribute('data-lane'));
|
||||
slhxTechdemoEffect(lane.closest('[data-slhx-root]'), data);
|
||||
return false;
|
||||
}}
|
||||
</script>
|
||||
@@ -346,7 +446,10 @@ fn shell(body: String) -> String {
|
||||
.lane {{ min-height:330px; border:1px solid var(--line); border-radius:24px; padding:14px; background:linear-gradient(180deg, rgba(0,0,0,.26), rgba(255,255,255,.035)); overflow:hidden; }}
|
||||
.lane h3 {{ margin:0 0 4px; }}
|
||||
.lane p {{ color:var(--muted); margin:0 0 12px; font-size:13px; }}
|
||||
.work-card {{ border:1px solid rgba(255,255,255,.18); border-radius:20px; margin:12px 0; padding:14px; background:linear-gradient(145deg, rgba(255,255,255,.15), rgba(255,255,255,.055)); box-shadow:0 14px 38px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.1); overflow:hidden; overflow-wrap:anywhere; }}
|
||||
.work-card {{ border:1px solid rgba(255,255,255,.18); border-radius:20px; margin:12px 0; padding:14px; background:linear-gradient(145deg, rgba(255,255,255,.15), rgba(255,255,255,.055)); box-shadow:0 14px 38px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.1); overflow:hidden; overflow-wrap:anywhere; cursor:grab; }}
|
||||
.work-card:active {{ cursor:grabbing; }}
|
||||
.work-card.is-selected {{ border-color:rgba(68,231,255,.9); box-shadow:0 0 0 1px rgba(68,231,255,.4), 0 22px 55px rgba(68,231,255,.14); }}
|
||||
.lane.drop-ready {{ border-color:rgba(184,255,90,.85); background:linear-gradient(180deg, rgba(184,255,90,.11), rgba(255,255,255,.04)); }}
|
||||
.work-card header {{ display:flex; justify-content:space-between; gap:10px; align-items:start; }}
|
||||
.pill {{ display:inline-flex; border:1px solid var(--line); border-radius:999px; padding:4px 9px; font-size:12px; color:var(--lime); }}
|
||||
.impact {{ height:7px; border-radius:999px; background:rgba(255,255,255,.12); overflow:hidden; margin:12px 0; }}
|
||||
@@ -358,6 +461,10 @@ fn shell(body: String) -> String {
|
||||
.glow {{ box-shadow:0 0 0 1px rgba(184,255,90,.12), 0 24px 90px rgba(184,255,90,.08); }}
|
||||
.activity {{ display:grid; gap:10px; padding:0; margin:0; list-style:none; }}
|
||||
.activity li, .inspector-row, .live-row {{ border:1px solid var(--line); border-radius:16px; padding:12px; background:rgba(0,0,0,.18); color:var(--muted); overflow-wrap:anywhere; }}
|
||||
.inspector-hero {{ border:1px solid rgba(68,231,255,.35); border-radius:20px; padding:16px; margin-bottom:12px; background:linear-gradient(135deg, rgba(68,231,255,.15), rgba(255,79,216,.1)); }}
|
||||
.inspector-hero span {{ display:block; color:var(--cyan); font-size:12px; text-transform:uppercase; letter-spacing:.14em; font-weight:900; }}
|
||||
.inspector-hero strong {{ display:block; font-size:22px; letter-spacing:-.035em; margin-top:8px; overflow-wrap:anywhere; }}
|
||||
.inspector-hero em {{ display:block; color:var(--muted); font-style:normal; margin-top:6px; }}
|
||||
.inspector-row b {{ color:var(--text); }}
|
||||
.live-row strong {{ color:var(--lime); }}
|
||||
code {{ color:var(--cyan); }}
|
||||
@@ -388,37 +495,47 @@ fn render_hero(demo: &DemoState) -> String {
|
||||
}
|
||||
|
||||
fn render_board(demo: &DemoState) -> String {
|
||||
let lanes = LANES
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, (lane_id, title, description))| IssueLane {
|
||||
class: String::from("lane"),
|
||||
lane_id,
|
||||
move_handle: ui::control_center::handles::move_to_lane.id().id,
|
||||
title,
|
||||
description,
|
||||
cards: demo
|
||||
.work
|
||||
.iter()
|
||||
.filter(|item| item.lane == idx)
|
||||
.map(|item| issue_card(item, demo.selected_id == Some(item.id)))
|
||||
.collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut out = String::from("<div class=\"lanes\">");
|
||||
for (idx, (_, title, description)) in LANES.iter().enumerate() {
|
||||
out.push_str(&format!(r#"<section class="lane"><h3>{}</h3><p>{}</p>"#, escape_html(title), escape_html(description)));
|
||||
for item in demo.work.iter().filter(|item| item.lane == idx) {
|
||||
out.push_str(&render_card(item));
|
||||
}
|
||||
out.push_str("</section>");
|
||||
for lane in lanes {
|
||||
out.push_str(&render_template(&lane));
|
||||
}
|
||||
out.push_str("</div>");
|
||||
out
|
||||
}
|
||||
|
||||
fn render_card(item: &WorkItem) -> String {
|
||||
format!(
|
||||
r#"<article class="work-card" data-key="{id}">
|
||||
<header><strong>{title}</strong><span class="pill">{stage}</span></header>
|
||||
<div class="impact"><i style="width:{impact_percent}%"></i></div>
|
||||
<div class="card-actions">
|
||||
<button type="button" data-hid="{spotlight}" data-work-id="{id}">Inspect</button>
|
||||
<button type="button" data-hid="{advance}" data-work-id="{id}">Advance</button>
|
||||
<button type="button" data-hid="{delete}" data-work-id="{id}">Delete</button>
|
||||
</div>
|
||||
</article>"#,
|
||||
id = item.id,
|
||||
title = escape_html(&item.title),
|
||||
stage = item.stage.label(),
|
||||
impact_percent = item.impact as usize * 11,
|
||||
spotlight = ui::control_center::handles::spotlight_work.id().id,
|
||||
advance = ui::control_center::handles::advance_work.id().id,
|
||||
delete = ui::control_center::handles::delete_work.id().id,
|
||||
)
|
||||
fn issue_card(item: &WorkItem, selected: bool) -> IssueCard {
|
||||
IssueCard {
|
||||
class: if selected {
|
||||
String::from("work-card is-selected")
|
||||
} else {
|
||||
String::from("work-card")
|
||||
},
|
||||
id: item.id,
|
||||
title: item.title.clone(),
|
||||
stage: item.stage.label(),
|
||||
impact_style: format!("width:{}%", item.impact as usize * 11),
|
||||
spotlight_handle: ui::control_center::handles::spotlight_work.id().id,
|
||||
advance_handle: ui::control_center::handles::advance_work.id().id,
|
||||
delete_handle: ui::control_center::handles::delete_work.id().id,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_activity(demo: &DemoState) -> String {
|
||||
@@ -431,12 +548,24 @@ fn render_activity(demo: &DemoState) -> String {
|
||||
}
|
||||
|
||||
fn render_inspector(demo: &DemoState) -> String {
|
||||
format!(
|
||||
r#"<div class="inspector-row"><b>Selected fact</b><br>{}</div>
|
||||
<div class="inspector-row"><b>Wire contract</b><br><code>POST __h → application/slhx → EffectBatch</code></div>
|
||||
<div class="inspector-row"><b>Runtime</b><br>Root-scoped delegated listeners; numeric targets only.</div>"#,
|
||||
escape_html(&demo.spotlight),
|
||||
)
|
||||
let selected = demo
|
||||
.selected_id
|
||||
.and_then(|id| demo.work.iter().find(|item| item.id == id));
|
||||
let selected = selected.map_or_else(
|
||||
|| render_template(&InspectorEmpty),
|
||||
|item| {
|
||||
render_template(&InspectorSelected {
|
||||
title: item.title.clone(),
|
||||
lane: LANES[item.lane].1,
|
||||
stage: item.stage.label(),
|
||||
impact: item.impact,
|
||||
})
|
||||
},
|
||||
);
|
||||
render_template(&InspectorPanel {
|
||||
selected,
|
||||
spotlight: demo.spotlight.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn live_feed(tick: u64) -> String {
|
||||
@@ -461,6 +590,10 @@ fn architecture_activity() -> String {
|
||||
"<ol class=\"activity\"><li>Clicked a real anchor</li><li>Fetched HTML with X-SLHX-Partial</li><li>Preserved native fallback semantics</li></ol>".into()
|
||||
}
|
||||
|
||||
fn render_template(template: &impl Hemplate) -> String {
|
||||
template.render().expect("techdemo hemplate partial renders")
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
|
||||
@@ -63,4 +63,5 @@
|
||||
<button type="button" data-slhx-handle="advance_work" hidden="hidden">Advance</button>
|
||||
<button type="button" data-slhx-handle="delete_work" hidden="hidden">Delete</button>
|
||||
<button type="button" data-slhx-handle="spotlight_work" hidden="hidden">Spotlight</button>
|
||||
<button type="button" data-slhx-handle="move_to_lane" hidden="hidden">Move to lane</button>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="inspector-hero">
|
||||
<span>No issue selected</span>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
{+= self.selected =+}
|
||||
<div class="inspector-row"><b>What happened</b><br>{+ self.spotlight +}</div>
|
||||
<div class="inspector-row"><b>Wire contract</b><br><code>POST __h → application/slhx → EffectBatch</code></div>
|
||||
<div class="inspector-row"><b>Runtime</b><br>Root-scoped delegated listeners; numeric targets only.</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="inspector-hero">
|
||||
<span>Selected issue</span>
|
||||
<strong>{+ self.title +}</strong>
|
||||
<em>{+ self.lane +} · {+ self.stage +} · impact {+ self.impact +}</em>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<article +class="self.class" +data-key="self.id" draggable="true" ondragstart="slhxTechdemoDrag(event)">
|
||||
<header><strong>{+ self.title +}</strong><span class="pill">{+ self.stage +}</span></header>
|
||||
<div class="impact"><i +style="self.impact_style"></i></div>
|
||||
<div class="card-actions">
|
||||
<button type="button" +data-hid="self.spotlight_handle" +data-work-id="self.id">Inspect</button>
|
||||
<button type="button" +data-hid="self.advance_handle" +data-work-id="self.id">Advance</button>
|
||||
<button type="button" +data-hid="self.delete_handle" +data-work-id="self.id">Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,7 @@
|
||||
<section +class="self.class" +data-lane="self.lane_id" +data-move-hid="self.move_handle" ondragover="slhxTechdemoDragOver(event)" ondragleave="slhxTechdemoDragLeave(event)" ondrop="slhxTechdemoDrop(event)">
|
||||
<h3>{+ self.title +}</h3>
|
||||
<p>{+ self.description +}</p>
|
||||
<template h-for="card in &self.cards">
|
||||
{+ card +}
|
||||
</template>
|
||||
</section>
|
||||
@@ -81,6 +81,10 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
|
||||
wait_for_text(&driver, "body", "Browser verified issue").await?;
|
||||
assert_text(&driver, "width:88%").await?;
|
||||
|
||||
drag_card_to_lane(&driver, 4, "runtime").await?;
|
||||
wait_for_text(&driver, ".lane[data-lane='runtime'] .work-card[data-key='4']", "Active").await?;
|
||||
wait_for_text(&driver, &slot_selector(ui::control_center::slots::notice.id().id), "Drag-and-drop move persisted").await?;
|
||||
|
||||
driver
|
||||
.find(By::Css(&card_button_selector(ui::control_center::handles::spotlight_work.id().id, 4)))
|
||||
.await?
|
||||
@@ -89,7 +93,7 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
|
||||
wait_for_text(
|
||||
&driver,
|
||||
&slot_selector(ui::control_center::slots::inspector.id().id),
|
||||
"Browser verified issue · lane=Product · stage=Draft · impact=8",
|
||||
"Browser verified issue · lane=Runtime · stage=Active · impact=8",
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -98,7 +102,7 @@ async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
|
||||
.await?
|
||||
.click()
|
||||
.await?;
|
||||
wait_for_text(&driver, ".work-card[data-key='4']", "Active").await?;
|
||||
wait_for_text(&driver, ".lane[data-lane='product'] .work-card[data-key='4']", "Shipped").await?;
|
||||
|
||||
driver
|
||||
.find(By::Css(&handle_selector(ui::control_center::handles::simulate_push.id().id)))
|
||||
@@ -145,6 +149,28 @@ fn card_button_selector(handle_id: u32, work_id: u64) -> String {
|
||||
format!(r#"[data-hid="{handle_id}"][data-work-id="{work_id}"]"#)
|
||||
}
|
||||
|
||||
async fn drag_card_to_lane(driver: &WebDriver, work_id: u64, lane: &str) -> WebDriverResult<()> {
|
||||
driver
|
||||
.execute(
|
||||
&format!(
|
||||
r#"
|
||||
const card = document.querySelector({:?});
|
||||
const lane = document.querySelector({:?});
|
||||
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;
|
||||
"#,
|
||||
format!(".work-card[data-key='{work_id}']"),
|
||||
format!(".lane[data-lane='{lane}']"),
|
||||
),
|
||||
Vec::new(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_runtime(driver: &WebDriver) -> WebDriverResult<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(8);
|
||||
loop {
|
||||
|
||||
@@ -101,10 +101,19 @@ fn product_is_e2e_working_over_http() {
|
||||
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", ui::control_center::handles::move_to_lane.id().id),
|
||||
);
|
||||
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("/", "__h=3112123592&work_id=4");
|
||||
assert_effect_response(&inspect);
|
||||
let inspect_batch = inspect.batch();
|
||||
assert_payload_contains(&inspect_batch, "Design hero moment · lane=Product");
|
||||
assert_payload_contains(&inspect_batch, "Design hero moment · lane=Runtime");
|
||||
assert_payload_contains(&inspect_batch, "Inspector focused");
|
||||
|
||||
let advance = post("/", "__h=4085043678&work_id=4");
|
||||
|
||||
Reference in New Issue
Block a user