Preserve native interaction semantics
This commit is contained in:
@@ -1,17 +1,11 @@
|
|||||||
# Current plan
|
# Current plan
|
||||||
|
|
||||||
## Slice HMX-002 — Native interaction and recovery
|
## Closed receipt — HMX-002
|
||||||
|
|
||||||
|
Result: Generated semantic anchors and forms use the canonical marker vocabulary, bounded request policy, accessible validation markup, enhanced requests, and native recovery.
|
||||||
|
Proof: Focused Redgate checks, workspace gates, request-boundary tests, navigation failure recovery, and fresh-context review passed; HMX-002 IDs have no findings.
|
||||||
|
Residual risk: Browser behavior is deterministically checked at generated-markup, runtime, and Axum boundaries rather than through an external browser harness.
|
||||||
|
|
||||||
Outcome: Generated semantic HTML submits forms and navigation through the closed effect path while preserving accessibility and ordinary browser recovery.
|
|
||||||
Delta: html/001–007, runtime/003–004, axum/003–004.
|
|
||||||
Path: generated marker and native anchor/form -> request policy and Axum boundary -> rendered validation or navigation response -> accessible browser result or native fallback.
|
|
||||||
Build: Align generated marker vocabulary, native-event defaults, bounded request policies, form encoding, validation markup, partial navigation metadata, and full-navigation recovery.
|
|
||||||
Risk: Enhancement can suppress native input, accessibility, or fallback behavior and leave users unable to submit, navigate, or recover.
|
|
||||||
Checks: Generated-markup assertions for every marker and policy value; browser scenarios with enhancement enabled and unavailable; form encoding and body-limit integration tests; validation accessibility assertions; partial/full navigation recovery scenarios; `redgate check` for the slice IDs.
|
|
||||||
Non-goals: SSE, WebSocket, timers, reveal behavior, or island-owned local state.
|
|
||||||
Residual risk: Optional adapter and island lifecycle remains open.
|
|
||||||
State: Ready
|
|
||||||
Blocked by: none
|
|
||||||
|
|
||||||
## Slice HMX-003 — Direct adapters and explicit islands
|
## Slice HMX-003 — Direct adapters and explicit islands
|
||||||
|
|
||||||
|
|||||||
+10
-7
@@ -1359,7 +1359,7 @@ impl IntoResponse for PageResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn html_with_root_fingerprint(mut html: String, fingerprint: BuildFingerprint) -> String {
|
fn html_with_root_fingerprint(mut html: String, fingerprint: BuildFingerprint) -> String {
|
||||||
if html.contains("data-hemx-fp=") {
|
if html.contains("data-hemx-build=") {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
let Some(root_attr) = html.find("data-hemx-root") else {
|
let Some(root_attr) = html.find("data-hemx-root") else {
|
||||||
@@ -1372,7 +1372,10 @@ fn html_with_root_fingerprint(mut html: String, fingerprint: BuildFingerprint) -
|
|||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
let insert_at = tag_start + tag_end;
|
let insert_at = tag_start + tag_end;
|
||||||
html.insert_str(insert_at, &format!(" data-hemx-fp=\"{}\"", fingerprint.0));
|
html.insert_str(
|
||||||
|
insert_at,
|
||||||
|
&format!(" data-hemx-build=\"{}\"", fingerprint.0),
|
||||||
|
);
|
||||||
html
|
html
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1582,14 +1585,14 @@ mod tests {
|
|||||||
.select(&selector("main[data-hemx-root]"))
|
.select(&selector("main[data-hemx-root]"))
|
||||||
.next()
|
.next()
|
||||||
.expect("root element is rendered");
|
.expect("root element is rendered");
|
||||||
assert_eq!(root.value().attr("data-hemx-fp"), Some("99"));
|
assert_eq!(root.value().attr("data-hemx-build"), Some("99"));
|
||||||
assert_eq!(root.text().collect::<String>(), "Docs");
|
assert_eq!(root.text().collect::<String>(), "Docs");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn existing_root_fingerprint_is_preserved() {
|
fn existing_root_fingerprint_is_preserved() {
|
||||||
let html = html_with_root_fingerprint(
|
let html = html_with_root_fingerprint(
|
||||||
"<main data-hemx-root data-hemx-fp=\"1\">Docs</main>".into(),
|
"<main data-hemx-root data-hemx-build=\"1\">Docs</main>".into(),
|
||||||
BuildFingerprint(99),
|
BuildFingerprint(99),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1598,10 +1601,10 @@ mod tests {
|
|||||||
.select(&selector("main[data-hemx-root]"))
|
.select(&selector("main[data-hemx-root]"))
|
||||||
.next()
|
.next()
|
||||||
.expect("root element is rendered");
|
.expect("root element is rendered");
|
||||||
assert_eq!(root.value().attr("data-hemx-fp"), Some("1"));
|
assert_eq!(root.value().attr("data-hemx-build"), Some("1"));
|
||||||
assert_eq!(root.text().collect::<String>(), "Docs");
|
assert_eq!(root.text().collect::<String>(), "Docs");
|
||||||
assert_eq!(html.matches("data-hemx-fp=").count(), 1);
|
assert_eq!(html.matches("data-hemx-build=").count(), 1);
|
||||||
assert!(!html.contains("data-hemx-fp=\"99\""));
|
assert!(!html.contains("data-hemx-build=\"99\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+43
-44
@@ -1029,17 +1029,17 @@ fn __hemx_lower_html(html: &str, table: &[(&str, &str, u32)]) -> ::std::string::
|
|||||||
let mut out = html.to_owned();
|
let mut out = html.to_owned();
|
||||||
for (attr, name, id) in table {
|
for (attr, name, id) in table {
|
||||||
let runtime_attr = match *attr {
|
let runtime_attr = match *attr {
|
||||||
"data-hemx-slot" => "data-sid",
|
"data-hemx-slot" => "data-hemx-resource",
|
||||||
"data-hemx-handle" => "data-hid",
|
"data-hemx-handle" => "data-hemx-action",
|
||||||
"data-hemx-form" => "data-fid",
|
"data-hemx-form" => "data-hemx-resource",
|
||||||
"data-hemx-revealed" => "data-hemx-revealed",
|
"data-hemx-revealed" => "data-hemx-revealed",
|
||||||
"data-hemx-atom" => "data-aid",
|
"data-hemx-atom" => "data-hemx-resource",
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
out = __hemx_replace_attr(out, attr, name, runtime_attr, *id);
|
out = __hemx_replace_attr(out, attr, name, runtime_attr, *id);
|
||||||
}
|
}
|
||||||
out = __hemx_lower_static_attr(out, "data-hemx-key", "data-key");
|
out = __hemx_lower_static_attr(out, "data-hemx-key", "data-hemx-key");
|
||||||
out = __hemx_lower_static_attr(out, "h-key", "data-key");
|
out = __hemx_lower_static_attr(out, "h-key", "data-hemx-key");
|
||||||
__hemx_inject_handle_inputs(out)
|
__hemx_inject_handle_inputs(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1075,7 +1075,7 @@ fn __hemx_inject_handle_inputs(html: ::std::string::String) -> ::std::string::St
|
|||||||
out.push_str(opening);
|
out.push_str(opening);
|
||||||
rest = &rest[open_end + 1..];
|
rest = &rest[open_end + 1..];
|
||||||
|
|
||||||
let Some(handle_id) = __hemx_attr(opening, "data-hid") else {
|
let Some(handle_id) = __hemx_attr(opening, "data-hemx-action") else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let form_body_end = rest.find("</form>").unwrap_or(rest.len());
|
let form_body_end = rest.find("</form>").unwrap_or(rest.len());
|
||||||
@@ -1536,7 +1536,6 @@ fn known_hemx_attr(name: &str) -> bool {
|
|||||||
| "data-hemx-indicator"
|
| "data-hemx-indicator"
|
||||||
| "data-hemx-confirm"
|
| "data-hemx-confirm"
|
||||||
| "data-hemx-debounce"
|
| "data-hemx-debounce"
|
||||||
| "data-hemx-delay"
|
|
||||||
| "data-hemx-throttle"
|
| "data-hemx-throttle"
|
||||||
| "data-hemx-every"
|
| "data-hemx-every"
|
||||||
| "data-hemx-interval"
|
| "data-hemx-interval"
|
||||||
@@ -1622,7 +1621,7 @@ fn reject_invalid_hemx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
|
|||||||
"expected a non-negative number of viewports",
|
"expected a non-negative number of viewports",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
"data-hemx-debounce" | "data-hemx-delay" | "data-hemx-throttle" | "data-hemx-every"
|
"data-hemx-debounce" | "data-hemx-throttle" | "data-hemx-every"
|
||||||
| "data-hemx-interval"
|
| "data-hemx-interval"
|
||||||
if !valid_duration(value) =>
|
if !valid_duration(value) =>
|
||||||
{
|
{
|
||||||
@@ -2025,7 +2024,7 @@ fn unkeyed_generated_target_diagnostic(
|
|||||||
"a stable template h-key on h-for=\"{pattern} in {expr}\" so generated keyed helpers such as ui::{name}.replace(row) can target this partial"
|
"a stable template h-key on h-for=\"{pattern} in {expr}\" so generated keyed helpers such as ui::{name}.replace(row) can target this partial"
|
||||||
),
|
),
|
||||||
repair: format!(
|
repair: format!(
|
||||||
"add h-key=\"{pattern}.id\" to that h-for; dynamic +data-key on the child is rendered HTML, not the template fact hemx uses for generated targets"
|
"add h-key=\"{pattern}.id\" to that h-for; dynamic +data-hemx-key on the child is rendered HTML, not the template fact hemx uses for generated targets"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2655,12 +2654,12 @@ mod tests {
|
|||||||
let canonical_syms = resources.syms();
|
let canonical_syms = resources.syms();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stable_id("generated-rs", &canonical_generated),
|
stable_id("generated-rs", &canonical_generated),
|
||||||
262_445_078,
|
3_787_638_288,
|
||||||
"canonical generated Rust changed"
|
"canonical generated Rust changed"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stable_id("generated-rs-global", &canonical_globals),
|
stable_id("generated-rs-global", &canonical_globals),
|
||||||
3_211_924_671,
|
1_112_484_789,
|
||||||
"canonical global-export Rust changed"
|
"canonical global-export Rust changed"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3036,8 +3035,9 @@ mod tests {
|
|||||||
assert!(generated.contains("pub fn replace<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect"));
|
assert!(generated.contains("pub fn replace<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect"));
|
||||||
assert!(generated.contains("data-hemx-slot"));
|
assert!(generated.contains("data-hemx-slot"));
|
||||||
assert!(generated.contains("data-hemx-form"));
|
assert!(generated.contains("data-hemx-form"));
|
||||||
assert!(generated.contains("data-sid"));
|
assert!(generated.contains("data-hemx-resource"));
|
||||||
assert!(generated.contains("data-fid"));
|
assert!(!generated.contains("data-sid"));
|
||||||
|
assert!(!generated.contains("data-fid"));
|
||||||
assert!(generated.contains("__hemx_inject_handle_inputs"));
|
assert!(generated.contains("__hemx_inject_handle_inputs"));
|
||||||
|
|
||||||
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
|
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
|
||||||
@@ -3224,7 +3224,7 @@ mod tests {
|
|||||||
std::fs::create_dir_all(&templates).unwrap();
|
std::fs::create_dir_all(&templates).unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
templates.join("todos.heml"),
|
templates.join("todos.heml"),
|
||||||
r#"<ul data-hemx-slot="row"><template h-for="todo in &self.todos" h-key="todo.id"><li +data-key="todo.id">{+ todo +}</li></template></ul>"#,
|
r#"<ul data-hemx-slot="row"><template h-for="todo in &self.todos" h-key="todo.id"><li +data-hemx-key="todo.id">{+ todo +}</li></template></ul>"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -3465,6 +3465,8 @@ mod hemx {{
|
|||||||
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
|
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
|
||||||
pub struct Effect;
|
pub struct Effect;
|
||||||
#[derive(Clone, Copy)] pub struct ResourceId;
|
#[derive(Clone, Copy)] pub struct ResourceId;
|
||||||
|
#[derive(Clone, Copy)] pub struct EventName(&'static str);
|
||||||
|
impl EventName {{ pub const fn new(name: &'static str) -> Self {{ Self(name) }} }}
|
||||||
pub trait IntoEffect {{}}
|
pub trait IntoEffect {{}}
|
||||||
impl IntoEffect for Effect {{}}
|
impl IntoEffect for Effect {{}}
|
||||||
pub trait GeneratedTarget {{ fn __hemx_resource_id(self) -> ResourceId; }}
|
pub trait GeneratedTarget {{ fn __hemx_resource_id(self) -> ResourceId; }}
|
||||||
@@ -3722,7 +3724,9 @@ fn main() {{
|
|||||||
.contains("h-for=\"todo in &self.todos\" without h-key"));
|
.contains("h-for=\"todo in &self.todos\" without h-key"));
|
||||||
assert!(diagnostic.expected.contains("ui::todo_row.replace(row)"));
|
assert!(diagnostic.expected.contains("ui::todo_row.replace(row)"));
|
||||||
assert!(diagnostic.repair.contains("h-key=\"todo.id\""));
|
assert!(diagnostic.repair.contains("h-key=\"todo.id\""));
|
||||||
assert!(diagnostic.repair.contains("dynamic +data-key on the child"));
|
assert!(diagnostic
|
||||||
|
.repair
|
||||||
|
.contains("dynamic +data-hemx-key on the child"));
|
||||||
assert!(diagnostic.to_string().contains("templates/todo.heml"));
|
assert!(diagnostic.to_string().contains("templates/todo.heml"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3737,7 +3741,7 @@ fn main() {{
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"slot_data_key",
|
"slot_data_key",
|
||||||
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row" +data-key="todo.id">{+ todo.title +}</li></template>"#,
|
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row" +data-hemx-key="todo.id">{+ todo.title +}</li></template>"#,
|
||||||
"data-hemx-slot=\"todo_row\"",
|
"data-hemx-slot=\"todo_row\"",
|
||||||
"ui::todo_row",
|
"ui::todo_row",
|
||||||
),
|
),
|
||||||
@@ -3778,7 +3782,7 @@ fn main() {{
|
|||||||
assert!(err.contains("h-key=\"todo.id\""));
|
assert!(err.contains("h-key=\"todo.id\""));
|
||||||
assert!(err.contains("generated keyed helpers"));
|
assert!(err.contains("generated keyed helpers"));
|
||||||
assert!(err.contains(helper));
|
assert!(err.contains(helper));
|
||||||
assert!(err.contains("dynamic +data-key on the child"));
|
assert!(err.contains("dynamic +data-hemx-key on the child"));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
@@ -3985,12 +3989,6 @@ fn main() {{
|
|||||||
"data-hemx-ws",
|
"data-hemx-ws",
|
||||||
"non-empty",
|
"non-empty",
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"delay-empty",
|
|
||||||
r#"<button data-hemx-handle="save" data-hemx-delay="">Save</button>"#,
|
|
||||||
"data-hemx-delay",
|
|
||||||
"milliseconds",
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"revealed-ahead-negative",
|
"revealed-ahead-negative",
|
||||||
r#"<form data-hemx-revealed data-hemx-revealed-ahead="-1"></form>"#,
|
r#"<form data-hemx-revealed data-hemx-revealed-ahead="-1"></form>"#,
|
||||||
@@ -4003,12 +4001,6 @@ fn main() {{
|
|||||||
"data-hemx-throttle",
|
"data-hemx-throttle",
|
||||||
"milliseconds",
|
"milliseconds",
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"duration-suffix-only",
|
|
||||||
r#"<button data-hemx-handle="save" data-hemx-delay="s">Save</button>"#,
|
|
||||||
"data-hemx-delay",
|
|
||||||
"milliseconds",
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"policy-duplicate-anchor",
|
"policy-duplicate-anchor",
|
||||||
r#"<button data-hemx-handle="save" data-hemx-policy="newest">Save</button>"#,
|
r#"<button data-hemx-handle="save" data-hemx-policy="newest">Save</button>"#,
|
||||||
@@ -4021,12 +4013,6 @@ fn main() {{
|
|||||||
"data-hemx-debounce",
|
"data-hemx-debounce",
|
||||||
"250ms",
|
"250ms",
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"delay",
|
|
||||||
r#"<button data-hemx-handle="save" data-hemx-delay="soon">Save</button>"#,
|
|
||||||
"data-hemx-delay",
|
|
||||||
"250ms",
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"every",
|
"every",
|
||||||
r#"<button data-hemx-handle="save" data-hemx-every="1sec">Save</button>"#,
|
r#"<button data-hemx-handle="save" data-hemx-every="1sec">Save</button>"#,
|
||||||
@@ -4103,8 +4089,8 @@ fn main() {{
|
|||||||
"data-hemx-confirm" => "expected a non-empty confirmation message",
|
"data-hemx-confirm" => "expected a non-empty confirmation message",
|
||||||
"data-hemx-sse" => "expected a non-empty same-origin SSE URL",
|
"data-hemx-sse" => "expected a non-empty same-origin SSE URL",
|
||||||
"data-hemx-ws" => "expected a non-empty same-origin WebSocket URL",
|
"data-hemx-ws" => "expected a non-empty same-origin WebSocket URL",
|
||||||
"data-hemx-debounce" | "data-hemx-delay" | "data-hemx-throttle"
|
"data-hemx-debounce" | "data-hemx-throttle" | "data-hemx-every"
|
||||||
| "data-hemx-every" | "data-hemx-interval" => {
|
| "data-hemx-interval" => {
|
||||||
"expected milliseconds like `250`/`250ms` or seconds like `1s`"
|
"expected milliseconds like `250`/`250ms` or seconds like `1s`"
|
||||||
}
|
}
|
||||||
_ => panic!("missing expectation for {attr}"),
|
_ => panic!("missing expectation for {attr}"),
|
||||||
@@ -4170,7 +4156,7 @@ fn main() {{
|
|||||||
|
|
||||||
let valid_source = r#"
|
let valid_source = r#"
|
||||||
<main data-hemx-root="app" data-hemx-sse="/events" data-hemx-ws="/room/socket">
|
<main data-hemx-root="app" data-hemx-sse="/events" data-hemx-ws="/room/socket">
|
||||||
<button data-hemx-handle="save" data-hemx-policy="latest" data-hemx-on="click change" data-hemx-confirm="Save?" data-hemx-delay="250ms" data-hemx-throttle="1s">Save</button>
|
<button data-hemx-handle="save" data-hemx-policy="latest" data-hemx-on="click change" data-hemx-confirm="Save?" data-hemx-debounce="250ms" data-hemx-throttle="1s">Save</button>
|
||||||
<form method="get" action="/search" data-hemx-history="replace"><input name="q"></form>
|
<form method="get" action="/search" data-hemx-history="replace"><input name="q"></form>
|
||||||
</main>
|
</main>
|
||||||
"#;
|
"#;
|
||||||
@@ -4282,7 +4268,7 @@ fn main() {{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generated_lowering_injects_progressive_form_handle_and_key() {
|
fn generated_lowering_preserves_native_interaction_contract() {
|
||||||
let base = test_dir("hemx-build-lowering-test");
|
let base = test_dir("hemx-build-lowering-test");
|
||||||
let templates = base.join("templates");
|
let templates = base.join("templates");
|
||||||
let out = base.join("out");
|
let out = base.join("out");
|
||||||
@@ -4290,7 +4276,7 @@ fn main() {{
|
|||||||
std::fs::create_dir_all(&templates).unwrap();
|
std::fs::create_dir_all(&templates).unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
templates.join("todo.heml"),
|
templates.join("todo.heml"),
|
||||||
r#"<form data-hemx-handle="create"><input name="title"></form><li data-hemx-slot="row" data-hemx-key="7"></li>"#,
|
r#"<main data-hemx-root><a href="/docs">Docs</a><form action="/todos" method="post" data-hemx-handle="create"><label for="title">Title</label><input id="title" name="title" aria-invalid="true" aria-describedby="title-error"><span id="title-error">Required</span><button data-hemx-on="click">Create</button></form><form action="/search" method="get"><input name="q"></form><li data-hemx-slot="row" data-hemx-key="7"></li></main>"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -4305,6 +4291,8 @@ mod hemx {{
|
|||||||
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
|
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
|
||||||
pub struct Effect;
|
pub struct Effect;
|
||||||
#[derive(Clone, Copy)] pub struct ResourceId;
|
#[derive(Clone, Copy)] pub struct ResourceId;
|
||||||
|
#[derive(Clone, Copy)] pub struct EventName(&'static str);
|
||||||
|
impl EventName {{ pub const fn new(name: &'static str) -> Self {{ Self(name) }} }}
|
||||||
pub trait IntoEffect {{}}
|
pub trait IntoEffect {{}}
|
||||||
impl IntoEffect for Effect {{}}
|
impl IntoEffect for Effect {{}}
|
||||||
pub trait GeneratedTarget {{ fn __hemx_resource_id(self) -> ResourceId; }}
|
pub trait GeneratedTarget {{ fn __hemx_resource_id(self) -> ResourceId; }}
|
||||||
@@ -4337,10 +4325,21 @@ mod hemplate {{
|
|||||||
}}
|
}}
|
||||||
{generated}
|
{generated}
|
||||||
fn main() {{
|
fn main() {{
|
||||||
let html = todo::lower_html(r#"<form data-hemx-handle="create"><input name="title"></form><li data-hemx-slot="row" h-key="7"></li>"#);
|
let html = todo::lower_html(r#"<main data-hemx-root><a href="/docs">Docs</a><form action="/todos" method="post" data-hemx-handle="create"><label for="title">Title</label><input id="title" name="title" aria-invalid="true" aria-describedby="title-error"><span id="title-error">Required</span><button data-hemx-on="click">Create</button></form><form action="/search" method="get"><input name="q"></form><li data-hemx-slot="row" h-key="7"></li></main>"#);
|
||||||
assert!(html.contains(r#"data-hid="#));
|
assert!(html.contains(r#"data-hemx-root"#));
|
||||||
|
assert!(html.contains(r#"data-hemx-action="#));
|
||||||
|
assert!(html.contains(r#"data-hemx-resource="#));
|
||||||
|
assert!(html.contains(r#"data-hemx-key="7""#));
|
||||||
|
assert!(html.contains(r#"data-hemx-on="click""#));
|
||||||
|
assert!(html.contains(r#"<a href="/docs">"#));
|
||||||
|
assert!(html.contains(r#"action="/todos" method="post""#));
|
||||||
|
assert!(html.contains(r#"action="/search" method="get""#));
|
||||||
|
assert!(html.contains(r#"aria-invalid="true""#));
|
||||||
|
assert!(html.contains(r#"aria-describedby="title-error""#));
|
||||||
|
assert!(html.contains(r#"id="title-error">Required"#));
|
||||||
assert!(html.contains(r#"name="__h""#));
|
assert!(html.contains(r#"name="__h""#));
|
||||||
assert!(html.contains(r#"data-key="7""#));
|
assert!(!html.contains("data-hid"));
|
||||||
|
assert!(!html.contains("data-sid"));
|
||||||
assert!(!html.contains("h-key"));
|
assert!(!html.contains("h-key"));
|
||||||
}}
|
}}
|
||||||
"###,
|
"###,
|
||||||
|
|||||||
+39
-34
@@ -1,9 +1,9 @@
|
|||||||
(() => {
|
(() => {
|
||||||
const ROOT = "data-hemx-root";
|
const ROOT = "data-hemx-root";
|
||||||
const HID = "data-hid";
|
const ACTION = "data-hemx-action";
|
||||||
const SID = "data-sid";
|
const RESOURCE = "data-hemx-resource";
|
||||||
const runtimeAbiVersion = 1;
|
const runtimeAbiVersion = 1;
|
||||||
const FINGERPRINT = "data-hemx-fp";
|
const BUILD = "data-hemx-build";
|
||||||
const STATE = "data-hemx-st";
|
const STATE = "data-hemx-st";
|
||||||
const pending = new WeakMap();
|
const pending = new WeakMap();
|
||||||
const queues = new WeakMap();
|
const queues = new WeakMap();
|
||||||
@@ -54,8 +54,8 @@
|
|||||||
|
|
||||||
function formHandleId(form) {
|
function formHandleId(form) {
|
||||||
if (!form) return null;
|
if (!form) return null;
|
||||||
const holder = form.hasAttribute(HID) ? form : firstElement(form, (el) => el.hasAttribute(HID));
|
const holder = form.hasAttribute(ACTION) ? form : firstElement(form, (el) => el.hasAttribute(ACTION));
|
||||||
const raw = holder && holder.getAttribute(HID);
|
const raw = holder && holder.getAttribute(ACTION);
|
||||||
return raw && /^\d+$/.test(raw) ? raw : null;
|
return raw && /^\d+$/.test(raw) ? raw : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleId(el) {
|
function handleId(el) {
|
||||||
const raw = el && el.getAttribute(HID);
|
const raw = el && el.getAttribute(ACTION);
|
||||||
return raw && /^\d+$/.test(raw) ? raw : null;
|
return raw && /^\d+$/.test(raw) ? raw : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
const dragKey = eventName === "drop" && dragKeys.get(rootOf(el));
|
const dragKey = eventName === "drop" && dragKeys.get(rootOf(el));
|
||||||
if (dragKey && !data.has("work_id")) data.set("work_id", dragKey);
|
if (dragKey && !data.has("work_id")) data.set("work_id", dragKey);
|
||||||
for (const { name, value } of Array.from(el.attributes || [])) {
|
for (const { name, value } of Array.from(el.attributes || [])) {
|
||||||
if (name.startsWith("data-") && !name.startsWith("data-hemx-") && name !== HID && name !== SID) {
|
if (name.startsWith("data-") && !name.startsWith("data-hemx-") && name !== ACTION && name !== RESOURCE) {
|
||||||
data.set(name.slice(5).replace(/-/g, "_"), value);
|
data.set(name.slice(5).replace(/-/g, "_"), value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,17 +356,25 @@
|
|||||||
|
|
||||||
async function navigateUrl(href, root, mode = "replace") {
|
async function navigateUrl(href, root, mode = "replace") {
|
||||||
if (mode === "push") savePage(root);
|
if (mode === "push") savePage(root);
|
||||||
const response = await fetch(href, {
|
try {
|
||||||
headers: { "X-HEMX-Partial": "1", "Accept": "text/html" },
|
const response = await fetch(href, {
|
||||||
credentials: "same-origin",
|
headers: { "X-HEMX-Partial": "1", "Accept": "text/html" },
|
||||||
});
|
credentials: "same-origin",
|
||||||
if (!await applyResponse(response, root)) {
|
});
|
||||||
if (mode === "none") location.reload();
|
if (!response.ok || !await applyResponse(response, root)) {
|
||||||
else location.href = href;
|
fallbackNavigation(href, mode);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
if (mode === "push") history.pushState({ hemx: true }, "", href);
|
||||||
|
else if (mode === "replace") history.replaceState({ hemx: true }, "", href);
|
||||||
|
} catch (_) {
|
||||||
|
fallbackNavigation(href, mode);
|
||||||
}
|
}
|
||||||
if (mode === "push") history.pushState({ hemx: true }, "", href);
|
}
|
||||||
else if (mode === "replace") history.replaceState({ hemx: true }, "", href);
|
|
||||||
|
function fallbackNavigation(href, mode) {
|
||||||
|
if (mode === "none") location.reload();
|
||||||
|
else location.href = href;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyResponse(response, root) {
|
async function applyResponse(response, root) {
|
||||||
@@ -389,7 +397,7 @@
|
|||||||
|
|
||||||
function compatibleFingerprint(response, root) {
|
function compatibleFingerprint(response, root) {
|
||||||
const received = response.headers.get("x-hemx-fingerprint");
|
const received = response.headers.get("x-hemx-fingerprint");
|
||||||
const expected = root && root.getAttribute(FINGERPRINT);
|
const expected = root && root.getAttribute(BUILD);
|
||||||
return !received || !expected || received === expected;
|
return !received || !expected || received === expected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +412,7 @@
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const expected = root && root.getAttribute(FINGERPRINT);
|
const expected = root && root.getAttribute(BUILD);
|
||||||
if (expected && String(batch.fingerprint) !== expected) {
|
if (expected && String(batch.fingerprint) !== expected) {
|
||||||
location.reload();
|
location.reload();
|
||||||
return;
|
return;
|
||||||
@@ -495,11 +503,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generatedResource(el, id) {
|
function generatedResource(el, id) {
|
||||||
return attrEquals(el, "data-sid", id) || attrEquals(el, "data-slot-id", id);
|
return attrEquals(el, RESOURCE, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatedForm(el, id) {
|
function generatedForm(el, id) {
|
||||||
return attrEquals(el, "data-fid", id) || attrEquals(el, "data-form-id", id);
|
return generatedResource(el, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatedTarget(scope, id) {
|
function generatedTarget(scope, id) {
|
||||||
@@ -550,7 +558,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function keyedTarget(scope, id, key) {
|
function keyedTarget(scope, id, key) {
|
||||||
return firstElement(scope, (el) => attrEquals(el, "data-key", key) && withinGeneratedResource(el, scope, id));
|
return firstElement(scope, (el) => attrEquals(el, "data-hemx-key", key) && withinGeneratedResource(el, scope, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function fieldTarget(scope, id, field) {
|
function fieldTarget(scope, id, field) {
|
||||||
@@ -585,8 +593,8 @@
|
|||||||
else template.textContent = payload.value;
|
else template.textContent = payload.value;
|
||||||
const nodes = Array.from(template.content.childNodes);
|
const nodes = Array.from(template.content.childNodes);
|
||||||
const firstElement = nodes.find((node) => node.nodeType === 1);
|
const firstElement = nodes.find((node) => node.nodeType === 1);
|
||||||
if (firstElement && key != null && !firstElement.hasAttribute("data-key")) firstElement.setAttribute("data-key", key);
|
if (firstElement && key != null && !firstElement.hasAttribute("data-hemx-key")) firstElement.setAttribute("data-hemx-key", key);
|
||||||
if (firstElement && resourceId != null && !firstElement.hasAttribute("data-sid")) firstElement.setAttribute("data-sid", resourceId);
|
if (firstElement && resourceId != null && !firstElement.hasAttribute("data-hemx-resource")) firstElement.setAttribute("data-hemx-resource", resourceId);
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,7 +627,7 @@
|
|||||||
function replaceLoweredSlots(scope, doc) {
|
function replaceLoweredSlots(scope, doc) {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
forEachElement(doc.body || doc, (source) => {
|
forEachElement(doc.body || doc, (source) => {
|
||||||
const id = source.getAttribute("data-sid") || source.getAttribute("data-slot-id");
|
const id = source.getAttribute(RESOURCE);
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const target = generatedTarget(scope, id);
|
const target = generatedTarget(scope, id);
|
||||||
if (target) {
|
if (target) {
|
||||||
@@ -674,15 +682,15 @@
|
|||||||
["click", "submit", "input", "change", "keydown", "dragstart", "dragover", "drop"].forEach((name) => {
|
["click", "submit", "input", "change", "keydown", "dragstart", "dragover", "drop"].forEach((name) => {
|
||||||
root.addEventListener(name, (event) => {
|
root.addEventListener(name, (event) => {
|
||||||
if (name === "dragstart") {
|
if (name === "dragstart") {
|
||||||
const item = closestInRoot(event.target, root, (el) => el.hasAttribute("data-key"));
|
const item = closestInRoot(event.target, root, (el) => el.hasAttribute("data-hemx-key"));
|
||||||
if (item) {
|
if (item) {
|
||||||
dragKeys.set(root, item.getAttribute("data-key"));
|
dragKeys.set(root, item.getAttribute("data-hemx-key"));
|
||||||
if (event.dataTransfer) event.dataTransfer.setData("text/plain", item.getAttribute("data-key"));
|
if (event.dataTransfer) event.dataTransfer.setData("text/plain", item.getAttribute("data-hemx-key"));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "dragover") {
|
if (name === "dragover") {
|
||||||
const drop = closestInRoot(event.target, root, (el) => el.hasAttribute(HID) && el.getAttribute("data-hemx-on") === "drop");
|
const drop = closestInRoot(event.target, root, (el) => el.hasAttribute(ACTION) && el.getAttribute("data-hemx-on") === "drop");
|
||||||
if (drop) event.preventDefault();
|
if (drop) event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -696,7 +704,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "click") {
|
if (name === "click") {
|
||||||
const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(HID));
|
const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(ACTION));
|
||||||
if (direct && handlesEvent(direct, "click")) {
|
if (direct && handlesEvent(direct, "click")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
schedule(direct, name);
|
schedule(direct, name);
|
||||||
@@ -717,7 +725,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let el = closestInRoot(event.target, root, (node) =>
|
let el = closestInRoot(event.target, root, (node) =>
|
||||||
node.hasAttribute(HID) ||
|
node.hasAttribute(ACTION) ||
|
||||||
(node.tagName === "FORM" && (pageFormHistoryMode(event.submitter || node, node) || boostRoot(node)))
|
(node.tagName === "FORM" && (pageFormHistoryMode(event.submitter || node, node) || boostRoot(node)))
|
||||||
);
|
);
|
||||||
if (name === "submit" && !el && event.target && event.target.tagName === "FORM" && (formHandleId(event.target) || pageFormHistoryMode(event.submitter || event.target, event.target))) el = event.target;
|
if (name === "submit" && !el && event.target && event.target.tagName === "FORM" && (formHandleId(event.target) || pageFormHistoryMode(event.submitter || event.target, event.target))) el = event.target;
|
||||||
@@ -732,13 +740,10 @@
|
|||||||
|
|
||||||
function schedule(el, eventName, source = el) {
|
function schedule(el, eventName, source = el) {
|
||||||
const debounce = duration(el.getAttribute("data-hemx-debounce"));
|
const debounce = duration(el.getAttribute("data-hemx-debounce"));
|
||||||
const delay = duration(el.getAttribute("data-hemx-delay"));
|
|
||||||
const throttle = duration(el.getAttribute("data-hemx-throttle"));
|
const throttle = duration(el.getAttribute("data-hemx-throttle"));
|
||||||
if (debounce) {
|
if (debounce) {
|
||||||
clearTimeout(timers.get(el));
|
clearTimeout(timers.get(el));
|
||||||
timers.set(el, setTimeout(() => send(el, eventName, source), debounce));
|
timers.set(el, setTimeout(() => send(el, eventName, source), debounce));
|
||||||
} else if (delay) {
|
|
||||||
setTimeout(() => send(el, eventName, source), delay);
|
|
||||||
} else if (throttle) {
|
} else if (throttle) {
|
||||||
if (timers.get(el)) return;
|
if (timers.get(el)) return;
|
||||||
send(el, eventName, source).finally(() => setTimeout(() => timers.delete(el), throttle));
|
send(el, eventName, source).finally(() => setTimeout(() => timers.delete(el), throttle));
|
||||||
|
|||||||
@@ -75,6 +75,20 @@ fn runtime_preserves_multipart_file_upload_fallback_shape() {
|
|||||||
assert!(source.contains("if (body instanceof URLSearchParams) headers[\"Content-Type\"] = \"application/x-www-form-urlencoded;charset=UTF-8\""));
|
assert!(source.contains("if (body instanceof URLSearchParams) headers[\"Content-Type\"] = \"application/x-www-form-urlencoded;charset=UTF-8\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_preserves_native_form_controls_and_validation() {
|
||||||
|
let source = hemx_js::RUNTIME_JS;
|
||||||
|
|
||||||
|
assert!(source.contains("if (form.reportValidity && !form.reportValidity()) return;"));
|
||||||
|
assert!(source.contains(
|
||||||
|
"return source && source !== form ? new FormData(form, source) : new FormData(form);"
|
||||||
|
));
|
||||||
|
assert!(source.contains("event.submitter || node"));
|
||||||
|
assert!(source.contains("multipart/form-data"));
|
||||||
|
assert!(!source.contains("compositionstart"));
|
||||||
|
assert!(!source.contains("compositionend"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_uses_root_scoped_walks_not_dom_selector_apis() {
|
fn runtime_uses_root_scoped_walks_not_dom_selector_apis() {
|
||||||
let source = hemx_js::RUNTIME_JS;
|
let source = hemx_js::RUNTIME_JS;
|
||||||
@@ -98,7 +112,7 @@ fn runtime_targets_generated_resources_not_response_selectors() {
|
|||||||
assert!(source.contains("return generatedTarget(scope, ref.resource.id)"));
|
assert!(source.contains("return generatedTarget(scope, ref.resource.id)"));
|
||||||
assert!(source.contains("function firstElement(scope, predicate)"));
|
assert!(source.contains("function firstElement(scope, predicate)"));
|
||||||
assert!(source.contains("function generatedResource(el, id)"));
|
assert!(source.contains("function generatedResource(el, id)"));
|
||||||
assert!(source.contains("return firstElement(scope, (el) => attrEquals(el, \"data-key\", key) && withinGeneratedResource(el, scope, id))"));
|
assert!(source.contains("return firstElement(scope, (el) => attrEquals(el, \"data-hemx-key\", key) && withinGeneratedResource(el, scope, id))"));
|
||||||
assert!(source.contains("if (node.hasAttribute && node.hasAttribute(ROOT)) continue"));
|
assert!(source.contains("if (node.hasAttribute && node.hasAttribute(ROOT)) continue"));
|
||||||
assert!(source.contains("if (!applyOp(scope, op)) return"));
|
assert!(source.contains("if (!applyOp(scope, op)) return"));
|
||||||
assert!(!source.contains("canApplyOp"));
|
assert!(!source.contains("canApplyOp"));
|
||||||
@@ -129,11 +143,10 @@ fn runtime_interval_dispatch_avoids_duplicate_timers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_supports_tiny_delay_and_revealed_scheduling() {
|
fn runtime_supports_revealed_scheduling() {
|
||||||
let source = hemx_js::RUNTIME_JS;
|
let source = hemx_js::RUNTIME_JS;
|
||||||
|
|
||||||
assert!(source.contains("data-hemx-delay"));
|
assert!(!source.contains("data-hemx-delay"));
|
||||||
assert!(source.contains("setTimeout(() => send(el, eventName, source), delay)"));
|
|
||||||
assert!(source.contains("const revealObservers = new WeakMap()"));
|
assert!(source.contains("const revealObservers = new WeakMap()"));
|
||||||
assert!(source.contains("const revealed = new WeakSet()"));
|
assert!(source.contains("const revealed = new WeakSet()"));
|
||||||
assert!(source.contains("data-hemx-revealed"));
|
assert!(source.contains("data-hemx-revealed"));
|
||||||
@@ -238,7 +251,7 @@ fn runtime_clicking_submitter_schedules_form_submit() {
|
|||||||
);
|
);
|
||||||
assert!(!source.contains("document.getElementById(el.getAttribute(\"form\"))"));
|
assert!(!source.contains("document.getElementById(el.getAttribute(\"form\"))"));
|
||||||
assert!(source.contains(
|
assert!(source.contains(
|
||||||
"const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(HID))"
|
"const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(ACTION))"
|
||||||
));
|
));
|
||||||
assert!(source.contains("const submitter = closestInRoot(event.target, root, (el) =>"));
|
assert!(source.contains("const submitter = closestInRoot(event.target, root, (el) =>"));
|
||||||
assert!(source.contains("el.tagName === \"BUTTON\" && (!el.hasAttribute(\"type\") || el.getAttribute(\"type\") === \"submit\")"));
|
assert!(source.contains("el.tagName === \"BUTTON\" && (!el.hasAttribute(\"type\") || el.getAttribute(\"type\") === \"submit\")"));
|
||||||
@@ -309,6 +322,7 @@ fn runtime_exposes_page_swap_hooks() {
|
|||||||
assert!(source.contains("x-hemx-fingerprint"));
|
assert!(source.contains("x-hemx-fingerprint"));
|
||||||
assert!(source.contains("hemx:missing-content-slot"));
|
assert!(source.contains("hemx:missing-content-slot"));
|
||||||
assert!(source.contains("else location.href = href"));
|
assert!(source.contains("else location.href = href"));
|
||||||
|
assert!(source.contains("if (!response.ok || !await applyResponse(response, root))"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -317,6 +331,7 @@ fn runtime_popstate_failed_partials_reload_instead_of_stale_ui() {
|
|||||||
|
|
||||||
assert!(source.contains("if (mode === \"none\") location.reload();"));
|
assert!(source.contains("if (mode === \"none\") location.reload();"));
|
||||||
assert!(source.contains("if (root) navigateUrl(location.href, root, \"none\")"));
|
assert!(source.contains("if (root) navigateUrl(location.href, root, \"none\")"));
|
||||||
|
assert!(source.contains("catch (_) {\n fallbackNavigation(href, mode);"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -338,7 +353,7 @@ fn runtime_refuses_partial_updates_on_fingerprint_mismatch() {
|
|||||||
|
|
||||||
assert!(source.contains("function compatibleFingerprint(response, root)"));
|
assert!(source.contains("function compatibleFingerprint(response, root)"));
|
||||||
assert!(source.contains("const received = response.headers.get(\"x-hemx-fingerprint\")"));
|
assert!(source.contains("const received = response.headers.get(\"x-hemx-fingerprint\")"));
|
||||||
assert!(source.contains("const expected = root && root.getAttribute(FINGERPRINT)"));
|
assert!(source.contains("const expected = root && root.getAttribute(BUILD)"));
|
||||||
assert!(source.contains("if (!compatibleFingerprint(response, root))"));
|
assert!(source.contains("if (!compatibleFingerprint(response, root))"));
|
||||||
assert!(source.contains("location.reload()"));
|
assert!(source.contains("location.reload()"));
|
||||||
assert!(source.contains("if (expected && String(batch.fingerprint) !== expected)"));
|
assert!(source.contains("if (expected && String(batch.fingerprint) !== expected)"));
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ Complete server-rendered pages can be inspected directly:
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
let page = hemx_test::inspect_html_document(
|
let page = hemx_test::inspect_html_document(
|
||||||
"<!doctype html><html><body><main id=app data-sid=7>Ready</main></body></html>",
|
"<!doctype html><html><body><main id=app data-hemx-resource=7>Ready</main></body></html>",
|
||||||
);
|
);
|
||||||
page.assert_text("main#app", "Ready");
|
page.assert_text("main#app", "Ready");
|
||||||
# use hemx_core::{GeneratedTarget, ResourceId, Slot};
|
# use hemx_core::{GeneratedTarget, ResourceId, Slot};
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ impl HtmlInspector {
|
|||||||
|
|
||||||
/// Select elements carrying the runtime marker for a typed handle.
|
/// Select elements carrying the runtime marker for a typed handle.
|
||||||
pub fn select_handle<I>(&self, handle: Handle<I>) -> HtmlSelection {
|
pub fn select_handle<I>(&self, handle: Handle<I>) -> HtmlSelection {
|
||||||
self.selection_or_panic(&attribute_selector("data-hid", &handle.to_string()))
|
self.selection_or_panic(&attribute_selector("data-hemx-action", &handle.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assert that rendered HTML contains a typed handle marker.
|
/// Assert that rendered HTML contains a typed handle marker.
|
||||||
@@ -299,10 +299,10 @@ impl Error for HtmlInspectionError {}
|
|||||||
|
|
||||||
fn resource_attribute(kind: ResourceKind) -> &'static str {
|
fn resource_attribute(kind: ResourceKind) -> &'static str {
|
||||||
match kind {
|
match kind {
|
||||||
ResourceKind::Slot => "data-sid",
|
ResourceKind::Slot => "data-hemx-resource",
|
||||||
ResourceKind::Atom => "data-aid",
|
ResourceKind::Atom => "data-hemx-resource",
|
||||||
ResourceKind::Handle => "data-hid",
|
ResourceKind::Handle => "data-hemx-action",
|
||||||
ResourceKind::Form => "data-fid",
|
ResourceKind::Form => "data-hemx-resource",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ impl EffectInspector {
|
|||||||
|
|
||||||
/// Assert that generated keyed-row metadata for a key is absent from payloads.
|
/// Assert that generated keyed-row metadata for a key is absent from payloads.
|
||||||
pub fn payload_excludes_key(&self, key: impl ToString) -> bool {
|
pub fn payload_excludes_key(&self, key: impl ToString) -> bool {
|
||||||
self.payload_excludes(&format!("data-key=\"{}\"", key.to_string()))
|
self.payload_excludes(&format!("data-hemx-key=\"{}\"", key.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return HTML for a generated target containing text, without exposing raw payloads.
|
/// Return HTML for a generated target containing text, without exposing raw payloads.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ fn inspects_generated_targets_and_handles_without_raw_runtime_ids() {
|
|||||||
let target = TestTarget(ResourceKind::Slot, 42);
|
let target = TestTarget(ResourceKind::Slot, 42);
|
||||||
let handle = hemx_core::Handle::<()>::new(7);
|
let handle = hemx_core::Handle::<()>::new(7);
|
||||||
let inspected = hemx_test::inspect_html_fragment(
|
let inspected = hemx_test::inspect_html_fragment(
|
||||||
r#"<section data-sid="42"><button data-hid="7">Save</button></section>"#,
|
r#"<section data-hemx-resource="42"><button data-hemx-action="7">Save</button></section>"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
inspected.assert_target(target);
|
inspected.assert_target(target);
|
||||||
@@ -100,7 +100,7 @@ fn inspects_generated_target_document_and_fragment_payloads() {
|
|||||||
Effect::Insert {
|
Effect::Insert {
|
||||||
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 2)),
|
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 2)),
|
||||||
position: InsertPosition::Last,
|
position: InsertPosition::Last,
|
||||||
html: SafeHtml::trusted("<li data-key=later>later</li>"),
|
html: SafeHtml::trusted("<li data-hemx-key=later>later</li>"),
|
||||||
},
|
},
|
||||||
"Insert",
|
"Insert",
|
||||||
),
|
),
|
||||||
@@ -108,7 +108,7 @@ fn inspects_generated_target_document_and_fragment_payloads() {
|
|||||||
Effect::Insert {
|
Effect::Insert {
|
||||||
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 2)),
|
target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 2)),
|
||||||
position: InsertPosition::First,
|
position: InsertPosition::First,
|
||||||
html: SafeHtml::trusted("<li data-key=first>first</li>"),
|
html: SafeHtml::trusted("<li data-hemx-key=first>first</li>"),
|
||||||
},
|
},
|
||||||
"Insert",
|
"Insert",
|
||||||
),
|
),
|
||||||
@@ -116,7 +116,7 @@ fn inspects_generated_target_document_and_fragment_payloads() {
|
|||||||
let fragment = hemx_test::inspect(effect)
|
let fragment = hemx_test::inspect(effect)
|
||||||
.target_html_fragment(TestTarget(ResourceKind::Slot, 2))
|
.target_html_fragment(TestTarget(ResourceKind::Slot, 2))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
fragment.assert_count("li[data-key]", 1);
|
fragment.assert_count("li[data-hemx-key]", 1);
|
||||||
assert!(fragment.origin().contains(operation));
|
assert!(fragment.origin().contains(operation));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ fn inspector_predicates_bind_operation_target_scope_kind_and_payload() {
|
|||||||
Effect::Patch {
|
Effect::Patch {
|
||||||
target: keyed_ref.clone(),
|
target: keyed_ref.clone(),
|
||||||
mode: PatchMode::Replace,
|
mode: PatchMode::Replace,
|
||||||
html: SafeHtml::trusted("<li data-key=\"row-1\">replacement</li>"),
|
html: SafeHtml::trusted("<li data-hemx-key=\"row-1\">replacement</li>"),
|
||||||
},
|
},
|
||||||
Effect::Insert {
|
Effect::Insert {
|
||||||
target: ResourceRef::unscoped(resource),
|
target: ResourceRef::unscoped(resource),
|
||||||
@@ -202,7 +202,7 @@ fn inspector_predicates_bind_operation_target_scope_kind_and_payload() {
|
|||||||
assert!(!inspected.payload_excludes_key("row-1"));
|
assert!(!inspected.payload_excludes_key("row-1"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inspected.target_html_containing(expected, "replacement"),
|
inspected.target_html_containing(expected, "replacement"),
|
||||||
Some("<li data-key=\"row-1\">replacement</li>")
|
Some("<li data-hemx-key=\"row-1\">replacement</li>")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inspected.target_html_containing(expected, "inserted"),
|
inspected.target_html_containing(expected, "inserted"),
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ cargo test -p hemx-derive --test compile_fail generated_resource_references_fail
|
|||||||
cargo test -p hemx-axum --test response effect_response_is_wire_batch_with_fingerprint_header -- --exact # spec: axum/001 check
|
cargo test -p hemx-axum --test response effect_response_is_wire_batch_with_fingerprint_header -- --exact # spec: axum/001 check
|
||||||
cargo test -p hemx-axum --test response runtime_js_response_serves_embedded_runtime -- --exact # spec: axum/002 check
|
cargo test -p hemx-axum --test response runtime_js_response_serves_embedded_runtime -- --exact # spec: axum/002 check
|
||||||
cargo test -p hemx-axum --test response partial_page_response_sets_partial_and_title_headers -- --exact # spec: axum/003 check
|
cargo test -p hemx-axum --test response partial_page_response_sets_partial_and_title_headers -- --exact # spec: axum/003 check
|
||||||
|
cargo test -p hemx-js --test runtime runtime_popstate_failed_partials_reload_instead_of_stale_ui -- --exact # spec: axum/003 check
|
||||||
cargo test -p hemx-axum --test response interaction_boundary_honors_media_type_and_host_body_limit -- --exact # spec: axum/004 check
|
cargo test -p hemx-axum --test response interaction_boundary_honors_media_type_and_host_body_limit -- --exact # spec: axum/004 check
|
||||||
cargo test -p hemx-axum --test response effect_and_dispatch_responses_preserve_status_wire_and_diagnostics -- --exact # spec: axum/005 check
|
cargo test -p hemx-axum --test response effect_and_dispatch_responses_preserve_status_wire_and_diagnostics -- --exact # spec: axum/005 check
|
||||||
cargo test -p hemx-js --test runtime runtime_exposes_debug_api_before_startup_side_effects -- --exact # spec: runtime/002 check
|
cargo test -p hemx-js --test runtime runtime_exposes_debug_api_before_startup_side_effects -- --exact # spec: runtime/002 check
|
||||||
|
cargo test -p hemx-build generated_lowering_preserves_native_interaction_contract --lib # spec: html/001 check # spec: html/002 check # spec: html/003 check # spec: html/004 check
|
||||||
|
cargo test -p hemx-build static_attribute_value_validators_cover_every_boundary --lib # spec: html/006 check # spec: html/007 check
|
||||||
|
cargo test -p hemx-js --test runtime runtime_preserves_native_form_controls_and_validation -- --exact # spec: html/005 check
|
||||||
cargo test -p hemx-js --test runtime runtime_posts_urlencoded_forms_by_default -- --exact # spec: runtime/003 check
|
cargo test -p hemx-js --test runtime runtime_posts_urlencoded_forms_by_default -- --exact # spec: runtime/003 check
|
||||||
cargo test -p hemx-js --test runtime runtime_turns_page_get_forms_into_url_state_navigation -- --exact # spec: runtime/004 check
|
cargo test -p hemx-js --test runtime runtime_turns_page_get_forms_into_url_state_navigation -- --exact # spec: runtime/004 check
|
||||||
cargo test -p hemx-js --test runtime runtime_targets_generated_resources_not_response_selectors -- --exact # spec: kernel/003 check # spec: runtime/001 check
|
cargo test -p hemx-js --test runtime runtime_targets_generated_resources_not_response_selectors -- --exact # spec: kernel/003 check # spec: runtime/001 check
|
||||||
|
|||||||
Reference in New Issue
Block a user