1use bevy::prelude::*;
17use serde::{Deserialize, Deserializer};
18use serde_json::{Map, Value};
19
20use super::FilterUse;
21use super::registry::ResolvedFilterPass;
22use super::resolve::{ChainInput, ResolvedChain, ResolvedFilterChain};
23use super::wire::FilterChain;
24
25#[derive(Debug, Clone, PartialEq)]
29pub struct MorphFilter {
30 pub key: Value,
33 pub filter: FilterUse,
36}
37
38pub(crate) fn de_morph_filter<'de, D: Deserializer<'de>>(
41 d: D,
42) -> Result<Option<MorphFilter>, D::Error> {
43 let v = Value::deserialize(d)?;
44 if v.is_null() {
45 return Ok(None);
46 }
47 Ok(morph_from_value(v))
48}
49
50fn morph_from_value(value: Value) -> Option<MorphFilter> {
51 let mut obj = match value {
52 Value::Object(obj) => obj,
53 other => {
54 warn_decode(
55 &other,
56 &format!("morphFilter must be a {{ key, name, params }} object, got {other}"),
57 );
58 return None;
59 }
60 };
61 let key = match obj.remove("key") {
62 Some(key @ (Value::String(_) | Value::Number(_))) => key,
63 Some(other) => {
64 warn_decode(
65 &other,
66 &format!("morphFilter key must be a string or number, got {other}"),
67 );
68 return None;
69 }
70 None => {
71 let entry = Value::Object(obj);
72 warn_decode(&entry, &format!("morphFilter {entry} is missing \"key\""));
73 return None;
74 }
75 };
76 let name = match obj.remove("name") {
77 Some(Value::String(name)) => name,
78 Some(other) => {
79 warn_decode(
80 &other,
81 &format!("morphFilter name must be a string, got {other}"),
82 );
83 return None;
84 }
85 None => {
86 let entry = Value::Object(obj);
87 warn_decode(&entry, &format!("morphFilter {entry} is missing \"name\""));
88 return None;
89 }
90 };
91 let params = match obj.remove("params") {
92 None | Some(Value::Null) => Map::new(),
94 Some(Value::Object(params)) => params,
95 Some(other) => {
96 warn_decode(
97 &other,
98 &format!("morphFilter params must be an object, got {other}"),
99 );
100 return None;
101 }
102 };
103 Some(MorphFilter {
104 key,
105 filter: FilterUse { name, params },
106 })
107}
108
109fn warn_decode(value: &Value, message: &str) {
110 crate::protocol::decode_warn("morphFilterParams", &value.to_string(), message);
111}
112
113pub const MORPH_MAX_USER_PARAM_VECS: usize = 6;
119
120#[derive(Component, Debug, Clone, Default, PartialEq)]
128pub struct MorphInput {
129 pub chain: FilterChain,
132 pub key: Value,
134}
135
136impl ChainInput for MorphInput {
137 const KIND_UNKNOWN: &'static str = "morphFilterUnknown";
138 const KIND_PARAMS: &'static str = "morphFilterParams";
139 const FORCE_ALWAYS_DIRTY: bool = false;
140 fn chain(&self) -> &FilterChain {
141 &self.chain
142 }
143 fn validate_entry(
149 name: &str,
150 is_morph: bool,
151 passes: &[ResolvedFilterPass],
152 ) -> Result<(), String> {
153 if !is_morph {
154 return Err(format!(
155 "{name:?} is not a morph filter — register it with #[react_morph_filter] / \
156 add_react_morph_filter"
157 ));
158 }
159 if passes.len() != 1 {
160 return Err(format!(
161 "a morph filter must resolve to exactly one pass, got {} — \
162 multi-pass filters cannot be used as morphs",
163 passes.len()
164 ));
165 }
166 if passes[0].params.len() > MORPH_MAX_USER_PARAM_VECS {
167 return Err(format!(
168 "morph filter params occupy {} vec4 slots, over the cap of \
169 {MORPH_MAX_USER_PARAM_VECS} (the last two are engine-reserved)",
170 passes[0].params.len()
171 ));
172 }
173 Ok(())
174 }
175}
176
177#[derive(Component, Debug, Clone, Default)]
185pub struct ResolvedMorphChain(pub ResolvedFilterChain);
186
187impl ResolvedChain for ResolvedMorphChain {
188 fn from_inner(inner: ResolvedFilterChain) -> Self {
189 Self(inner)
190 }
191 fn inner(&self) -> &ResolvedFilterChain {
192 &self.0
193 }
194 fn inner_mut(&mut self) -> &mut ResolvedFilterChain {
195 &mut self.0
196 }
197}
198
199#[derive(Component, Debug, Clone, Default, PartialEq)]
210pub struct MorphState {
211 pub active: bool,
217 pub progress: f32,
220 pub freeze_seq: u64,
223}
224
225#[cfg(test)]
226mod resolve_tests {
227 use serde_json::json;
228
229 use super::super::test_util::{create, entity_of, resolve_app, update};
230 use super::*;
231 use crate::layer::PromotedLayer;
232
233 #[test]
238 fn morph_create_attaches_resolved_chain() {
239 let (mut app, ops_tx) = resolve_app();
240 ops_tx
241 .send(vec![create(
242 1,
243 json!({ "style": { "morphFilter": {
244 "key": "a", "name": "crossfade"
245 } } }),
246 )])
247 .unwrap();
248 app.update();
249 let e = entity_of(&app, 1);
250 assert!(app.world().get::<PromotedLayer>(e).is_some(), "promoted");
251 let input = app.world().get::<MorphInput>(e).expect("input stamped");
252 assert_eq!(input.key, json!("a"));
253 assert_eq!(input.chain.0.len(), 1);
254 let chain = app
255 .world()
256 .get::<ResolvedMorphChain>(e)
257 .expect("morph chain resolved");
258 assert_eq!(chain.0.version, 1);
259 assert_eq!(chain.0.passes.len(), 1);
260 assert!(!chain.0.always_dirty);
261 assert!(
262 app.world()
263 .get::<super::super::ResolvedFilterChain>(e)
264 .is_none(),
265 "content channel untouched"
266 );
267 assert!(
268 app.world()
269 .get::<super::super::ResolvedBackdropChain>(e)
270 .is_none(),
271 "backdrop channel untouched"
272 );
273
274 ops_tx
277 .send(vec![update(
278 1,
279 json!({ "style": { "morphFilter": {
280 "key": "b", "name": "crossfade"
281 } } }),
282 &[],
283 )])
284 .unwrap();
285 app.update();
286 assert_eq!(app.world().get::<MorphInput>(e).unwrap().key, json!("b"));
287 assert_eq!(
288 app.world().get::<ResolvedMorphChain>(e).unwrap().0.version,
289 1,
290 "identical resolve output must not bump"
291 );
292 }
293
294 #[cfg(all(feature = "devtools", debug_assertions))]
298 #[test]
299 fn unknown_morph_name_warns_and_attaches_no_chain() {
300 let _lock = crate::diag::test_lock();
301 crate::diag::arm_runtime();
302 let _ = crate::diag::take_runtime_warnings();
303
304 let (mut app, ops_tx) = resolve_app();
305 ops_tx
306 .send(vec![create(
307 7,
308 json!({ "style": { "morphFilter": { "key": 1, "name": "nope" } } }),
309 )])
310 .unwrap();
311 app.update();
312 let e = entity_of(&app, 7);
313 assert!(app.world().get::<PromotedLayer>(e).is_some(), "promoted");
314 assert!(app.world().get::<ResolvedMorphChain>(e).is_none());
315
316 let warns: Vec<_> = crate::diag::take_runtime_warnings()
317 .into_iter()
318 .filter(|w| w.node == Some(7))
319 .collect();
320 assert_eq!(warns.len(), 1, "{warns:?}");
321 assert_eq!(warns[0].kind, "morphFilterUnknown");
322 }
323
324 #[cfg(all(feature = "devtools", debug_assertions))]
329 #[test]
330 fn non_morph_filter_rejected_as_morph() {
331 let _lock = crate::diag::test_lock();
332 crate::diag::arm_runtime();
333 let _ = crate::diag::take_runtime_warnings();
334
335 let (mut app, ops_tx) = resolve_app();
336 ops_tx
337 .send(vec![create(
338 8,
339 json!({ "style": {
340 "morphFilter": { "key": "a", "name": "blur" },
341 "filter": { "name": "blur" }
342 } }),
343 )])
344 .unwrap();
345 app.update();
346 let e = entity_of(&app, 8);
347 assert!(
348 app.world().get::<ResolvedMorphChain>(e).is_none(),
349 "regular filter rejected as morph"
350 );
351 let content = app
352 .world()
353 .get::<super::super::ResolvedFilterChain>(e)
354 .expect("content chain unaffected by the morph cap");
355 assert_eq!(content.passes.len(), 2, "blur H+V");
356
357 let warns: Vec<_> = crate::diag::take_runtime_warnings()
358 .into_iter()
359 .filter(|w| w.node == Some(8))
360 .collect();
361 assert_eq!(warns.len(), 1, "{warns:?}");
362 assert_eq!(warns[0].kind, "morphFilterParams");
363 assert!(
364 warns[0].message.contains("not a morph filter"),
365 "{}",
366 warns[0].message
367 );
368 }
369
370 #[test]
374 fn validate_entry_orders_family_then_pass_count_then_param_cap() {
375 let pass = || ResolvedFilterPass {
376 shader: Handle::default(),
377 params: Vec::new(),
378 layout: Vec::new().into(),
379 wire_index: 0,
380 };
381 let family = <MorphInput as ChainInput>::validate_entry("blur", false, &[pass()]);
382 assert!(family.unwrap_err().contains("not a morph filter"));
383
384 let two = <MorphInput as ChainInput>::validate_entry("x", true, &[pass(), pass()]);
385 assert!(two.unwrap_err().contains("exactly one pass"));
386
387 let mut fat = pass();
388 fat.params = vec![Vec4::ZERO; MORPH_MAX_USER_PARAM_VECS + 1];
389 let capped = <MorphInput as ChainInput>::validate_entry("x", true, &[fat]);
390 assert!(capped.unwrap_err().contains("over the cap"));
391
392 assert!(<MorphInput as ChainInput>::validate_entry("x", true, &[pass()]).is_ok());
393 }
394
395 #[test]
399 fn unset_removes_chain_while_promoted_and_on_demote() {
400 let (mut app, ops_tx) = resolve_app();
401 ops_tx
402 .send(vec![create(
403 1,
404 json!({ "style": {
405 "cache": "always",
406 "morphFilter": { "key": "a", "name": "crossfade" }
407 } }),
408 )])
409 .unwrap();
410 app.update();
411 let e = entity_of(&app, 1);
412 assert!(app.world().get::<ResolvedMorphChain>(e).is_some());
413
414 ops_tx
415 .send(vec![update(1, json!({}), &["morphFilter"])])
416 .unwrap();
417 app.update();
418 assert!(
419 app.world().get::<PromotedLayer>(e).is_some(),
420 "cache: always still holds the layer"
421 );
422 assert!(app.world().get::<MorphInput>(e).is_none(), "input gone");
423 assert!(
424 app.world().get::<ResolvedMorphChain>(e).is_none(),
425 "chain cleaned up while still promoted"
426 );
427
428 ops_tx
429 .send(vec![update(
430 1,
431 json!({ "style": { "morphFilter": { "key": "a", "name": "linearWipe" } } }),
432 &["cache"],
433 )])
434 .unwrap();
435 app.update();
436 assert!(app.world().get::<ResolvedMorphChain>(e).is_some());
437 ops_tx
438 .send(vec![update(1, json!({}), &["morphFilter"])])
439 .unwrap();
440 app.update();
441 assert!(app.world().get::<PromotedLayer>(e).is_none(), "demoted");
442 assert!(
443 app.world().get::<ResolvedMorphChain>(e).is_none(),
444 "demote removes the resolved chain"
445 );
446 assert!(
447 app.world().get::<MorphState>(e).is_none(),
448 "demote removes the morph state"
449 );
450 }
451}
452
453#[cfg(test)]
454mod channel_tests {
455 use serde_json::json;
456
457 use super::super::test_util::{create, drain_dirt, ease_app, entity_of, tick, update};
458 use super::*;
459 use crate::layer::{LayerCaptureRect, LayerContentDirt};
460
461 fn stamp_rect(app: &mut bevy::app::App, e: Entity) {
464 app.world_mut().entity_mut(e).insert(LayerCaptureRect {
465 min: Vec2::new(10.0, 20.0),
466 size: UVec2::new(100, 50),
467 outset: 0,
468 });
469 }
470
471 #[test]
476 fn key_change_arms_with_default_duration() {
477 let (mut app, ops_tx) = ease_app();
478 ops_tx
479 .send(vec![create(
480 1,
481 json!({ "style": { "morphFilter": { "key": "a", "name": "crossfade" } } }),
482 )])
483 .unwrap();
484 app.update();
485 let e = entity_of(&app, 1);
486 assert!(
487 app.world().get::<MorphState>(e).is_none(),
488 "mount inserts no state"
489 );
490 stamp_rect(&mut app, e);
491 drain_dirt(&mut app);
492
493 ops_tx
494 .send(vec![update(
495 1,
496 json!({ "style": { "morphFilter": { "key": "b", "name": "crossfade" } } }),
497 &[],
498 )])
499 .unwrap();
500 tick(&mut app, 0.15);
501 {
502 let s = app.world().get::<MorphState>(e).expect("state inserted");
503 assert!(s.active);
504 assert_eq!(s.freeze_seq, 1);
505 assert_eq!(s.progress, 0.0, "freeze frame renders fully 'from'");
506 let dirt = app.world().resource::<LayerContentDirt>();
507 assert!(dirt.nodes.contains(&e), "freeze re-captures: {dirt:?}");
508 }
509
510 tick(&mut app, 0.15);
511 {
512 let s = app.world().get::<MorphState>(e).unwrap();
513 assert!(
514 s.progress > 0.0 && s.progress < 1.0,
515 "mid-ease: {}",
516 s.progress
517 );
518 assert!(s.active);
519 }
520
521 tick(&mut app, 0.3);
522 {
523 let s = app.world().get::<MorphState>(e).unwrap();
524 assert_eq!(s.progress, 1.0, "settle writes exactly 1.0");
525 assert!(s.active, "settle frame still renders the pass");
526 }
527
528 tick(&mut app, 0.016);
529 let s = app.world().get::<MorphState>(e).unwrap();
530 assert!(!s.active, "deactivated the frame after settle");
531 assert_eq!(s.progress, 1.0);
532 }
533
534 #[test]
537 fn explicit_morph_spec_wins() {
538 let (mut app, ops_tx) = ease_app();
539 ops_tx
540 .send(vec![create(
541 1,
542 json!({ "style": {
543 "morphFilter": { "key": "a", "name": "crossfade" },
544 "transition": { "morphFilter": { "duration": 1000, "easing": "linear" } },
545 } }),
546 )])
547 .unwrap();
548 app.update();
549 let e = entity_of(&app, 1);
550 stamp_rect(&mut app, e);
551
552 ops_tx
553 .send(vec![update(
554 1,
555 json!({ "style": {
556 "morphFilter": { "key": "b", "name": "crossfade" },
557 "transition": { "morphFilter": { "duration": 1000, "easing": "linear" } },
558 } }),
559 &[],
560 )])
561 .unwrap();
562 tick(&mut app, 0.25); tick(&mut app, 0.5);
564 let s = app.world().get::<MorphState>(e).unwrap();
565 assert_eq!(s.progress, 0.5, "1s linear at 0.5s");
566 }
567
568 #[test]
571 fn mount_and_rectless_key_change_do_not_animate() {
572 let (mut app, ops_tx) = ease_app();
573 ops_tx
574 .send(vec![create(
575 1,
576 json!({ "style": { "morphFilter": { "key": "a", "name": "crossfade" } } }),
577 )])
578 .unwrap();
579 app.update();
580 let e = entity_of(&app, 1);
581 assert!(app.world().get::<MorphState>(e).is_none());
582
583 ops_tx
585 .send(vec![update(
586 1,
587 json!({ "style": { "morphFilter": { "key": "b", "name": "crossfade" } } }),
588 &[],
589 )])
590 .unwrap();
591 tick(&mut app, 0.016);
592 assert!(
593 app.world().get::<MorphState>(e).is_none(),
594 "rectless key change snaps"
595 );
596 stamp_rect(&mut app, e);
599 ops_tx
600 .send(vec![update(
601 1,
602 json!({ "style": { "morphFilter": { "key": "c", "name": "crossfade" } } }),
603 &[],
604 )])
605 .unwrap();
606 tick(&mut app, 0.016);
607 assert!(app.world().get::<MorphState>(e).is_some_and(|s| s.active));
608 }
609
610 #[test]
613 fn unresolved_morph_key_change_snaps() {
614 let (mut app, ops_tx) = ease_app();
615 ops_tx
616 .send(vec![create(
617 1,
618 json!({ "style": { "morphFilter": { "key": "a", "name": "nope" } } }),
619 )])
620 .unwrap();
621 app.update();
622 let e = entity_of(&app, 1);
623 stamp_rect(&mut app, e);
624 ops_tx
625 .send(vec![update(
626 1,
627 json!({ "style": { "morphFilter": { "key": "b", "name": "nope" } } }),
628 &[],
629 )])
630 .unwrap();
631 tick(&mut app, 0.016);
632 assert!(app.world().get::<MorphState>(e).is_none());
633 }
634
635 #[test]
639 fn key_rebump_mid_flight_bumps_freeze_seq_and_restarts() {
640 let (mut app, ops_tx) = ease_app();
641 ops_tx
642 .send(vec![create(
643 1,
644 json!({ "style": { "morphFilter": { "key": "a", "name": "crossfade" } } }),
645 )])
646 .unwrap();
647 app.update();
648 let e = entity_of(&app, 1);
649 stamp_rect(&mut app, e);
650
651 ops_tx
652 .send(vec![update(
653 1,
654 json!({ "style": { "morphFilter": { "key": "b", "name": "crossfade" } } }),
655 &[],
656 )])
657 .unwrap();
658 tick(&mut app, 0.15);
659 tick(&mut app, 0.1);
660 let mid = app.world().get::<MorphState>(e).unwrap().progress;
661 assert!(mid > 0.0 && mid < 1.0, "mid-flight: {mid}");
662
663 ops_tx
664 .send(vec![update(
665 1,
666 json!({ "style": { "morphFilter": { "key": "c", "name": "crossfade" } } }),
667 &[],
668 )])
669 .unwrap();
670 tick(&mut app, 0.016);
671 let s = app.world().get::<MorphState>(e).unwrap();
672 assert_eq!(s.freeze_seq, 2, "re-freeze requested");
673 assert_eq!(s.progress, 0.0, "restarted");
674 assert!(s.active);
675 }
676
677 #[test]
682 fn unset_morph_mid_flight_snaps_and_deactivates() {
683 let (mut app, ops_tx) = ease_app();
684 ops_tx
685 .send(vec![create(
686 1,
687 json!({ "style": {
688 "cache": "always",
689 "morphFilter": { "key": "a", "name": "crossfade" }
690 } }),
691 )])
692 .unwrap();
693 app.update();
694 let e = entity_of(&app, 1);
695 stamp_rect(&mut app, e);
696 ops_tx
697 .send(vec![update(
698 1,
699 json!({ "style": { "morphFilter": { "key": "b", "name": "crossfade" } } }),
700 &[],
701 )])
702 .unwrap();
703 tick(&mut app, 0.1);
704 assert!(app.world().get::<MorphState>(e).is_some_and(|s| s.active));
705
706 ops_tx
707 .send(vec![update(1, json!({}), &["morphFilter"])])
708 .unwrap();
709 tick(&mut app, 0.016);
710 assert!(
711 app.world().get::<crate::layer::PromotedLayer>(e).is_some(),
712 "cache: always still holds the layer"
713 );
714 assert!(
715 app.world().get::<MorphState>(e).is_none(),
716 "unset removes the runtime state with the input"
717 );
718 assert!(app.world().get::<MorphInput>(e).is_none());
719 }
720
721 #[test]
727 fn morph_param_binding_drives_packed_slot_and_never_parks_progress() {
728 use super::super::test_util::anim_app;
729 let (mut app, ops_tx, anim_tx) = anim_app();
730 anim_tx
731 .send(crate::animations::AnimationCommand::Set { id: 1, value: 90.0 })
732 .unwrap();
733 ops_tx
734 .send(vec![create(
735 1,
736 json!({ "style": { "morphFilter": {
737 "key": "a", "name": "linearWipe",
738 "params": { "angle": { "animated": { "id": 1 } } }
739 } } }),
740 )])
741 .unwrap();
742 app.update();
743 let e = entity_of(&app, 1);
744 {
745 let chain = &app.world().get::<ResolvedMorphChain>(e).unwrap().0;
746 assert!(
747 (chain.passes[0].params[0].x - std::f32::consts::FRAC_PI_2).abs() < 1e-5,
748 "angle driven, degrees → packed radians: {}",
749 chain.passes[0].params[0].x
750 );
751 }
752
753 drain_dirt(&mut app);
754 anim_tx
755 .send(crate::animations::AnimationCommand::Set {
756 id: 1,
757 value: 180.0,
758 })
759 .unwrap();
760 tick(&mut app, 0.016);
761 {
762 let chain = &app.world().get::<ResolvedMorphChain>(e).unwrap().0;
763 assert!((chain.passes[0].params[0].x - std::f32::consts::PI).abs() < 1e-5);
764 let dirt = app.world().resource::<crate::layer::LayerContentDirt>();
765 assert!(dirt.composite_only.contains(&e), "{dirt:?}");
766 assert!(!dirt.nodes.contains(&e), "never capture dirt: {dirt:?}");
767 }
768
769 stamp_rect(&mut app, e);
772 ops_tx
773 .send(vec![update(
774 1,
775 json!({ "style": { "morphFilter": {
776 "key": "b", "name": "linearWipe",
777 "params": { "angle": { "animated": { "id": 1 } } }
778 } } }),
779 &[],
780 )])
781 .unwrap();
782 tick(&mut app, 0.016);
783 tick(&mut app, 0.15);
784 let s = app.world().get::<MorphState>(e).expect("morph armed");
785 assert!(s.active);
786 assert!(
787 s.progress > 0.0 && s.progress < 1.0,
788 "progress eases despite the param binding: {}",
789 s.progress
790 );
791 }
792
793 #[test]
796 fn params_change_without_key_change_does_not_restart() {
797 let (mut app, ops_tx) = ease_app();
798 ops_tx
799 .send(vec![create(
800 1,
801 json!({ "style": { "morphFilter": {
802 "key": "a", "name": "linearWipe", "params": { "softness": 0.0 }
803 } } }),
804 )])
805 .unwrap();
806 app.update();
807 let e = entity_of(&app, 1);
808 stamp_rect(&mut app, e);
809
810 ops_tx
811 .send(vec![update(
812 1,
813 json!({ "style": { "morphFilter": {
814 "key": "a", "name": "linearWipe", "params": { "softness": 40.0 }
815 } } }),
816 &[],
817 )])
818 .unwrap();
819 tick(&mut app, 0.016);
820 assert!(
821 app.world().get::<MorphState>(e).is_none(),
822 "no morph started"
823 );
824 let chain = app.world().get::<ResolvedMorphChain>(e).unwrap();
825 assert_eq!(chain.0.version, 2, "params snapped by the resolver");
826 assert_eq!(chain.0.passes[0].params[0].y, 40.0);
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 fn decode(json: &str) -> Option<MorphFilter> {
836 #[derive(Deserialize)]
837 struct Holder {
838 #[serde(default, deserialize_with = "de_morph_filter")]
839 morph_filter: Option<MorphFilter>,
840 }
841 let h: Holder = serde_json::from_str(&format!(r#"{{"morph_filter":{json}}}"#))
842 .expect("morphFilter decode must not error");
843 h.morph_filter
844 }
845
846 #[test]
847 fn full_object_decodes() {
848 let m = decode(r#"{"key":"a","name":"linearWipe","params":{"angle":45}}"#)
849 .expect("decodes to Some");
850 assert_eq!(m.key, Value::from("a"));
851 assert_eq!(m.filter.name, "linearWipe");
852 assert_eq!(m.filter.params["angle"], serde_json::json!(45));
853 }
854
855 #[test]
856 fn numeric_key_and_absent_params_decode() {
857 let m = decode(r#"{"key":3,"name":"crossfade"}"#).expect("decodes to Some");
858 assert_eq!(m.key, Value::from(3));
859 assert!(m.filter.params.is_empty());
860 let m = decode(r#"{"key":3,"name":"crossfade","params":null}"#).expect("decodes to Some");
862 assert!(m.filter.params.is_empty());
863 }
864
865 #[test]
868 fn malformed_values_degrade_to_none() {
869 assert_eq!(decode("42"), None); assert_eq!(decode(r#"{"name":"crossfade"}"#), None); assert_eq!(decode(r#"{"key":true,"name":"crossfade"}"#), None); assert_eq!(decode(r#"{"key":"a"}"#), None); assert_eq!(decode(r#"{"key":"a","name":7}"#), None); assert_eq!(decode(r#"{"key":"a","name":"crossfade","params":3}"#), None); assert_eq!(decode("null"), None); }
877
878 #[cfg(all(feature = "devtools", debug_assertions))]
881 #[test]
882 fn malformed_values_report_decode_warnings() {
883 let _ = crate::diag::take_decode_warnings();
884 let _ = decode(r#"{"name":"crossfade"}"#);
885 let _ = decode("42");
886 let warns = crate::diag::take_decode_warnings();
887 assert_eq!(warns.len(), 2);
888 assert!(warns.iter().all(|w| w.kind == "morphFilterParams"));
889 let _ = decode(r#"{"key":"a","name":"crossfade"}"#);
891 let _ = decode("null");
892 assert!(crate::diag::take_decode_warnings().is_empty());
893 }
894}