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,
- "",
- 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(""));
+ 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.