feat(kanban): expose queued persistence state
req: accessibility/004\nreq: client_local/013\nreq: performance/003
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user