feat(host): wire techdemo host capability flow
Route browser/PWA share and native-shell haptic host results through techdemo app code before returning normal hemx effects; serve the tiny browser host adapter as an app asset. req: host/001 req: host/002 req: host/004 req: host/005 req: examples/001
This commit is contained in:
Generated
+1
@@ -573,6 +573,7 @@ dependencies = [
|
|||||||
"hemx",
|
"hemx",
|
||||||
"hemx-axum",
|
"hemx-axum",
|
||||||
"hemx-build",
|
"hemx-build",
|
||||||
|
"hemx-host",
|
||||||
"hemx-test",
|
"hemx-test",
|
||||||
"scraper",
|
"scraper",
|
||||||
"thirtyfour",
|
"thirtyfour",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ futures-util = "0.3"
|
|||||||
hemplate = { path = "../../../hemplate/hemplate" }
|
hemplate = { path = "../../../hemplate/hemplate" }
|
||||||
hemx = { path = "../../hemx" }
|
hemx = { path = "../../hemx" }
|
||||||
hemx-axum = { path = "../../hemx-axum" }
|
hemx-axum = { path = "../../hemx-axum" }
|
||||||
|
hemx-host = { path = "../../hemx-host" }
|
||||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -10,9 +10,16 @@ use hemx_axum::{
|
|||||||
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
|
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
|
||||||
EffectResponse, InteractionRequest, PageRequest,
|
EffectResponse, InteractionRequest, PageRequest,
|
||||||
};
|
};
|
||||||
|
use hemx_host::{
|
||||||
|
browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest,
|
||||||
|
CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent, SharePayload,
|
||||||
|
BROWSER_HOST_JS,
|
||||||
|
};
|
||||||
use hemx_techdemo::ui;
|
use hemx_techdemo::ui;
|
||||||
use hemx_techdemo::ui::control_center::{self as control, classes};
|
use hemx_techdemo::ui::control_center::{self as control, classes};
|
||||||
use hemx_techdemo::ui::{issue_card as card_control, issue_lane as lane_control};
|
use hemx_techdemo::ui::{
|
||||||
|
host_panel as host_control, issue_card as card_control, issue_lane as lane_control,
|
||||||
|
};
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
@@ -25,6 +32,7 @@ const LANES: [(&str, &str, &str); 3] = [
|
|||||||
("product", "Product", "Native UX, zero app JS"),
|
("product", "Product", "Native UX, zero app JS"),
|
||||||
];
|
];
|
||||||
const ISLAND_ORBIT: EventName = EventName::new("hemx:island-orbit");
|
const ISLAND_ORBIT: EventName = EventName::new("hemx:island-orbit");
|
||||||
|
const HOST_CALL: EventName = EventName::new("hemx:host-call");
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct WorkItem {
|
struct WorkItem {
|
||||||
@@ -67,6 +75,7 @@ struct DemoState {
|
|||||||
activity: VecDeque<String>,
|
activity: VecDeque<String>,
|
||||||
spotlight: String,
|
spotlight: String,
|
||||||
selected_id: Option<u64>,
|
selected_id: Option<u64>,
|
||||||
|
host_status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DemoState {
|
impl Default for DemoState {
|
||||||
@@ -99,6 +108,7 @@ impl Default for DemoState {
|
|||||||
activity: VecDeque::new(),
|
activity: VecDeque::new(),
|
||||||
spotlight: "No selectors. Generated resources address every target.".into(),
|
spotlight: "No selectors. Generated resources address every target.".into(),
|
||||||
selected_id: Some(2),
|
selected_id: Some(2),
|
||||||
|
host_status: "Host calls are typed facts until app code accepts a result.".into(),
|
||||||
};
|
};
|
||||||
state.log("Demo booted from server-rendered HTML");
|
state.log("Demo booted from server-rendered HTML");
|
||||||
state.log("Runtime attached one delegated listener per root");
|
state.log("Runtime attached one delegated listener per root");
|
||||||
@@ -151,6 +161,7 @@ struct ControlCenter {
|
|||||||
board: Html,
|
board: Html,
|
||||||
inspector: Html,
|
inspector: Html,
|
||||||
activity: Html,
|
activity: Html,
|
||||||
|
host: Html,
|
||||||
island_snapshot: String,
|
island_snapshot: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +203,13 @@ struct ActivityFeed {
|
|||||||
items: Vec<String>,
|
items: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Hemplate)]
|
||||||
|
#[hemplate = "partials"]
|
||||||
|
struct HostPanel {
|
||||||
|
status: String,
|
||||||
|
boundary: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Hemplate)]
|
#[derive(Hemplate)]
|
||||||
#[hemplate = "partials"]
|
#[hemplate = "partials"]
|
||||||
struct ArchitectureActivity;
|
struct ArchitectureActivity;
|
||||||
@@ -230,6 +248,7 @@ async fn main() {
|
|||||||
.route(runtime_js_path(), get(runtime))
|
.route(runtime_js_path(), get(runtime))
|
||||||
.route("/app.css", get(app_css))
|
.route("/app.css", get(app_css))
|
||||||
.route("/control_center.css", get(control_center_css))
|
.route("/control_center.css", get(control_center_css))
|
||||||
|
.route("/hemx-browser-host.js", get(browser_host_js))
|
||||||
.route("/island.js", get(island_js))
|
.route("/island.js", get(island_js))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -258,6 +277,7 @@ async fn architecture(request: PageRequest) -> impl IntoResponse {
|
|||||||
board: architecture_board(),
|
board: architecture_board(),
|
||||||
inspector: architecture_inspector(),
|
inspector: architecture_inspector(),
|
||||||
activity: architecture_activity(),
|
activity: architecture_activity(),
|
||||||
|
host: render_host_panel(&DemoState::default()),
|
||||||
island_snapshot: "2|3|21|architecture route · same opaque island bridge".to_owned(),
|
island_snapshot: "2|3|21|architecture route · same opaque island bridge".to_owned(),
|
||||||
});
|
});
|
||||||
request
|
request
|
||||||
@@ -288,6 +308,14 @@ async fn control_center_css() -> impl IntoResponse {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn browser_host_js() -> impl IntoResponse {
|
||||||
|
// req: host/001 req: host/002 req: host/005
|
||||||
|
(
|
||||||
|
[("content-type", "application/javascript; charset=utf-8")],
|
||||||
|
BROWSER_HOST_JS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn island_js() -> impl IntoResponse {
|
async fn island_js() -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
[("content-type", "text/javascript; charset=utf-8")],
|
[("content-type", "text/javascript; charset=utf-8")],
|
||||||
@@ -324,6 +352,72 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
|
|||||||
sse(batches)
|
sse(batches)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HostAppCommand {
|
||||||
|
BrowserShareCompleted,
|
||||||
|
NativeHapticAcknowledged,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browser_manifest() -> CapabilityManifest {
|
||||||
|
CapabilityManifest::new([CapabilityUse::new(
|
||||||
|
Capability::Share,
|
||||||
|
CapabilityShape::Request,
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn native_manifest() -> CapabilityManifest {
|
||||||
|
CapabilityManifest::new([CapabilityUse::new(
|
||||||
|
Capability::Haptics,
|
||||||
|
CapabilityShape::Fire,
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browser_share_call() -> HostCall {
|
||||||
|
HostCall::Share {
|
||||||
|
id: HostCallId::new("browser-share-1"),
|
||||||
|
payload: SharePayload::text("hemx host capability demo"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn native_haptic_call() -> HostCall {
|
||||||
|
HostCall::Haptic {
|
||||||
|
id: HostCallId::new("native-haptic-tap"),
|
||||||
|
pattern: HapticPattern::Success,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_event_to_command(event: HostEvent) -> Option<HostAppCommand> {
|
||||||
|
match event {
|
||||||
|
HostEvent::ShareCompleted {
|
||||||
|
completed: true, ..
|
||||||
|
} => Some(HostAppCommand::BrowserShareCompleted),
|
||||||
|
HostEvent::Acknowledged { id } if id.0 == "native-haptic-tap" => {
|
||||||
|
Some(HostAppCommand::NativeHapticAcknowledged)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_host_event(demo: &mut DemoState, event: HostEvent) {
|
||||||
|
// req: host/002 req: host/005
|
||||||
|
match host_event_to_command(event) {
|
||||||
|
Some(HostAppCommand::BrowserShareCompleted) => {
|
||||||
|
demo.host_status =
|
||||||
|
"Browser/PWA HostEvent became an app command before UI effects.".into();
|
||||||
|
demo.log("Browser share completed through app host pipeline");
|
||||||
|
}
|
||||||
|
Some(HostAppCommand::NativeHapticAcknowledged) => {
|
||||||
|
demo.host_status =
|
||||||
|
"Native-shell HostEvent became an app command before UI effects.".into();
|
||||||
|
demo.log("Native haptic acknowledgment accepted by app code");
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
demo.host_status =
|
||||||
|
"HostEvent ignored by app policy; no domain change was appended.".into();
|
||||||
|
demo.log("Ignored host event by app policy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
|
fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
|
||||||
interactions(ui::BUILD_FINGERPRINT)
|
interactions(ui::BUILD_FINGERPRINT)
|
||||||
.on(control::launch_work, {
|
.on(control::launch_work, {
|
||||||
@@ -419,6 +513,74 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
|
|||||||
demo_effects(&demo, "Inspector focused")
|
demo_effects(&demo, "Inspector focused")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.on(host_control::request_browser_share, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |_| {
|
||||||
|
// req: host/001 req: host/004 req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
let call = browser_share_call();
|
||||||
|
match browser_manifest().validate_call(&browser_pwa_host_profile(), &call) {
|
||||||
|
Ok(()) => {
|
||||||
|
demo.host_status = "Browser/PWA share request accepted; waiting for HostEvent from the host adapter.".into();
|
||||||
|
demo.log("Requested browser/PWA share through hemx-host");
|
||||||
|
(demo_effects(&demo, "Browser host call requested"), HOST_CALL.emit("browser-share-1"))
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
demo.host_status = format!("Browser/PWA host check failed: {error}");
|
||||||
|
(demo_effects(&demo, "Browser host check failed"), HOST_CALL.emit("browser-share-failed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on(host_control::record_browser_share, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |form| {
|
||||||
|
// req: host/002 req: host/005 req: examples/001
|
||||||
|
let completed = form.parse::<bool>("completed").unwrap_or(true);
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
apply_host_event(
|
||||||
|
&mut demo,
|
||||||
|
HostEvent::ShareCompleted {
|
||||||
|
id: HostCallId::new("browser-share-1"),
|
||||||
|
completed,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
demo_effects(&demo, "Browser host result accepted by app code")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on(host_control::request_native_haptic, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |_| {
|
||||||
|
// req: host/001 req: host/004 req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
let call = native_haptic_call();
|
||||||
|
match native_manifest().validate_call(&native_shell_host_profile("ios-android-webview"), &call) {
|
||||||
|
Ok(()) => {
|
||||||
|
demo.host_status = "Native-shell haptic request accepted; waiting for host acknowledgment.".into();
|
||||||
|
demo.log("Requested native-shell haptic through hemx-host");
|
||||||
|
(demo_effects(&demo, "Native host call requested"), HOST_CALL.emit("native-haptic-tap"))
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
demo.host_status = format!("Native host check failed: {error}");
|
||||||
|
(demo_effects(&demo, "Native host check failed"), HOST_CALL.emit("native-haptic-failed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on(host_control::record_native_haptic_ack, {
|
||||||
|
let shared = shared.clone();
|
||||||
|
move |_| {
|
||||||
|
// req: host/002 req: host/005 req: examples/001
|
||||||
|
let mut demo = shared.demo.lock().unwrap();
|
||||||
|
apply_host_event(
|
||||||
|
&mut demo,
|
||||||
|
HostEvent::Acknowledged {
|
||||||
|
id: HostCallId::new("native-haptic-tap"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
demo_effects(&demo, "Native host result accepted by app code")
|
||||||
|
}
|
||||||
|
})
|
||||||
.on(control::simulate_push, {
|
.on(control::simulate_push, {
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
@@ -464,6 +626,7 @@ fn demo_effects(demo: &DemoState, notice: &'static str) -> impl IntoEffect {
|
|||||||
control::board.put(&board_view(demo)),
|
control::board.put(&board_view(demo)),
|
||||||
control::activity.put(&activity_view(demo)),
|
control::activity.put(&activity_view(demo)),
|
||||||
control::inspector.put(&inspector_view(demo)),
|
control::inspector.put(&inspector_view(demo)),
|
||||||
|
control::host_panel.put(&host_panel(demo)),
|
||||||
control::notice.text(notice),
|
control::notice.text(notice),
|
||||||
control::launch_work_form.clear(),
|
control::launch_work_form.clear(),
|
||||||
ISLAND_ORBIT.emit(island_snapshot(demo)),
|
ISLAND_ORBIT.emit(island_snapshot(demo)),
|
||||||
@@ -510,6 +673,7 @@ fn page_html(demo: &DemoState) -> Html {
|
|||||||
board: ui::render(&board_view(demo)),
|
board: ui::render(&board_view(demo)),
|
||||||
inspector: render_inspector(demo),
|
inspector: render_inspector(demo),
|
||||||
activity: render_activity(demo),
|
activity: render_activity(demo),
|
||||||
|
host: render_host_panel(demo),
|
||||||
island_snapshot: island_snapshot(demo),
|
island_snapshot: island_snapshot(demo),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -536,7 +700,7 @@ fn hero_view(demo: &DemoState) -> HeroMetrics {
|
|||||||
.count();
|
.count();
|
||||||
let impact: u64 = demo.work.iter().map(|item| item.impact as u64).sum();
|
let impact: u64 = demo.work.iter().map(|item| item.impact as u64).sum();
|
||||||
HeroMetrics {
|
HeroMetrics {
|
||||||
resource_count: 13,
|
resource_count: 18,
|
||||||
active_count: active,
|
active_count: active,
|
||||||
shipped_count: shipped,
|
shipped_count: shipped,
|
||||||
impact_score: impact,
|
impact_score: impact,
|
||||||
@@ -592,6 +756,19 @@ fn render_activity(demo: &DemoState) -> Html {
|
|||||||
ui::render(&activity_view(demo))
|
ui::render(&activity_view(demo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn host_panel(demo: &DemoState) -> HostPanel {
|
||||||
|
// req: host/001 req: host/002 req: host/005
|
||||||
|
HostPanel {
|
||||||
|
status: demo.host_status.clone(),
|
||||||
|
boundary: "HostCall → HostEvent → app command → EffectBatch",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_host_panel(demo: &DemoState) -> Html {
|
||||||
|
// req: host/001 req: host/002 req: host/005
|
||||||
|
ui::render(&host_panel(demo))
|
||||||
|
}
|
||||||
|
|
||||||
fn inspector_view(demo: &DemoState) -> InspectorPanel {
|
fn inspector_view(demo: &DemoState) -> InspectorPanel {
|
||||||
// req: html_safety/002 req: view/001
|
// req: html_safety/002 req: view/001
|
||||||
let selected = demo
|
let selected = demo
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>hemx Techdemo</title>
|
<title>hemx Techdemo</title>
|
||||||
<script +src="self.runtime_src" defer></script>
|
<script +src="self.runtime_src" defer></script>
|
||||||
|
<script src="/hemx-browser-host.js" defer></script>
|
||||||
<script src="/island.js" defer></script>
|
<script src="/island.js" defer></script>
|
||||||
<link rel="stylesheet" href="/app.css">
|
<link rel="stylesheet" href="/app.css">
|
||||||
<link rel="stylesheet" href="/control_center.css">
|
<link rel="stylesheet" href="/control_center.css">
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<button type="button" data-hemx-handle="simulate_push">Simulate server push</button>
|
<button type="button" data-hemx-handle="simulate_push">Simulate server push</button>
|
||||||
<button type="button" data-hemx-handle="reset_demo">Reset demo</button>
|
<button type="button" data-hemx-handle="reset_demo">Reset demo</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div data-hemx-slot="host_panel">{+= self.host =+}</div>
|
||||||
<p data-hemx-slot="notice" class="notice">Every control posts through a generated handle and receives typed updates.</p>
|
<p data-hemx-slot="notice" class="notice">Every control posts through a generated handle and receives typed updates.</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="host-panel">
|
||||||
|
<p><strong>Typed host boundary</strong><br>{+ self.status +}</p>
|
||||||
|
<div class="quick-actions">
|
||||||
|
<button type="button" data-hemx-handle="request_browser_share">Request browser/PWA share</button>
|
||||||
|
<button type="button" data-hemx-handle="record_browser_share" name="completed" value="true">Record browser share result</button>
|
||||||
|
<button type="button" data-hemx-handle="request_native_haptic">Request native haptic</button>
|
||||||
|
<button type="button" data-hemx-handle="record_native_haptic_ack">Record native haptic ack</button>
|
||||||
|
</div>
|
||||||
|
<code>{+ self.boundary +}</code>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
use hemx_axum::runtime_js_path;
|
use hemx_axum::runtime_js_path;
|
||||||
use hemx_techdemo::ui::control_center::{self as control, launch_work, reset_demo, simulate_push};
|
use hemx_techdemo::ui::control_center::{self as control, launch_work, reset_demo, simulate_push};
|
||||||
|
use hemx_techdemo::ui::host_panel::{
|
||||||
|
record_browser_share, record_native_haptic_ack, request_browser_share, request_native_haptic,
|
||||||
|
};
|
||||||
use hemx_techdemo::ui::issue_card::{advance_work, delete_work, spotlight_work};
|
use hemx_techdemo::ui::issue_card::{advance_work, delete_work, spotlight_work};
|
||||||
use hemx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle;
|
use hemx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle;
|
||||||
use hemx_techdemo::ui::BUILD_FINGERPRINT;
|
use hemx_techdemo::ui::BUILD_FINGERPRINT;
|
||||||
@@ -256,6 +259,53 @@ fn product_is_e2e_working_over_http() {
|
|||||||
);
|
);
|
||||||
assert_emit(&push_batch, &island_event_name("orbit"), "activity rows");
|
assert_emit(&push_batch, &island_event_name("orbit"), "activity rows");
|
||||||
|
|
||||||
|
let browser_host_request = post("/", &handle_form_body(request_browser_share, &[]));
|
||||||
|
assert_effect_response(&browser_host_request);
|
||||||
|
let browser_request_batch = browser_host_request.effects();
|
||||||
|
assert_payload_contains(&browser_request_batch, "Browser host call requested");
|
||||||
|
assert_payload_contains(
|
||||||
|
&browser_request_batch,
|
||||||
|
"Browser/PWA share request accepted; waiting for HostEvent",
|
||||||
|
);
|
||||||
|
assert_emit(&browser_request_batch, "hemx:host-call", "browser-share-1");
|
||||||
|
|
||||||
|
let browser_host_result = post(
|
||||||
|
"/",
|
||||||
|
&handle_form_body(record_browser_share, &[("completed", "true")]),
|
||||||
|
);
|
||||||
|
assert_effect_response(&browser_host_result);
|
||||||
|
let browser_result_batch = browser_host_result.effects();
|
||||||
|
assert_payload_contains(
|
||||||
|
&browser_result_batch,
|
||||||
|
"Browser/PWA HostEvent became an app command before UI effects.",
|
||||||
|
);
|
||||||
|
assert_payload_contains(
|
||||||
|
&browser_result_batch,
|
||||||
|
"Browser share completed through app host pipeline",
|
||||||
|
);
|
||||||
|
|
||||||
|
let native_host_request = post("/", &handle_form_body(request_native_haptic, &[]));
|
||||||
|
assert_effect_response(&native_host_request);
|
||||||
|
let native_request_batch = native_host_request.effects();
|
||||||
|
assert_payload_contains(&native_request_batch, "Native host call requested");
|
||||||
|
assert_payload_contains(
|
||||||
|
&native_request_batch,
|
||||||
|
"Native-shell haptic request accepted; waiting for host acknowledgment.",
|
||||||
|
);
|
||||||
|
assert_emit(&native_request_batch, "hemx:host-call", "native-haptic-tap");
|
||||||
|
|
||||||
|
let native_host_result = post("/", &handle_form_body(record_native_haptic_ack, &[]));
|
||||||
|
assert_effect_response(&native_host_result);
|
||||||
|
let native_result_batch = native_host_result.effects();
|
||||||
|
assert_payload_contains(
|
||||||
|
&native_result_batch,
|
||||||
|
"Native-shell HostEvent became an app command before UI effects.",
|
||||||
|
);
|
||||||
|
assert_payload_contains(
|
||||||
|
&native_result_batch,
|
||||||
|
"Native haptic acknowledgment accepted by app code",
|
||||||
|
);
|
||||||
|
|
||||||
let delete_missing = post("/", &handle_form_body(delete_work, &[("work_id", "999")]));
|
let delete_missing = post("/", &handle_form_body(delete_work, &[("work_id", "999")]));
|
||||||
assert_effect_response(&delete_missing);
|
assert_effect_response(&delete_missing);
|
||||||
let delete_missing_batch = delete_missing.effects();
|
let delete_missing_batch = delete_missing.effects();
|
||||||
|
|||||||
Reference in New Issue
Block a user