| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| use super::Application; | |
| use super::{support, timer}; | |
| use crate::support::scene::Node; | |
| use maka_plugin_core::{ | |
| composition::PresentationTarget, | |
| contract::Contract, | |
| package::{EntryTarget, Implementation}, | |
| }; | |
| use maka_ui_runtime::{ | |
| ComponentCall, ComponentOutput, Frame, NativeCapability, NativeComponent, NativeExport, | |
| NativeRegistry, NativeView, Surface, | |
| }; | |
| use serde_json::{Value, json}; | |
| use std::{cell::Cell, collections::BTreeMap, rc::Rc}; | |
| struct Counter { | |
| calls: Rc<Cell<usize>>, | |
| } | |
| impl NativeComponent<String> for Counter { | |
| fn start( | |
| &self, | |
| call: ComponentCall<'_>, | |
| previous: Option<&NativeView<String>>, | |
| values: &maka_ui_runtime::abi::ValueBindings, | |
| _: &tokio::runtime::Handle, | |
| ) -> maka_ui_runtime::backend::Job<ComponentOutput<NativeView<String>>> { | |
| match &call { | |
| ComponentCall::Mount { .. } => assert!(previous.is_none()), | |
| ComponentCall::Update { state, .. } | ComponentCall::Event { state, .. } => { | |
| assert_eq!( | |
| previous.unwrap().view.as_str(), | |
| format!("{}={}", state["label"].as_str().unwrap(), state["count"]) | |
| ); | |
| } | |
| } | |
| maka_ui_runtime::backend::Job::ready(self.call(call, values)) | |
| } | |
| fn call( | |
| &self, | |
| call: ComponentCall<'_>, | |
| _values: &maka_ui_runtime::abi::ValueBindings, | |
| ) -> maka_ui_runtime::Result<ComponentOutput<NativeView<String>>> { | |
| self.calls.set(self.calls.get() + 1); | |
| let state = match call { | |
| ComponentCall::Mount { props } => { | |
| json!({"count":props["initial"],"label":props["label"],"revision":0}) | |
| } | |
| ComponentCall::Update { state, props } => { | |
| if props["label"] == "Right 1" { | |
| return Err(maka_ui_runtime::Error::Invalid( | |
| "reject later sibling".into(), | |
| )); | |
| } | |
| json!({"count":state["count"],"label":props["label"],"revision":state["revision"].as_u64().unwrap()+1}) | |
| } | |
| ComponentCall::Event { state, input, .. } => { | |
| json!({"count":state["count"].as_i64().unwrap()+input["delta"].as_i64().unwrap(),"label":state["label"],"revision":state["revision"].as_u64().unwrap()+1}) | |
| } | |
| }; | |
| Ok(Frame { | |
| revision: state["revision"].as_u64().unwrap(), | |
| surface: None, | |
| view: NativeView::new( | |
| Rc::new(format!( | |
| "{}={}", | |
| state["label"].as_str().unwrap(), | |
| state["count"] | |
| )), | |
| BTreeMap::from([( | |
| "inc".into(), | |
| json!({"type":"object","properties":{"delta":{"type":"integer","minimum":-10,"maximum":10},"large":{"type":"string"}},"required":["delta"],"additionalProperties":false}), | |
| )]), vec![])?, | |
| state, | |
| }.output(Vec::new())) | |
| } | |
| } | |
| fn registered(contract: Contract, calls: Rc<Cell<usize>>) -> NativeRegistry<String> { | |
| let mut registry = crate::support::scene::registry(PresentationTarget::Tui); | |
| registry | |
| .register("test.counter", move |_, _values| { | |
| Ok(BTreeMap::from([( | |
| "Counter".into(), | |
| NativeExport { | |
| contract: contract.clone(), | |
| value: NativeCapability::Component(Rc::new(Counter { | |
| calls: calls.clone(), | |
| })), | |
| }, | |
| )])) | |
| }) | |
| .unwrap(); | |
| registry | |
| } | |
| fn fixture() -> ( | |
| maka_plugin_core::composition::Input, | |
| Vec<maka_packages::Package>, | |
| Contract, | |
| ) { | |
| let (mut input, mut packages) = | |
| support::composed_fixture(support::CHILD_UPDATE, support::PARENT_EVENT); | |
| let old = packages.pop().unwrap(); | |
| let mut declaration = old | |
| .manifest() | |
| .application | |
| .as_ref() | |
| .unwrap() | |
| .declaration() | |
| .clone(); | |
| let entry = declaration.entries.get_mut(&EntryTarget::Tui).unwrap(); | |
| let contract = entry.exports["Counter"].contract.clone(); | |
| entry.scene = None; | |
| entry.implementation = Implementation::Native { | |
| implementation: "test.counter".into(), | |
| }; | |
| let counter = support::package("native.counter", declaration, None); | |
| input.instances.get_mut("counter").unwrap().package = counter.application_ref().into(); | |
| packages.push(counter); | |
| (input, packages, contract) | |
| } | |
| fn close_all<V: ?Sized + 'static>( | |
| app: &mut Application<V>, | |
| root: &maka_plugin_core::runtime::Mount, | |
| ) { | |
| app.retire(root).unwrap(); | |
| for mount in app.retiring_mounts() { | |
| app.close(&mount, Ok(())).unwrap(); | |
| } | |
| app.shutdown().unwrap(); | |
| } | |
| #[test] | |
| fn opaque_native_children_share_transaction_identity_and_input_admission_with_js() { | |
| let timer = timer(); | |
| let (input, packages, contract) = fixture(); | |
| let calls = Rc::new(Cell::new(0)); | |
| let mut app = Application::new( | |
| &input, | |
| packages, | |
| timer.handle().clone(), | |
| maka_ui_js::execution(Default::default()), | |
| registered(contract, calls.clone()), | |
| Default::default(), | |
| ) | |
| .unwrap(); | |
| let root = app.mount_launch(json!({})).unwrap(); | |
| let left = app.child_mount(&root, "left").unwrap().clone(); | |
| let right = app.child_mount(&root, "right").unwrap().clone(); | |
| let count = calls.get(); | |
| assert!( | |
| app.dispatch(&left, 0, "missing", json!({"delta":1})) | |
| .is_err() | |
| ); | |
| assert!(app.dispatch(&left, 0, "inc", json!({"delta":11})).is_err()); | |
| assert_eq!( | |
| calls.get(), | |
| count, | |
| "invalid raw inputs cannot enter the native callback" | |
| ); | |
| app.dispatch(&left, 0, "inc", json!({"delta":2})).unwrap(); | |
| let view = &app.frame(&left).unwrap().view; | |
| assert_eq!(view.view.as_str(), "Left 0=12"); | |
| assert_eq!(app.frame(&right).unwrap().state["count"], 20); | |
| assert!(app.dispatch(&left, 0, "inc", json!({"delta":1})).is_err()); | |
| // The first sibling prepares successfully, but a later native rejection | |
| // must leave the root and both previously published scenes untouched. | |
| assert!(app.dispatch(&root, 0, "parent", Value::Null).is_err()); | |
| assert_eq!(app.frame(&root).unwrap().revision, 0); | |
| assert_eq!(app.frame(&left).unwrap().revision, 1); | |
| assert_eq!(app.frame(&left).unwrap().state["label"], "Left 0"); | |
| assert_eq!(app.frame(&right).unwrap().revision, 0); | |
| assert!(!app.is_retiring(&left)); | |
| app.dispatch(&root, 0, "toggle", Value::Null).unwrap(); | |
| app.close(&right, Ok(())).unwrap(); | |
| // With the rejecting sibling gone, props update retains the first native | |
| // child's identity and independently accumulated local state. | |
| app.dispatch(&root, 1, "parent", Value::Null).unwrap(); | |
| assert_eq!(app.child_mount(&root, "left").unwrap(), &left); | |
| assert_eq!(app.frame(&left).unwrap().state["count"], 12); | |
| assert_eq!(app.frame(&left).unwrap().state["label"], "Left 1"); | |
| app.dispatch(&root, 2, "toggle", Value::Null).unwrap(); | |
| let replacement = app.child_mount(&root, "right").unwrap(); | |
| assert_ne!(replacement, &right); | |
| assert_eq!(app.frame(replacement).unwrap().state["count"], 20); | |
| assert!(app.dispatch(&right, 0, "inc", json!({"delta":1})).is_err()); | |
| close_all(&mut app, &root); | |
| } | |
| struct DelayedCounter { | |
| counter: Counter, | |
| release: Rc<std::cell::RefCell<Option<tokio::sync::oneshot::Receiver<()>>>>, | |
| } | |
| impl NativeComponent<String> for DelayedCounter { | |
| fn call( | |
| &self, | |
| call: ComponentCall<'_>, | |
| values: &maka_ui_runtime::abi::ValueBindings, | |
| ) -> maka_ui_runtime::Result<ComponentOutput<NativeView<String>>> { | |
| self.counter.call(call, values) | |
| } | |
| fn start( | |
| &self, | |
| call: ComponentCall<'_>, | |
| _: Option<&NativeView<String>>, | |
| values: &maka_ui_runtime::abi::ValueBindings, | |
| _: &tokio::runtime::Handle, | |
| ) -> maka_ui_runtime::backend::Job<ComponentOutput<NativeView<String>>> { | |
| let release = if matches!(&call, ComponentCall::Event { .. }) { | |
| self.release.borrow_mut().take() | |
| } else { | |
| None | |
| }; | |
| let output = self.call(call, values); | |
| maka_ui_runtime::backend::Job::new(async move { | |
| if let Some(release) = release { | |
| release.await.unwrap(); | |
| } | |
| output | |
| }) | |
| } | |
| } | |
| #[test] | |
| fn waiting_for_native_work_does_not_expire_the_composed_update() { | |
| let timer = timer(); | |
| let (input, packages, contract) = fixture(); | |
| let (release, waiting) = tokio::sync::oneshot::channel(); | |
| let waiting = Rc::new(std::cell::RefCell::new(Some(waiting))); | |
| let mut registry = crate::support::scene::registry(PresentationTarget::Tui); | |
| registry | |
| .register("test.counter", move |_, _| { | |
| Ok(BTreeMap::from([( | |
| "Counter".into(), | |
| NativeExport { | |
| contract: contract.clone(), | |
| value: NativeCapability::Component(Rc::new(DelayedCounter { | |
| counter: Counter { | |
| calls: Rc::new(Cell::new(0)), | |
| }, | |
| release: waiting.clone(), | |
| })), | |
| }, | |
| )])) | |
| }) | |
| .unwrap(); | |
| let slice = std::time::Duration::from_millis(100); | |
| let mut app = Application::new( | |
| &input, | |
| packages, | |
| timer.handle().clone(), | |
| maka_ui_js::execution(maka_js_runtime::plugin::Limits { | |
| synchronous_slice: slice, | |
| ..Default::default() | |
| }), | |
| registry, | |
| Default::default(), | |
| ) | |
| .unwrap(); | |
| let root = app.mount_launch(json!({})).unwrap(); | |
| let left = app.child_mount(&root, "left").unwrap().clone(); | |
| maka_ui_runtime::Application::dispatch(&mut app, &left, 0, "inc", json!({"delta":2})).unwrap(); | |
| assert!( | |
| timer | |
| .block_on(async { tokio::time::timeout(slice * 2, app.wait_idle()).await }) | |
| .is_err() | |
| ); | |
| assert_eq!(app.frame(&left).unwrap().state["count"], 10); | |
| release.send(()).unwrap(); | |
| timer.block_on(app.wait_idle()).unwrap(); | |
| assert_eq!(app.frame(&left).unwrap().state["count"], 12); | |
| close_all(&mut app, &root); | |
| } | |
| #[test] | |
| fn native_availability_is_not_activation_and_actual_contract_or_value_mismatches_fail() { | |
| let timer = timer(); | |
| for failure in 0..4 { | |
| let (input, packages, mut contract) = fixture(); | |
| let mut registry = crate::support::scene::registry::<String>(PresentationTarget::Tui); | |
| if failure != 0 { | |
| if failure == 1 { | |
| contract.version += 1; | |
| } | |
| registry | |
| .register("test.counter", move |_, _values| { | |
| Ok(BTreeMap::from([( | |
| "Counter".into(), | |
| NativeExport { | |
| contract: contract.clone(), | |
| value: if failure == 2 { | |
| NativeCapability::Surface(PresentationTarget::Tui) | |
| } else { | |
| NativeCapability::Component(Rc::new(Counter { | |
| calls: Rc::new(Cell::new(0)), | |
| })) | |
| }, | |
| }, | |
| )])) | |
| }) | |
| .unwrap(); | |
| if failure == 3 { | |
| // Extra implementation availability must remain inert. | |
| registry | |
| .register("unused", |_, _values| { | |
| panic!("unselected native factory activated") | |
| }) | |
| .unwrap(); | |
| } | |
| } | |
| let result = Application::new( | |
| &input, | |
| packages, | |
| timer.handle().clone(), | |
| maka_ui_js::execution(Default::default()), | |
| registry, | |
| Default::default(), | |
| ); | |
| if failure == 3 { | |
| let mut app = result.unwrap(); | |
| let root = app.mount_launch(json!({})).unwrap(); | |
| close_all(&mut app, &root); | |
| } else { | |
| assert!(result.is_err()); | |
| } | |
| } | |
| // The example imports the label explicitly; the surface factory alone | |
| // cannot stand in for a declared native component implementation. | |
| let (input, packages) = support::editor_fixture(); | |
| assert!( | |
| Application::new( | |
| &input, | |
| packages, | |
| timer.handle().clone(), | |
| maka_ui_js::execution(Default::default()), | |
| crate::support::scene::registry::<String>(PresentationTarget::Tui), | |
| Default::default() | |
| ) | |
| .is_err() | |
| ); | |
| } | |
| struct Root; | |
| impl NativeComponent<String> for Root { | |
| fn call( | |
| &self, | |
| call: ComponentCall<'_>, | |
| _values: &maka_ui_runtime::abi::ValueBindings, | |
| ) -> maka_ui_runtime::Result<ComponentOutput<NativeView<String>>> { | |
| assert!(matches!(call, ComponentCall::Mount { .. })); | |
| Ok(Frame { | |
| revision: 0, | |
| state: json!({}), | |
| surface: Some(Surface::Terminal { | |
| binding: "surface".into(), | |
| }), | |
| view: crate::support::scene::view(Node::Component(maka_ui_runtime::ComponentSlot { | |
| key: "child".into(), | |
| binding: "counter".into(), | |
| member: 0, | |
| props: json!({"label":"Native","initial":4}), | |
| on: BTreeMap::new(), | |
| }))?, | |
| } | |
| .output(Vec::new())) | |
| } | |
| } | |
| #[test] | |
| fn native_root_uses_explicit_surface_and_can_compose_an_imported_component() { | |
| let timer = timer(); | |
| let (mut input, mut packages, contract) = fixture(); | |
| let index = packages | |
| .iter() | |
| .position(|p| p.manifest().id == "dynamic.app") | |
| .unwrap(); | |
| let old = packages.remove(index); | |
| let mut declaration = old | |
| .manifest() | |
| .application | |
| .as_ref() | |
| .unwrap() | |
| .declaration() | |
| .clone(); | |
| let entry = declaration.entries.get_mut(&EntryTarget::Tui).unwrap(); | |
| entry.scene = None; | |
| entry.implementation = Implementation::Native { | |
| implementation: "test.root".into(), | |
| }; | |
| let root_contract = entry.exports["App"].contract.clone(); | |
| let root = support::package("native.root", declaration, None); | |
| input.instances.get_mut("app").unwrap().package = root.application_ref().into(); | |
| packages.push(root); | |
| let mut registry = registered(contract, Rc::new(Cell::new(0))); | |
| registry | |
| .register("test.root", move |_, _values| { | |
| Ok(BTreeMap::from([( | |
| "App".into(), | |
| NativeExport { | |
| contract: root_contract.clone(), | |
| value: NativeCapability::Component(Rc::new(Root)), | |
| }, | |
| )])) | |
| }) | |
| .unwrap(); | |
| let mut app = Application::new( | |
| &input, | |
| packages, | |
| timer.handle().clone(), | |
| maka_ui_js::execution(Default::default()), | |
| registry, | |
| Default::default(), | |
| ) | |
| .unwrap(); | |
| let root = app.mount_launch(json!({})).unwrap(); | |
| let child = app.child_mount(&root, "child").unwrap().clone(); | |
| assert_eq!( | |
| app.frame(&root).unwrap().surface, | |
| Some(Surface::Terminal { | |
| binding: "surface".into() | |
| }) | |
| ); | |
| app.dispatch(&child, 0, "inc", json!({"delta":3})).unwrap(); | |
| assert_eq!(app.frame(&child).unwrap().state["count"], 7); | |
| close_all(&mut app, &root); | |
| } | |
| struct SlotParent; | |
| impl NativeComponent<String> for SlotParent { | |
| fn call( | |
| &self, | |
| call: ComponentCall<'_>, | |
| _values: &maka_ui_runtime::abi::ValueBindings, | |
| ) -> maka_ui_runtime::Result<ComponentOutput<NativeView<String>>> { | |
| let mut next = match call { | |
| ComponentCall::Mount { .. } => { | |
| json!({"revision":0,"version":0,"right":true,"native":true,"seen":[]}) | |
| } | |
| ComponentCall::Update { state, .. } | ComponentCall::Event { state, .. } => { | |
| state.clone() | |
| } | |
| }; | |
| if let ComponentCall::Event { | |
| key, input, origin, .. | |
| } = call | |
| { | |
| next["revision"] = json!(next["revision"].as_u64().unwrap() + 1); | |
| match key { | |
| "notice" => { | |
| let maka_ui_runtime::EventOrigin::Component { child, name } = origin else { | |
| panic!("kernel child origin") | |
| }; | |
| next["seen"] | |
| .as_array_mut() | |
| .unwrap() | |
| .push(json!([child, name, input])); | |
| } | |
| "parent" => next["version"] = json!(next["version"].as_u64().unwrap() + 1), | |
| "toggle" => next["right"] = json!(!next["right"].as_bool().unwrap()), | |
| "tree" => next["native"] = json!(!next["native"].as_bool().unwrap()), | |
| _ => {} | |
| } | |
| next["last"] = json!(key); | |
| } | |
| let mut slots = vec![maka_ui_runtime::ComponentSlot { | |
| key: "left".into(), | |
| binding: "counter".into(), | |
| member: 0, | |
| props: json!({"label":format!("Left {}",next["version"]),"initial":10}), | |
| on: BTreeMap::from([("changed".into(), "notice".into())]), | |
| }]; | |
| if next["right"] == true { | |
| slots.push(maka_ui_runtime::ComponentSlot { | |
| key: "right".into(), | |
| binding: "counter".into(), | |
| member: 0, | |
| props: json!({"label":if next["last"]=="reject" { "Right Reject".into() } else { format!("Right {}",next["version"]) },"initial":20}), | |
| on: BTreeMap::new(), | |
| }); | |
| } | |
| match next["last"].as_str() { | |
| Some("inexact") => slots[0].props["initial"] = json!(9_007_199_254_740_993u64), | |
| Some("invalid") => slots[0].binding = "undeclared".into(), | |
| Some("bad_event") => { | |
| slots[0].on.insert("undeclared".into(), "notice".into()); | |
| } | |
| Some("duplicate") => slots.push(slots[0].clone()), | |
| _ => {} | |
| } | |
| let view = if next["native"] == true { | |
| NativeView::new( | |
| Rc::new("Native parent".into()), | |
| [ | |
| "parent", | |
| "toggle", | |
| "tree", | |
| "invalid", | |
| "bad_event", | |
| "duplicate", | |
| "inexact", | |
| "reject", | |
| ] | |
| .into_iter() | |
| .map(|k| (k.into(), json!(true))) | |
| .collect(), | |
| slots, | |
| )? | |
| } else { | |
| crate::support::scene::view(Node::Stack { | |
| key: "root".into(), | |
| direction: crate::support::scene::Direction::Column, | |
| gap: 0, | |
| padding: 0, | |
| background: 0, | |
| children: slots | |
| .into_iter() | |
| .map(Node::Component) | |
| .chain([Node::Button { | |
| key: "tree".into(), | |
| label: "Native".into(), | |
| }]) | |
| .collect(), | |
| })? | |
| }; | |
| Ok(Frame { | |
| revision: next["revision"].as_u64().unwrap(), | |
| state: next, | |
| surface: Some(Surface::Terminal { | |
| binding: "surface".into(), | |
| }), | |
| view, | |
| } | |
| .output(vec![])) | |
| } | |
| } | |
| #[test] | |
| fn native_parent_slots_share_js_child_events_rollback_identity_and_retirement() { | |
| let timer = timer(); | |
| let (mut input, mut packages, _) = super::events::fixture( | |
| "return frame({count:props.initial,label:props.label,revision:0,calls:0});", | |
| "return frame({...state,count:state.count+1,revision:state.revision+1},[{name:'changed',value:state.count+1}]);", | |
| "if(props.label==='Right Reject')throw new Error('later sibling');globalThis.updates=(globalThis.updates??0)+1;return frame({...state,label:props.label,revision:state.revision+1,calls:globalThis.updates});", | |
| support::PARENT_EVENT, | |
| "{}", | |
| ); | |
| let i = packages | |
| .iter() | |
| .position(|p| p.manifest().id == "dynamic.app") | |
| .unwrap(); | |
| let old = packages.remove(i); | |
| let mut declaration = old | |
| .manifest() | |
| .application | |
| .as_ref() | |
| .unwrap() | |
| .declaration() | |
| .clone(); | |
| let entry = declaration.entries.get_mut(&EntryTarget::Tui).unwrap(); | |
| entry.scene = None; | |
| entry.implementation = Implementation::Native { | |
| implementation: "test.slot-parent".into(), | |
| }; | |
| let contract = entry.exports["App"].contract.clone(); | |
| let parent = support::package("native.slot-parent", declaration, None); | |
| input.instances.get_mut("app").unwrap().package = parent.application_ref().into(); | |
| packages.push(parent); | |
| let mut native = crate::support::scene::registry(PresentationTarget::Tui); | |
| native | |
| .register("test.slot-parent", move |_, _values| { | |
| Ok(BTreeMap::from([( | |
| "App".into(), | |
| NativeExport { | |
| contract: contract.clone(), | |
| value: NativeCapability::Component(Rc::new(SlotParent)), | |
| }, | |
| )])) | |
| }) | |
| .unwrap(); | |
| let mut app = Application::new( | |
| &input, | |
| packages, | |
| timer.handle().clone(), | |
| maka_ui_js::execution(Default::default()), | |
| native, | |
| Default::default(), | |
| ) | |
| .unwrap(); | |
| let root = app.mount_launch(json!({})).unwrap(); | |
| let left = app.child_mount(&root, "left").unwrap().clone(); | |
| let right = app.child_mount(&root, "right").unwrap().clone(); | |
| assert_eq!( | |
| app.frame(&root).unwrap().view.view.as_str(), | |
| "Native parent" | |
| ); | |
| app.dispatch(&left, 0, "inc", Value::Null).unwrap(); | |
| assert_eq!( | |
| app.frame(&root).unwrap().state["seen"], | |
| json!([["left", "changed", 11]]) | |
| ); | |
| for key in ["invalid", "bad_event", "duplicate", "inexact"] { | |
| assert!(app.dispatch(&root, 1, key, Value::Null).is_err(), "{key}"); | |
| assert_eq!(app.frame(&root).unwrap().revision, 1); | |
| assert_eq!(app.child_mount(&root, "left").unwrap(), &left); | |
| assert_eq!(app.frame(&left).unwrap().state["label"], "Left 0"); | |
| assert_eq!(app.frame(&left).unwrap().state["count"], 11); | |
| assert!(!app.is_retiring(&left)); | |
| assert!(!app.is_retiring(&right)); | |
| } | |
| app.dispatch(&root, 1, "toggle", Value::Null).unwrap(); | |
| assert!(app.is_retiring(&right)); | |
| app.close(&right, Ok(())).unwrap(); | |
| app.dispatch(&root, 2, "parent", Value::Null).unwrap(); | |
| assert_eq!(app.child_mount(&root, "left").unwrap(), &left); | |
| assert_eq!(app.frame(&left).unwrap().state["label"], "Left 1"); | |
| assert_eq!( | |
| app.frame(&left).unwrap().state["calls"], | |
| 1, | |
| "inexact props never executed JS callback or retired shared realm" | |
| ); | |
| app.dispatch(&root, 3, "tree", Value::Null).unwrap(); | |
| assert_eq!(app.frame(&root).unwrap().view.view.as_str(), ""); | |
| assert_eq!(app.child_mount(&root, "left").unwrap(), &left); | |
| app.dispatch(&root, 4, "tree", Value::Null).unwrap(); | |
| assert_eq!( | |
| app.frame(&root).unwrap().view.view.as_str(), | |
| "Native parent" | |
| ); | |
| assert_eq!(app.child_mount(&root, "left").unwrap(), &left); | |
| // Restore the removed slot at the non-rejecting version 2; it is a new mount. | |
| app.dispatch(&root, 5, "parent", Value::Null).unwrap(); | |
| app.dispatch(&root, 6, "toggle", Value::Null).unwrap(); | |
| assert_ne!(app.child_mount(&root, "right").unwrap(), &right); | |
| assert_eq!(app.frame(&left).unwrap().state["count"], 11); | |
| assert!(app.dispatch(&right, 0, "inc", Value::Null).is_err()); | |
| let before = app.frame(&left).unwrap().revision; | |
| assert!(app.dispatch(&root, 7, "reject", Value::Null).is_err()); | |
| assert_eq!(app.frame(&root).unwrap().revision, 7); | |
| assert_eq!(app.frame(&left).unwrap().revision, before); | |
| assert_eq!(app.frame(&left).unwrap().state["label"], "Left 2"); | |
| // A real JS callback failure retires its realm, while the last committed | |
| // scenes and exact mount identities remain intact pending actual cleanup. | |
| assert!(app.is_retiring(&left)); | |
| assert!( | |
| app.is_retiring(&root), | |
| "fault retirement reaches dependent parent" | |
| ); | |
| close_all(&mut app, &root); | |
| } | |
| #[test] | |
| fn native_slot_metadata_rejects_duplicate_and_empty_keys() { | |
| let slot = |key: String| maka_ui_runtime::ComponentSlot { | |
| key, | |
| binding: "child".into(), | |
| member: 0, | |
| props: json!({}), | |
| on: BTreeMap::new(), | |
| }; | |
| assert!( | |
| NativeView::new( | |
| Rc::new(()), | |
| BTreeMap::new(), | |
| vec![slot("same".into()), slot("same".into())] | |
| ) | |
| .is_err() | |
| ); | |
| assert!(NativeView::new(Rc::new(()), BTreeMap::new(), vec![slot(String::new())]).is_err()); | |
| } |