use super::*;
use crate::{use_flow_handle, Canvas, Flow, FlowHandle, Rect, Viewport};
use std::cell::Cell;
use std::future::Future;
use std::io::Write;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::task::{Context, Waker};
use std::time::{Duration, Instant};
#[derive(Clone)]
struct FixtureState {
visible: Signal<bool>,
measure: Signal<bool>,
nodes: Signal<Vec<Node>>,
handle: FlowHandle,
core: Rc<Cell<Option<FlowCore>>>,
}
#[component]
fn Fixture(standalone: bool) -> Element {
let visible = use_signal(|| true);
let measure = use_signal(|| true);
let nodes = use_signal(|| vec![Node::new("one", "One", (0.0, 0.0))]);
let edges = use_signal(Vec::new);
let handle = use_flow_handle();
use_context_provider(|| FixtureState {
visible,
measure,
nodes,
handle,
core: Rc::new(Cell::new(None)),
});
rsx! {
if visible() {
if standalone {
Canvas {
world: rsx! {
for node in nodes() {
NodeItem { key: "{node.id}", nodes, node }
}
},
CaptureCore {}
}
} else {
Flow { nodes, edges, handle, fit_view: true,
CaptureCore {}
}
}
}
}
}
#[component]
fn CaptureCore() -> Element {
let state = use_context::<FixtureState>();
let core = use_context::<FlowCore>();
use_hook(|| state.core.set(Some(core)));
use_effect(move || {
core.container
.clone()
.set(Rect::new(0.0, 0.0, 800.0, 600.0))
});
rsx! {
if (state.measure)() {
MeasureThenUnmount {}
}
}
}
#[component]
fn MeasureThenUnmount() -> Element {
let state = use_context::<FixtureState>();
let core = use_context::<FlowCore>();
use_effect(move || {
store_measured(core, state.nodes, &"one".into(), Size::new(240.0, 120.0));
state.measure.clone().set(false);
});
rsx! {}
}
struct Harness {
dom: VirtualDom,
state: FixtureState,
}
impl Harness {
fn new(standalone: bool) -> Self {
let mut dom = VirtualDom::new_with_props(Fixture, FixtureProps { standalone });
dom.rebuild_in_place();
let state = dom.in_scope(ScopeId::APP, consume_context::<FixtureState>);
Self { dom, state }
}
fn core(&self) -> FlowCore {
self.state.core.get().unwrap()
}
fn in_canvas<R>(&self, action: impl FnOnce(FlowCore) -> R) -> R {
let core = self.core();
self.dom
.in_scope(core.viewport.origin_scope(), || action(core))
}
fn settle(&mut self) {
let deadline = Instant::now() + Duration::from_secs(2);
loop {
let idle = std::pin::pin!(self.dom.wait_for_work())
.as_mut()
.poll(&mut Context::from_waker(Waker::noop()))
.is_pending();
self.dom.render_immediate(&mut dioxus::core::NoOpMutations);
let ready = self.in_canvas(|core| {
!*core.size_flush_queued.peek() && !*core.handle_flush_queued.peek()
});
if idle && ready {
break;
}
assert!(Instant::now() < deadline, "canvas flush did not complete");
std::thread::sleep(Duration::from_millis(1));
}
self.dom.render_immediate(&mut dioxus::core::NoOpMutations);
}
fn set_visible(&mut self, visible: bool) {
self.dom
.in_scope(ScopeId::APP, || self.state.visible.set(visible));
self.dom.render_immediate(&mut dioxus::core::NoOpMutations);
}
}
#[derive(Clone, Default)]
struct WarningLog(Arc<Mutex<Vec<u8>>>);
impl Write for WarningLog {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl WarningLog {
fn capture(&self) -> tracing::subscriber::DefaultGuard {
let writer = self.clone();
tracing::subscriber::set_default(
tracing_subscriber::fmt()
.without_time()
.with_ansi(false)
.with_max_level(tracing::Level::WARN)
.with_writer(move || writer.clone())
.finish(),
)
}
fn assert_clean(&self) {
let output = String::from_utf8(self.0.lock().unwrap().clone()).unwrap();
assert!(!output.contains("A Copy Value created"), "{output}");
assert!(!output.contains("Changing the props of `Style"), "{output}");
}
}
#[test]
fn canvas_and_flow_flush_after_children_unmount_without_scope_warnings() {
let log = WarningLog::default();
let _guard = log.capture();
for standalone in [true, false] {
let mut harness = Harness::new(standalone);
harness.settle();
harness.dom.in_scope(ScopeId::APP, || {
assert!(!*harness.state.measure.peek());
assert_eq!(
harness.state.nodes.peek()[0].measured,
Some(Size::new(240.0, 120.0))
);
});
harness.in_canvas(|core| {
assert_eq!(core.handles.peek().len(), 2);
core.interaction.clone().set(Interaction::Pan);
});
harness.settle();
harness
.dom
.in_scope(ScopeId::APP, || harness.state.nodes.clear());
harness.settle();
harness.in_canvas(|core| assert!(core.handles.peek().is_empty()));
}
log.assert_clean();
}
#[test]
fn flow_handle_detaches_and_can_be_reused_after_unmount() {
let log = WarningLog::default();
let _guard = log.capture();
let mut harness = Harness::new(false);
harness.settle();
let first = harness.core().iid;
harness.dom.in_scope(ScopeId::APP, || {
let handle = harness.state.handle;
assert_ne!(handle.viewport(), Some(Viewport::default()));
handle.set_viewport(Viewport::new(15.0, 25.0, 1.5), 0);
assert_eq!(handle.viewport(), Some(Viewport::new(15.0, 25.0, 1.5)));
handle.zoom_in(200);
});
harness.set_visible(false);
harness.dom.in_scope(ScopeId::APP, || {
assert!(harness.state.handle.core().is_none());
assert!(harness.state.handle.viewport().is_none());
harness.state.handle.zoom_out(0);
});
harness.set_visible(true);
harness.settle();
assert_ne!(harness.core().iid, first);
harness.dom.in_scope(ScopeId::APP, || {
assert_eq!(harness.state.handle.core().unwrap().iid, harness.core().iid);
});
log.assert_clean();
}
#[test]
fn pending_flushes_are_cancelled_when_the_canvas_unmounts() {
let log = WarningLog::default();
let _guard = log.capture();
for standalone in [true, false] {
let mut harness = Harness::new(standalone);
harness.settle();
harness.in_canvas(|core| {
store_measured(
core,
harness.state.nodes,
&"one".into(),
Size::new(300.0, 150.0),
);
let key = core.handles.peek().keys().next().unwrap().clone();
core.queue_handle_write(key, None);
assert!(*core.size_flush_queued.peek());
assert!(*core.handle_flush_queued.peek());
});
harness.set_visible(false);
std::thread::sleep(Duration::from_millis(5));
harness
.dom
.render_immediate(&mut dioxus::core::NoOpMutations);
harness.dom.in_scope(ScopeId::APP, || {
assert_eq!(
harness.state.nodes.peek()[0].measured,
Some(Size::new(240.0, 120.0))
);
});
}
log.assert_clean();
}