Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/core/transfer_view_tests.rs
StratoSDK / crates / strato-ui-core / src / core / transfer_view_tests.rs
1use std::{cell::RefCell, rc::Rc};
2 
3use super::*;
4 
5#[test]
6fn test_transfer_view_to_window_updates_window_mapping() {
7 #[derive(Default)]
8 struct TestView {
9 value: usize,
10 }
11 
12 impl Entity for TestView {
13 type Event = ();
14 }
15 
16 impl View for TestView {
17 fn render(&self, _: &AppContext) -> Box<dyn Element> {
18 Empty::new().finish()
19 }
20 
21 fn ui_name() -> &'static str {
22 "TestView"
23 }
24 }
25 
26 impl TypedActionView for TestView {
27 type Action = ();
28 }
29 
30 App::test((), |mut app| async move {
31 let (window_1_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
32 let (window_2_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
33 
34 let view_to_transfer = app.add_view(window_1_id, |_| TestView { value: 42 });
35 let view_id = view_to_transfer.id();
36 
37 app.read(|ctx| {
38 assert_eq!(
39 view_to_transfer.window_id(ctx),
40 window_1_id,
41 "view should initially be in window 1"
42 );
43 });
44 
45 app.read(|ctx| {
46 assert!(
47 ctx.windows[&window_1_id].views.contains_key(&view_id),
48 "view should be in window 1's views map"
49 );
50 assert!(
51 !ctx.windows[&window_2_id].views.contains_key(&view_id),
52 "view should not be in window 2's views map yet"
53 );
54 });
55 
56 let success =
57 app.update(|ctx| ctx.transfer_view_to_window(view_id, window_1_id, window_2_id));
58 assert!(success, "transfer should succeed");
59 
60 app.read(|ctx| {
61 assert_eq!(
62 view_to_transfer.window_id(ctx),
63 window_2_id,
64 "view should now be in window 2"
65 );
66 });
67 
68 app.read(|ctx| {
69 assert!(
70 !ctx.windows[&window_1_id].views.contains_key(&view_id),
71 "view should no longer be in window 1's views map"
72 );
73 assert!(
74 ctx.windows[&window_2_id].views.contains_key(&view_id),
75 "view should now be in window 2's views map"
76 );
77 });
78 
79 view_to_transfer.read(&app, |view, _| {
80 assert_eq!(
81 view.value, 42,
82 "view data should be preserved after transfer"
83 );
84 });
85 });
86}
87 
88#[test]
89fn test_transfer_view_subscriptions_continue_working() {
90 #[derive(Default)]
91 struct EmitterView;
92 
93 impl Entity for EmitterView {
94 type Event = usize;
95 }
96 
97 impl View for EmitterView {
98 fn render(&self, _: &AppContext) -> Box<dyn Element> {
99 Empty::new().finish()
100 }
101 
102 fn ui_name() -> &'static str {
103 "EmitterView"
104 }
105 }
106 
107 impl TypedActionView for EmitterView {
108 type Action = ();
109 }
110 
111 #[derive(Default)]
112 struct SubscriberView {
113 received_events: Vec<usize>,
114 }
115 
116 impl Entity for SubscriberView {
117 type Event = ();
118 }
119 
120 impl View for SubscriberView {
121 fn render(&self, _: &AppContext) -> Box<dyn Element> {
122 Empty::new().finish()
123 }
124 
125 fn ui_name() -> &'static str {
126 "SubscriberView"
127 }
128 }
129 
130 impl TypedActionView for SubscriberView {
131 type Action = ();
132 }
133 
134 App::test((), |mut app| async move {
135 let (window_1_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| EmitterView);
136 let (window_2_id, _) =
137 app.add_window(WindowStyle::NotStealFocus, |_| SubscriberView::default());
138 
139 let emitter = app.add_view(window_1_id, |_| EmitterView);
140 let subscriber = app.add_view(window_2_id, |_| SubscriberView::default());
141 
142 subscriber.update(&mut app, |_, ctx| {
143 ctx.subscribe_to_view(&emitter, |view, _, event, _| {
144 view.received_events.push(*event);
145 });
146 });
147 
148 emitter.update(&mut app, |_, ctx| ctx.emit(1));
149 subscriber.read(&app, |view, _| {
150 assert_eq!(
151 view.received_events,
152 vec![1],
153 "should receive event before transfer"
154 );
155 });
156 
157 let emitter_id = emitter.id();
158 let success =
159 app.update(|ctx| ctx.transfer_view_to_window(emitter_id, window_1_id, window_2_id));
160 assert!(success, "transfer should succeed");
161 
162 emitter.update(&mut app, |_, ctx| ctx.emit(2));
163 subscriber.read(&app, |view, _| {
164 assert_eq!(
165 view.received_events,
166 vec![1, 2],
167 "should receive event after transfer"
168 );
169 });
170 
171 emitter.update(&mut app, |_, ctx| ctx.emit(3));
172 subscriber.read(&app, |view, _| {
173 assert_eq!(
174 view.received_events,
175 vec![1, 2, 3],
176 "should continue receiving events"
177 );
178 });
179 });
180}
181 
182#[test]
183fn test_transfer_view_app_subscriptions_continue_working() {
184 #[derive(Default)]
185 struct TestView;
186 
187 impl Entity for TestView {
188 type Event = usize;
189 }
190 
191 impl View for TestView {
192 fn render(&self, _: &AppContext) -> Box<dyn Element> {
193 Empty::new().finish()
194 }
195 
196 fn ui_name() -> &'static str {
197 "TestView"
198 }
199 }
200 
201 impl TypedActionView for TestView {
202 type Action = ();
203 }
204 
205 App::test((), |mut app| async move {
206 let (window_1_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView);
207 let (window_2_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView);
208 
209 let emitter = app.add_view(window_1_id, |_| TestView);
210 let received_events: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
211 let received_events_clone = received_events.clone();
212 
213 app.update(|ctx| {
214 ctx.subscribe_to_view(&emitter, move |_view, event, _ctx| {
215 received_events_clone.borrow_mut().push(*event);
216 });
217 });
218 
219 emitter.update(&mut app, |_, ctx| ctx.emit(1));
220 assert_eq!(
221 *received_events.borrow(),
222 vec![1],
223 "should receive event before transfer"
224 );
225 
226 let emitter_id = emitter.id();
227 let success =
228 app.update(|ctx| ctx.transfer_view_to_window(emitter_id, window_1_id, window_2_id));
229 assert!(success, "transfer should succeed");
230 
231 emitter.update(&mut app, |_, ctx| ctx.emit(2));
232 assert_eq!(
233 *received_events.borrow(),
234 vec![1, 2],
235 "should receive event after transfer"
236 );
237 });
238}
239 
240#[test]
241fn test_transfer_view_observations_continue_working() {
242 #[derive(Default)]
243 struct ObserverView {
244 observed_counts: Vec<usize>,
245 }
246 
247 impl Entity for ObserverView {
248 type Event = ();
249 }
250 
251 impl View for ObserverView {
252 fn render(&self, _: &AppContext) -> Box<dyn Element> {
253 Empty::new().finish()
254 }
255 
256 fn ui_name() -> &'static str {
257 "ObserverView"
258 }
259 }
260 
261 impl TypedActionView for ObserverView {
262 type Action = ();
263 }
264 
265 #[derive(Default)]
266 struct ObservedModel {
267 count: usize,
268 }
269 
270 impl Entity for ObservedModel {
271 type Event = ();
272 }
273 
274 App::test((), |mut app| async move {
275 let (window_1_id, _) =
276 app.add_window(WindowStyle::NotStealFocus, |_| ObserverView::default());
277 let (window_2_id, _) =
278 app.add_window(WindowStyle::NotStealFocus, |_| ObserverView::default());
279 
280 let model = app.add_model(|_| ObservedModel { count: 0 });
281 let observer = app.add_view(window_1_id, |_| ObserverView::default());
282 
283 observer.update(&mut app, |_, ctx| {
284 ctx.observe(&model, |view, observed, ctx| {
285 view.observed_counts.push(observed.as_ref(ctx).count);
286 });
287 });
288 
289 model.update(&mut app, |m, ctx| {
290 m.count = 1;
291 ctx.notify();
292 });
293 observer.read(&app, |view, _| {
294 assert_eq!(
295 view.observed_counts,
296 vec![1],
297 "should observe before transfer"
298 );
299 });
300 
301 let observer_id = observer.id();
302 let success =
303 app.update(|ctx| ctx.transfer_view_to_window(observer_id, window_1_id, window_2_id));
304 assert!(success, "transfer should succeed");
305 
306 model.update(&mut app, |m, ctx| {
307 m.count = 2;
308 ctx.notify();
309 });
310 observer.read(&app, |view, _| {
311 assert_eq!(
312 view.observed_counts,
313 vec![1, 2],
314 "should observe after transfer"
315 );
316 });
317 });
318}
319 
320#[test]
321fn test_on_window_transferred_callback_fires() {
322 struct TransferTrackingView {
323 transfer_events: Vec<(WindowId, WindowId)>,
324 }
325 
326 impl Entity for TransferTrackingView {
327 type Event = ();
328 }
329 
330 impl View for TransferTrackingView {
331 fn render(&self, _: &AppContext) -> Box<dyn Element> {
332 Empty::new().finish()
333 }
334 
335 fn ui_name() -> &'static str {
336 "TransferTrackingView"
337 }
338 
339 fn on_window_transferred(
340 &mut self,
341 source_window_id: WindowId,
342 target_window_id: WindowId,
343 _ctx: &mut ViewContext<Self>,
344 ) {
345 self.transfer_events
346 .push((source_window_id, target_window_id));
347 }
348 }
349 
350 impl TypedActionView for TransferTrackingView {
351 type Action = ();
352 }
353 
354 App::test((), |mut app| async move {
355 let (window_1_id, _) =
356 app.add_window(WindowStyle::NotStealFocus, |_| TransferTrackingView {
357 transfer_events: Vec::new(),
358 });
359 let (window_2_id, _) =
360 app.add_window(WindowStyle::NotStealFocus, |_| TransferTrackingView {
361 transfer_events: Vec::new(),
362 });
363 
364 let view = app.add_view(window_1_id, |_| TransferTrackingView {
365 transfer_events: Vec::new(),
366 });
367 
368 view.read(&app, |v, _| {
369 assert!(v.transfer_events.is_empty(), "no transfers yet");
370 });
371 
372 let view_id = view.id();
373 app.update(|ctx| ctx.transfer_view_to_window(view_id, window_1_id, window_2_id));
374 
375 view.read(&app, |v, _| {
376 assert_eq!(
377 v.transfer_events,
378 vec![(window_1_id, window_2_id)],
379 "callback should fire with correct window IDs"
380 );
381 });
382 });
383}
384 
385#[test]
386fn test_weak_view_handle_upgrade_after_transfer() {
387 #[derive(Default)]
388 struct TestView {
389 value: usize,
390 }
391 
392 impl Entity for TestView {
393 type Event = ();
394 }
395 
396 impl View for TestView {
397 fn render(&self, _: &AppContext) -> Box<dyn Element> {
398 Empty::new().finish()
399 }
400 
401 fn ui_name() -> &'static str {
402 "TestView"
403 }
404 }
405 
406 impl TypedActionView for TestView {
407 type Action = ();
408 }
409 
410 App::test((), |mut app| async move {
411 let (window_1_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
412 let (window_2_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
413 
414 let view = app.add_view(window_1_id, |_| TestView { value: 42 });
415 let weak = view.downgrade();
416 
417 app.read(|ctx| {
418 let upgraded = weak.upgrade(ctx);
419 assert!(
420 upgraded.is_some(),
421 "weak handle should upgrade before transfer"
422 );
423 assert_eq!(
424 upgraded.as_ref().map(|v| v.window_id(ctx)),
425 Some(window_1_id)
426 );
427 });
428 
429 let view_id = view.id();
430 app.update(|ctx| ctx.transfer_view_to_window(view_id, window_1_id, window_2_id));
431 
432 app.read(|ctx| {
433 let upgraded = weak.upgrade(ctx);
434 assert!(
435 upgraded.is_some(),
436 "weak handle should upgrade after transfer"
437 );
438 assert_eq!(
439 upgraded.as_ref().map(|v| v.window_id(ctx)),
440 Some(window_2_id),
441 "upgraded handle should point to new window"
442 );
443 });
444 
445 view.read(&app, |v, _| {
446 assert_eq!(v.value, 42, "view data preserved");
447 });
448 });
449}
450 
451#[test]
452fn test_transfer_nonexistent_view_returns_false() {
453 #[derive(Default)]
454 struct TestView;
455 
456 impl Entity for TestView {
457 type Event = ();
458 }
459 
460 impl View for TestView {
461 fn render(&self, _: &AppContext) -> Box<dyn Element> {
462 Empty::new().finish()
463 }
464 
465 fn ui_name() -> &'static str {
466 "TestView"
467 }
468 }
469 
470 impl TypedActionView for TestView {
471 type Action = ();
472 }
473 
474 App::test((), |mut app| async move {
475 let (window_1_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView);
476 let (window_2_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView);
477 
478 let fake_view_id = EntityId::new();
479 let success =
480 app.update(|ctx| ctx.transfer_view_to_window(fake_view_id, window_1_id, window_2_id));
481 assert!(!success, "transfer of nonexistent view should return false");
482 });
483}
484 
485#[test]
486fn test_transfer_to_same_window_is_noop() {
487 #[derive(Default)]
488 struct TestView {
489 value: usize,
490 }
491 
492 impl Entity for TestView {
493 type Event = usize;
494 }
495 
496 impl View for TestView {
497 fn render(&self, _: &AppContext) -> Box<dyn Element> {
498 Empty::new().finish()
499 }
500 
501 fn ui_name() -> &'static str {
502 "TestView"
503 }
504 }
505 
506 impl TypedActionView for TestView {
507 type Action = ();
508 }
509 
510 App::test((), |mut app| async move {
511 let (window_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
512 
513 let view = app.add_view(window_id, |_| TestView { value: 42 });
514 let view_id = view.id();
515 
516 let success = app.update(|ctx| ctx.transfer_view_to_window(view_id, window_id, window_id));
517 assert!(success, "transfer to same window should return true");
518 
519 app.read(|ctx| {
520 assert_eq!(
521 view.window_id(ctx),
522 window_id,
523 "view should still be in same window"
524 );
525 assert!(
526 ctx.windows[&window_id].views.contains_key(&view_id),
527 "view should still be in window's views map"
528 );
529 });
530 
531 view.update(&mut app, |_, ctx| ctx.emit(1));
532 view.read(&app, |v, _| {
533 assert_eq!(v.value, 42, "view should still work normally");
534 });
535 });
536}
537 
538#[test]
539fn test_transfer_view_drop_and_reference_counting() {
540 #[derive(Default)]
541 struct TestView {
542 value: usize,
543 }
544 
545 impl Entity for TestView {
546 type Event = ();
547 }
548 
549 impl View for TestView {
550 fn render(&self, _: &AppContext) -> Box<dyn Element> {
551 Empty::new().finish()
552 }
553 
554 fn ui_name() -> &'static str {
555 "TestView"
556 }
557 }
558 
559 impl TypedActionView for TestView {
560 type Action = ();
561 }
562 
563 App::test((), |mut app| async move {
564 let (window_1_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
565 let (window_2_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
566 
567 let view = app.add_view(window_1_id, |_| TestView { value: 42 });
568 let view_id = view.id();
569 let view_clone = view.clone();
570 
571 app.update(|ctx| ctx.transfer_view_to_window(view_id, window_1_id, window_2_id));
572 
573 drop(view_clone);
574 
575 app.read(|ctx| {
576 assert!(
577 ctx.windows[&window_2_id].views.contains_key(&view_id),
578 "view should still exist after dropping one handle"
579 );
580 assert!(
581 ctx.view_to_window.contains_key(&view_id),
582 "view_to_window mapping should still exist"
583 );
584 });
585 
586 view.read(&app, |v, _| {
587 assert_eq!(v.value, 42, "view data should be intact");
588 });
589 
590 drop(view);
591 
592 // Trigger cleanup via app.update which calls flush_effects -> remove_dropped_items
593 app.update(|_| {});
594 
595 // Verify the view was removed from the correct window (window_2, not window_1)
596 // and that the view_to_window mapping was cleaned up
597 app.read(|ctx| {
598 assert!(
599 !ctx.windows[&window_1_id].views.contains_key(&view_id),
600 "view should not be in original window"
601 );
602 assert!(
603 !ctx.windows[&window_2_id].views.contains_key(&view_id),
604 "view should be removed from target window after drop"
605 );
606 assert!(
607 !ctx.view_to_window.contains_key(&view_id),
608 "view_to_window mapping should be cleaned up"
609 );
610 });
611 });
612}
613 
614#[test]
615fn test_transfer_structural_children_follows_parent() {
616 #[derive(Default)]
617 struct TestView {
618 value: usize,
619 }
620 
621 impl Entity for TestView {
622 type Event = ();
623 }
624 
625 impl View for TestView {
626 fn render(&self, _: &AppContext) -> Box<dyn Element> {
627 Empty::new().finish()
628 }
629 
630 fn ui_name() -> &'static str {
631 "TestView"
632 }
633 }
634 
635 impl TypedActionView for TestView {
636 type Action = ();
637 }
638 
639 App::test((), |mut app| async move {
640 let (window_1_id, root_1) =
641 app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
642 let (window_2_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
643 
644 let parent = root_1.update(&mut app, |_, ctx| {
645 ctx.add_typed_action_view(|_| TestView { value: 1 })
646 });
647 
648 let structural_child = parent.update(&mut app, |_, ctx| {
649 ctx.add_typed_action_view(|_| TestView { value: 2 })
650 });
651 
652 let parent_id = parent.id();
653 let child_id = structural_child.id();
654 
655 app.read(|ctx| {
656 assert!(
657 ctx.windows[&window_1_id].views.contains_key(&child_id),
658 "child should initially be in window 1"
659 );
660 });
661 
662 let transferred =
663 app.update(|ctx| ctx.transfer_view_tree_to_window(parent_id, window_1_id, window_2_id));
664 
665 assert!(
666 transferred.contains(&parent_id),
667 "parent should be in transferred list"
668 );
669 assert!(
670 transferred.contains(&child_id),
671 "structural child should be in transferred list"
672 );
673 
674 app.read(|ctx| {
675 assert!(
676 ctx.windows[&window_2_id].views.contains_key(&parent_id),
677 "parent should be in window 2"
678 );
679 assert!(
680 ctx.windows[&window_2_id].views.contains_key(&child_id),
681 "structural child should be in window 2"
682 );
683 assert!(
684 !ctx.windows[&window_1_id].views.contains_key(&parent_id),
685 "parent should no longer be in window 1"
686 );
687 assert!(
688 !ctx.windows[&window_1_id].views.contains_key(&child_id),
689 "structural child should no longer be in window 1"
690 );
691 });
692 
693 structural_child.read(&app, |v, _| {
694 assert_eq!(v.value, 2, "structural child data should be preserved");
695 });
696 });
697}
698 
699#[test]
700fn test_transfer_structural_grandchildren_follows_transitively() {
701 #[derive(Default)]
702 struct TestView {
703 value: usize,
704 }
705 
706 impl Entity for TestView {
707 type Event = ();
708 }
709 
710 impl View for TestView {
711 fn render(&self, _: &AppContext) -> Box<dyn Element> {
712 Empty::new().finish()
713 }
714 
715 fn ui_name() -> &'static str {
716 "TestView"
717 }
718 }
719 
720 impl TypedActionView for TestView {
721 type Action = ();
722 }
723 
724 App::test((), |mut app| async move {
725 let (window_1_id, root_1) =
726 app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
727 let (window_2_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView::default());
728 
729 let parent = root_1.update(&mut app, |_, ctx| {
730 ctx.add_typed_action_view(|_| TestView { value: 1 })
731 });
732 
733 let child = parent.update(&mut app, |_, ctx| {
734 ctx.add_typed_action_view(|_| TestView { value: 2 })
735 });
736 
737 let grandchild = child.update(&mut app, |_, ctx| {
738 ctx.add_typed_action_view(|_| TestView { value: 3 })
739 });
740 
741 let parent_id = parent.id();
742 let child_id = child.id();
743 let grandchild_id = grandchild.id();
744 
745 let transferred =
746 app.update(|ctx| ctx.transfer_view_tree_to_window(parent_id, window_1_id, window_2_id));
747 
748 assert!(
749 transferred.contains(&parent_id),
750 "parent should be transferred"
751 );
752 assert!(
753 transferred.contains(&child_id),
754 "child should be transferred"
755 );
756 assert!(
757 transferred.contains(&grandchild_id),
758 "grandchild should be transferred transitively"
759 );
760 
761 app.read(|ctx| {
762 assert!(ctx.windows[&window_2_id].views.contains_key(&grandchild_id));
763 assert!(!ctx.windows[&window_1_id].views.contains_key(&grandchild_id));
764 });
765 
766 grandchild.read(&app, |v, _| {
767 assert_eq!(v.value, 3, "grandchild data should be preserved");
768 });
769 });
770}
771 
772#[test]
773fn test_transfer_structural_children_does_not_move_unrelated_views() {
774 #[derive(Default)]
775 struct TestView;
776 
777 impl Entity for TestView {
778 type Event = ();
779 }
780 
781 impl View for TestView {
782 fn render(&self, _: &AppContext) -> Box<dyn Element> {
783 Empty::new().finish()
784 }
785 
786 fn ui_name() -> &'static str {
787 "TestView"
788 }
789 }
790 
791 impl TypedActionView for TestView {
792 type Action = ();
793 }
794 
795 App::test((), |mut app| async move {
796 let (window_1_id, root_1) = app.add_window(WindowStyle::NotStealFocus, |_| TestView);
797 let (window_2_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView);
798 
799 let parent = root_1.update(&mut app, |_, ctx| ctx.add_typed_action_view(|_| TestView));
800 
801 let structural_child =
802 parent.update(&mut app, |_, ctx| ctx.add_typed_action_view(|_| TestView));
803 
804 let unrelated = app.add_view(window_1_id, |_| TestView);
805 
806 let parent_id = parent.id();
807 let child_id = structural_child.id();
808 let unrelated_id = unrelated.id();
809 
810 let transferred =
811 app.update(|ctx| ctx.transfer_view_tree_to_window(parent_id, window_1_id, window_2_id));
812 
813 assert!(
814 transferred.contains(&parent_id),
815 "parent should be transferred"
816 );
817 assert!(
818 transferred.contains(&child_id),
819 "structural child should be transferred"
820 );
821 assert!(
822 !transferred.contains(&unrelated_id),
823 "unrelated view should NOT be transferred"
824 );
825 
826 app.read(|ctx| {
827 assert!(
828 ctx.windows[&window_1_id].views.contains_key(&unrelated_id),
829 "unrelated view should remain in window 1"
830 );
831 assert!(
832 !ctx.windows[&window_2_id].views.contains_key(&unrelated_id),
833 "unrelated view should NOT be in window 2"
834 );
835 });
836 });
837}
838