feat(kanban): restore commands offline
req: local/001\nreq: local/002\nreq: local/003\nreq: local/004
This commit is contained in:
+68
-18
@@ -201,13 +201,13 @@ async fn kanban_command_persists_before_projection_and_restores_after_reload() -
|
||||
.to_owned();
|
||||
let (package, bootstrap, rendered) = build_kanban_artifact(&workspace);
|
||||
let runtime = workspace.join("hemx-js/runtime/hemx.js");
|
||||
let server = StaticServer::start(
|
||||
let mut server = StaticServer::start(
|
||||
package,
|
||||
runtime,
|
||||
bootstrap,
|
||||
rendered,
|
||||
"kanban_client",
|
||||
Some(workspace.join("examples/kanban/static/command-log.js")),
|
||||
Some(kanban_app_assets(&workspace)),
|
||||
);
|
||||
|
||||
let webdriver_port = available_port();
|
||||
@@ -222,7 +222,7 @@ async fn kanban_command_persists_before_projection_and_restores_after_reload() -
|
||||
driver.goto(&server.url()).await?;
|
||||
wait_until(
|
||||
&driver,
|
||||
"return document.querySelector('[data-hemx-root]').hasAttribute('data-kanban-command-ready')",
|
||||
"const root = document.querySelector('[data-hemx-root]'); return root.hasAttribute('data-kanban-command-ready') && root.hasAttribute('data-kanban-offline-ready')",
|
||||
)
|
||||
.await?;
|
||||
driver
|
||||
@@ -268,11 +268,26 @@ async fn kanban_command_persists_before_projection_and_restores_after_reload() -
|
||||
assert!(!session.is_empty());
|
||||
assert_eq!(persisted["persisted"]["detail"]["id"], format!("{actor}:1"));
|
||||
assert_eq!(persisted["count"], "1");
|
||||
let offline_shell = driver
|
||||
.execute_async(
|
||||
"const done = arguments[arguments.length - 1]; Promise.all([caches.keys(), caches.match('/')]).then(([keys, shell]) => done({ controlled: Boolean(navigator.serviceWorker.controller), keys, shell: Boolean(shell) })).catch((error) => done({ error: String(error) }))",
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.clone();
|
||||
assert_eq!(offline_shell["controlled"], true, "{offline_shell}");
|
||||
assert_eq!(offline_shell["shell"], true, "{offline_shell}");
|
||||
assert_eq!(offline_shell["keys"][0], "hemx-kanban-shell-v1");
|
||||
|
||||
driver.refresh().await?;
|
||||
server.stop();
|
||||
assert!(!server.is_reachable(), "fixture server must be unreachable");
|
||||
driver
|
||||
.execute("window.__reloadPending = true; location.reload()", Vec::new())
|
||||
.await?;
|
||||
wait_until(
|
||||
&driver,
|
||||
"return document.querySelector('[data-hemx-root]').hasAttribute('data-kanban-command-ready')",
|
||||
"return !window.__reloadPending && document.querySelector('[data-hemx-root]').hasAttribute('data-kanban-command-ready')",
|
||||
)
|
||||
.await?;
|
||||
let restored = driver
|
||||
@@ -307,10 +322,12 @@ async fn kanban_command_persists_before_projection_and_restores_after_reload() -
|
||||
)
|
||||
.await?;
|
||||
wait_until(&driver, "return window.__futureCommandStored === true").await?;
|
||||
driver.refresh().await?;
|
||||
driver
|
||||
.execute("window.__reloadPending = true; location.reload()", Vec::new())
|
||||
.await?;
|
||||
wait_until(
|
||||
&driver,
|
||||
"return document.querySelector('[data-hemx-root]').hasAttribute('data-kanban-command-error')",
|
||||
"return !window.__reloadPending && document.querySelector('[data-hemx-root]').hasAttribute('data-kanban-command-error')",
|
||||
)
|
||||
.await?;
|
||||
let rejected = driver
|
||||
@@ -350,7 +367,7 @@ async fn kanban_reorder_has_pointer_keyboard_focus_and_reduced_motion_parity() -
|
||||
bootstrap,
|
||||
rendered,
|
||||
"kanban_client",
|
||||
Some(workspace.join("examples/kanban/static/command-log.js")),
|
||||
Some(kanban_app_assets(&workspace)),
|
||||
);
|
||||
|
||||
let webdriver_port = available_port();
|
||||
@@ -592,9 +609,22 @@ fn newest_generated_bootstrap(build_dir: &Path, prefix: &str) -> PathBuf {
|
||||
.expect("generated hemx client bootstrap")
|
||||
}
|
||||
|
||||
struct AppAssets {
|
||||
module: PathBuf,
|
||||
service_worker: PathBuf,
|
||||
}
|
||||
|
||||
fn kanban_app_assets(workspace: &Path) -> AppAssets {
|
||||
AppAssets {
|
||||
module: workspace.join("examples/kanban/static/command-log.js"),
|
||||
service_worker: workspace.join("examples/kanban/static/offline.js"),
|
||||
}
|
||||
}
|
||||
|
||||
struct StaticServer {
|
||||
address: String,
|
||||
stop: Arc<AtomicBool>,
|
||||
thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl StaticServer {
|
||||
@@ -604,14 +634,14 @@ impl StaticServer {
|
||||
bootstrap: PathBuf,
|
||||
rendered: String,
|
||||
asset_stem: &'static str,
|
||||
app_module: Option<PathBuf>,
|
||||
app_assets: Option<AppAssets>,
|
||||
) -> Self {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind browser fixture");
|
||||
listener.set_nonblocking(true).expect("nonblocking fixture");
|
||||
let address = listener.local_addr().expect("fixture address").to_string();
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let thread_stop = Arc::clone(&stop);
|
||||
thread::spawn(move || {
|
||||
let thread = thread::spawn(move || {
|
||||
while !thread_stop.load(Ordering::Relaxed) {
|
||||
match listener.accept() {
|
||||
Ok((stream, _)) => serve(
|
||||
@@ -621,7 +651,7 @@ impl StaticServer {
|
||||
&bootstrap,
|
||||
&rendered,
|
||||
asset_stem,
|
||||
app_module.as_deref(),
|
||||
app_assets.as_ref(),
|
||||
),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(10))
|
||||
@@ -630,17 +660,32 @@ impl StaticServer {
|
||||
}
|
||||
}
|
||||
});
|
||||
Self { address, stop }
|
||||
Self {
|
||||
address,
|
||||
stop,
|
||||
thread: Some(thread),
|
||||
}
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
format!("http://{}", self.address)
|
||||
}
|
||||
|
||||
fn stop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
if let Some(thread) = self.thread.take() {
|
||||
thread.join().expect("stop browser fixture");
|
||||
}
|
||||
}
|
||||
|
||||
fn is_reachable(&self) -> bool {
|
||||
TcpStream::connect(&self.address).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StaticServer {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,7 +696,7 @@ fn serve(
|
||||
bootstrap: &Path,
|
||||
rendered: &str,
|
||||
asset_stem: &str,
|
||||
app_module: Option<&Path>,
|
||||
app_assets: Option<&AppAssets>,
|
||||
) {
|
||||
let mut request = [0_u8; 2048];
|
||||
let length = stream.read(&mut request).unwrap_or(0);
|
||||
@@ -660,7 +705,7 @@ fn serve(
|
||||
let (content_type, body) = match path {
|
||||
"/" => (
|
||||
"text/html; charset=utf-8",
|
||||
fixture_html(rendered, app_module.is_some()).into_bytes(),
|
||||
fixture_html(rendered, app_assets.is_some()).into_bytes(),
|
||||
),
|
||||
"/hemx.js" => (
|
||||
"text/javascript; charset=utf-8",
|
||||
@@ -678,16 +723,21 @@ fn serve(
|
||||
"text/javascript; charset=utf-8",
|
||||
fs::read(bootstrap).expect("read generated client bootstrap"),
|
||||
),
|
||||
"/app.js" if app_module.is_some() => (
|
||||
"/app.js" if app_assets.is_some() => (
|
||||
"text/javascript; charset=utf-8",
|
||||
fs::read(app_module.expect("checked app module")).expect("read app module"),
|
||||
fs::read(&app_assets.expect("checked app assets").module).expect("read app module"),
|
||||
),
|
||||
"/offline.js" if app_assets.is_some() => (
|
||||
"text/javascript; charset=utf-8",
|
||||
fs::read(&app_assets.expect("checked app assets").service_worker)
|
||||
.expect("read service worker"),
|
||||
),
|
||||
_ => ("text/plain", b"not found".to_vec()),
|
||||
};
|
||||
let status = if path == "/"
|
||||
|| path == "/hemx.js"
|
||||
|| path == "/hemx.client.js"
|
||||
|| (path == "/app.js" && app_module.is_some())
|
||||
|| ((path == "/app.js" || path == "/offline.js") && app_assets.is_some())
|
||||
|| path.starts_with(&format!("/{asset_stem}"))
|
||||
{
|
||||
"200 OK"
|
||||
|
||||
Reference in New Issue
Block a user