feat(kanban): expose queued persistence state

req: accessibility/004\nreq: client_local/013\nreq: performance/003
This commit is contained in:
slhx agent
2026-07-13 15:40:34 +02:00
parent 9b80444484
commit 978bf67a2c
3 changed files with 149 additions and 3 deletions
+137
View File
@@ -528,6 +528,107 @@ async fn kanban_command_export_delete_and_reset_are_recoverable() -> WebDriverRe
result.and(quit)
}
#[tokio::test]
async fn kanban_queued_status_precedes_durable_projection_within_budget() -> WebDriverResult<()> {
// test req: accessibility/004 req: client_local/013 req: performance/003
const INTERACTION_BUDGET_MS: f64 = 100.0;
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root")
.to_owned();
let (package, bootstrap, rendered) = build_kanban_artifact(&workspace);
let runtime = workspace.join("hemx-js/runtime/hemx.js");
let server = StaticServer::start(
package,
runtime,
bootstrap,
rendered,
"kanban_client",
Some(kanban_app_assets(&workspace)),
);
let webdriver_port = available_port();
let webdriver_addr = format!("127.0.0.1:{webdriver_port}");
let mut webdriver = Command::new("geckodriver");
webdriver.arg("--port").arg(webdriver_port.to_string());
let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr);
let mut caps = DesiredCapabilities::firefox();
caps.set_headless()?;
let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?;
let result = async {
driver.goto(&server.url()).await?;
wait_until(
&driver,
"const root = document.querySelector('[data-hemx-root]'); return root.hasAttribute('data-kanban-command-ready') && root.hasAttribute('data-kanban-offline-ready')",
)
.await?;
hold_command_transaction(&driver).await?;
driver
.execute(
r#"
const root = document.querySelector('[data-hemx-root]');
window.__queuedTiming = { input: null, queued: null, persisted: false };
root.addEventListener('click', () => { window.__queuedTiming.input = performance.now(); }, { capture: true, once: true });
root.addEventListener('kanban:command-queued', () => { window.__queuedTiming.queued = performance.now(); }, { once: true });
root.addEventListener('kanban:command-persisted', () => { window.__queuedTiming.persisted = true; }, { once: true });
return true;
"#,
Vec::new(),
)
.await?;
driver.find(By::Css("[data-card-id='1']")).await?.click().await?;
wait_until(
&driver,
"return window.__queuedTiming.queued !== null && document.querySelector('[data-hemx-root]').getAttribute('data-kanban-command-phase') === 'queued'",
)
.await?;
let queued = driver
.execute(
"const root = document.querySelector('[data-hemx-root]'); return { latency: window.__queuedTiming.queued - window.__queuedTiming.input, persisted: window.__queuedTiming.persisted, phase: root.getAttribute('data-kanban-command-phase'), busy: root.getAttribute('aria-busy'), status: root.querySelector('[role=status]').textContent, order: [...root.querySelectorAll('[data-key]')].map((node) => node.dataset.key).join('|'), count: root.getAttribute('data-kanban-command-count') }",
Vec::new(),
)
.await?
.json()
.clone();
assert!(
queued["latency"]
.as_f64()
.is_some_and(|latency| latency <= INTERACTION_BUDGET_MS),
"queued status missed interaction budget: {queued}"
);
assert_eq!(queued["persisted"], false);
assert_eq!(queued["phase"], "queued");
assert_eq!(queued["busy"], "true");
assert_eq!(queued["status"], "Queued card 1; saving for offline use.");
assert_eq!(queued["order"], "1|2");
assert_eq!(queued["count"], "0");
release_command_transaction(&driver).await?;
wait_until(
&driver,
"const root = document.querySelector('[data-hemx-root]'); return window.__queuedTiming.persisted && root.getAttribute('data-kanban-command-phase') === 'durable' && [...root.querySelectorAll('[data-key]')].map((node) => node.dataset.key).join('|') === '2|1'",
)
.await?;
let durable = driver
.execute(
"const root = document.querySelector('[data-hemx-root]'); return { phase: root.getAttribute('data-kanban-command-phase'), busy: root.hasAttribute('aria-busy'), count: root.getAttribute('data-kanban-command-count'), status: root.querySelector('[role=status]').textContent }",
Vec::new(),
)
.await?
.json()
.clone();
assert_eq!(durable["phase"], "durable");
assert_eq!(durable["busy"], false);
assert_eq!(durable["count"], "1");
assert_eq!(durable["status"], "Moved 1 with click");
Ok(())
}
.await;
let quit = driver.quit().await;
result.and(quit)
}
#[tokio::test]
async fn kanban_quota_failure_is_fail_closed_and_recoverable() -> WebDriverResult<()> {
// test req: sync/015
@@ -1403,6 +1504,42 @@ fn fixture_html(rendered: &str, has_app_module: bool) -> String {
)
}
async fn hold_command_transaction(driver: &WebDriver) -> WebDriverResult<()> {
driver
.execute(
r#"
window.__releaseCommandTransaction = false;
window.__commandTransactionHeld = false;
const open = indexedDB.open('hemx-kanban-v1', 1);
open.onsuccess = () => {
const tx = open.result.transaction('commands', 'readwrite');
const commands = tx.objectStore('commands');
window.__commandTransactionHeld = true;
const keepAlive = () => {
if (window.__releaseCommandTransaction) return;
const request = commands.count();
request.onsuccess = keepAlive;
};
keepAlive();
};
return true;
"#,
Vec::new(),
)
.await?;
wait_until(driver, "return window.__commandTransactionHeld === true").await
}
async fn release_command_transaction(driver: &WebDriver) -> WebDriverResult<()> {
driver
.execute(
"window.__releaseCommandTransaction = true; return true;",
Vec::new(),
)
.await?;
Ok(())
}
async fn inject_quota_failure(driver: &WebDriver) -> WebDriverResult<()> {
let injected = driver
.execute(