diff --git a/examples/html_examples/README.md b/examples/html_examples/README.md index 613aa46..b4104c9 100644 --- a/examples/html_examples/README.md +++ b/examples/html_examples/README.md @@ -39,7 +39,7 @@ Status legend: | `inline-validation` | implemented | A generated form reports field failure with `validate_email_form.error(...)`, focuses the field, and updates status text. | `templates/gallery.heml`, `gallery_handlers::validate_email` | | `infinite-scroll` | implemented | A revealed sentinel form posts to the same server-owned loading model and replaces generated keyed rows. | `gallery_handlers::infinite_scroll`, `data-hemx-revealed`, `infinite_row` | | `active-search` | implemented | The search form posts a query; the server derives result rows and reconciles generated keyed partials by removing filtered-out keys, replacing retained keys, and appending newly visible keys. | `gallery_handlers::search`, `SearchResult` | -| `progress-bar` | implemented | `data-hemx-interval` ticks a server-owned progress value and replaces a generated progress partial. | `gallery_handlers::tick_progress`, `ProgressMeter` | +| `progress-bar` | implemented | The Tick progress button advances server-owned progress and replaces a generated progress partial with visible percentage text. | `gallery_handlers::tick_progress`, `ProgressMeter` | | `value-select` | implemented | The first select posts a generated form; the server derives and replaces generated option rows for the second select. | `gallery_handlers::choose_category`, `ValueOption` | | `animations` | integration-owned | CSS transitions are presentation policy around generated replacements; hemx should only preserve stable DOM boundaries. | Use keyed partials and app CSS; no core animation framework. | | `file-upload` | integration-owned | Upload transport, size limits, progress, storage, and security are app/integration policy. | Needs product-owned upload route before becoming copyable. | diff --git a/examples/html_examples/src/main.rs b/examples/html_examples/src/main.rs index 651ad7a..246fb09 100644 --- a/examples/html_examples/src/main.rs +++ b/examples/html_examples/src/main.rs @@ -179,8 +179,8 @@ impl Hemplate for ProgressMeter { use std::fmt::Write as _; write!( out, - "{}% complete", - self.percent, self.percent + " {}% complete", + self.percent, self.percent, self.percent ) .expect("write to String cannot fail"); Ok(()) @@ -815,7 +815,8 @@ mod tests { assert!(html.contains("data-hemx-form=\"validate_email\"")); assert!(html.contains("data-hemx-revealed=\"true\"")); assert!(html.contains("data-hemx-handle=\"tick_progress\"")); - assert!(html.contains("data-hemx-interval=\"1000\"")); + assert!(!html.contains("data-hemx-interval=\"1000\"")); + assert!(html.contains("0% complete")); assert!(html.contains("data-hemx-slot=\"loaded_row\"")); assert!(html.contains("data-hemx-slot=\"infinite_row\"")); assert!(html.contains("data-hemx-slot=\"value_option\"")); @@ -921,7 +922,7 @@ mod tests { .unwrap() .batch, ); - assert!(progress.payload_contains("25% complete")); + assert!(progress.payload_contains("25% complete")); let values = inspect_batch( InteractionRequest::from(form(gallery::choose_category, &[("category", "numbers")])) diff --git a/examples/html_examples/templates/gallery.heml b/examples/html_examples/templates/gallery.heml index 6374c68..5794a37 100644 --- a/examples/html_examples/templates/gallery.heml +++ b/examples/html_examples/templates/gallery.heml @@ -78,7 +78,7 @@

progress-bar

-
+
diff --git a/hemx-xtask/src/main.rs b/hemx-xtask/src/main.rs index f42a0a6..e5b38f5 100644 --- a/hemx-xtask/src/main.rs +++ b/hemx-xtask/src/main.rs @@ -270,7 +270,7 @@ const LAZY_LOAD_SMOKE: &str = r#"(async()=>{const f=Array.from(document.querySel const CLICK_TO_LOAD_SMOKE: &str = r#"(async()=>{const text=()=>document.body.textContent.replace(/\s+/g," "); const f=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.includes("Load more")); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); if(!(text().includes("Loaded row 3")&&text().includes("Loaded row 4"))) throw new Error("click-to-load did not append rows"); return true;})()"#; const INFINITE_SCROLL_SMOKE: &str = r#"(async()=>{const text=()=>document.body.textContent.replace(/\s+/g," "); const f=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.includes("Reveal more rows")); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); if(!(text().includes("Loaded row 4")&&text().includes("Loaded row 6"))) throw new Error("infinite scroll did not append rows"); return true;})()"#; const REVEALED_FALLBACK_SMOKE: &str = r#"(async()=>{const text=()=>document.body.textContent.replace(/\s+/g," "); const runtime=document.querySelector('script[src*="hemx"][src$=".js"]'); if(!runtime) throw new Error("runtime script not found"); Object.defineProperty(window,"IntersectionObserver",{value:undefined,configurable:true}); const reloaded=document.createElement("script"); reloaded.src=runtime.src; document.head.appendChild(reloaded); for(let i=0;i<20;i++){if(text().includes("Lazy content loaded by server update #")&&text().includes("Loaded row 4")&&text().includes("Loaded row 6")) return true; await new Promise(r=>setTimeout(r,150));} throw new Error("revealed fallback did not dispatch lazy/infinite forms without a click");})()"#; -const PROGRESS_SMOKE: &str = r#"(async()=>{const f=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.includes("Tick progress")); if(f.getAttribute("data-hemx-interval")!=="1000") throw new Error("progress form is not interval-driven"); const text=()=>document.querySelector("progress").textContent.trim(); const before=text(); await new Promise(r=>setTimeout(r,1300)); const after=text(); if(!(after!==before&&after!=="100% complete"&&after.endsWith("% complete"))) throw new Error("progress interval did not visibly tick"); return true;})()"#; +const PROGRESS_SMOKE: &str = r#"(async()=>{const f=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.includes("Tick progress")); if(f.hasAttribute("data-hemx-interval")) throw new Error("progress form should wait for an explicit click"); const text=()=>document.querySelector("progress").parentElement.textContent.replace(/\s+/g," ").trim(); const before=text(); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); const after=text(); if(!(before.includes("0% complete")&&after.includes("25% complete"))) throw new Error("progress click did not visibly tick from 0% to 25%"); return true;})()"#; const VALUE_SELECT_SMOKE: &str = r#"(async()=>{const f=Array.from(document.querySelectorAll("form")).find(f=>f.elements.category); f.elements.category.value="numbers"; f.elements.category.dispatchEvent(new Event("change",{bubbles:true,cancelable:true})); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); const options=Array.from(document.querySelectorAll("select[name=value] option")).map(option=>option.textContent.trim()); if(!(options.includes("One")&&options.includes("Two")&&!options.includes("Alpha"))) throw new Error("value select did not replace options"); return true;})()"#; const RESET_INPUT_SMOKE: &str = r#"(async()=>{const f=Array.from(document.querySelectorAll("form")).find(f=>f.elements.message); f.elements.message.value="hello reset"; f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); if(!(document.body.textContent.includes("Sent: hello reset")&&f.elements.message.value==="")) throw new Error("reset user input did not clear field"); return true;})()"#; diff --git a/work/slices/SLICE-0002-browser-runtime-confidence.md b/work/slices/SLICE-0002-browser-runtime-confidence.md index 89a8e2f..86bcfdd 100644 --- a/work/slices/SLICE-0002-browser-runtime-confidence.md +++ b/work/slices/SLICE-0002-browser-runtime-confidence.md @@ -13,7 +13,7 @@ refs: ## Objective -Keep `html-examples-smoke` as the repo-owned browser proof for no-reload dynamic interactions, revealed fallback, interval triggers, and the htmx-equivalent gallery behaviors. +Keep `html-examples-smoke` as the repo-owned browser proof for no-reload dynamic interactions, explicit progress clicks, revealed fallback, and the htmx-equivalent gallery behaviors. ## Authority @@ -21,7 +21,7 @@ Keep `html-examples-smoke` as the repo-owned browser proof for no-reload dynamic ## Close when -- The smoke covers click-to-edit, edit-row, inline validation, active search, delete row, lazy load, click-to-load, infinite/reveal, progress interval, value select, reset input, and revealed fallback without IntersectionObserver. +- The smoke covers click-to-edit, edit-row, inline validation, active search, delete row, lazy load, click-to-load, infinite/reveal, explicit progress click, value select, reset input, and revealed fallback without IntersectionObserver. - The no-navigation/no-reload guard wraps dynamic interactions. - Failures identify the named smoke path. @@ -37,4 +37,4 @@ Keep `html-examples-smoke` as the repo-owned browser proof for no-reload dynamic ## Evidence -Done at HEAD `67d167f`: `cargo run -p hemx-xtask -- html-examples-smoke` passed and printed all named smoke paths: progress interval, click-to-edit, edit-row, inline validation, active search, delete row, lazy load, click-to-load, infinite/reveal, value select, reset input, and revealed fallback without IntersectionObserver. `hemx-xtask/src/main.rs` owns the CDP-driven smoke command, inline smoke scripts, and no-navigation/no-reload guard; no `/tmp` scripts or second browser runner were added. +Done at HEAD `67d167f`, then corrected for manual progress behavior: `cargo run -p hemx-xtask -- html-examples-smoke` covers explicit progress click, click-to-edit, edit-row, inline validation, active search, delete row, lazy load, click-to-load, infinite/reveal, value select, reset input, and revealed fallback without IntersectionObserver. `hemx-xtask/src/main.rs` owns the CDP-driven smoke command, inline smoke scripts, and no-navigation/no-reload guard; no `/tmp` scripts or second browser runner were added.