jellyflow-runtime 0.1.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use super::super::fixtures::{default_editor_config, make_graph, make_store};

use crate::io::NodeGraphViewState;
use crate::runtime::events::NodeGraphStoreEvent;
use jellyflow_core::core::{CanvasPoint, EdgeId, Graph, GraphId, NodeId};
use jellyflow_core::ops::{GraphOp, GraphTransaction};

#[test]
fn store_replace_view_state_emits_view_changed_event() {
    use std::cell::RefCell;
    use std::rc::Rc;

    let (g0, _a, _b, _out_port, _in_port, _eid) = make_graph();
    let mut store = make_store(g0);

    let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
    let events2 = events.clone();
    store.subscribe(move |ev| match ev {
        NodeGraphStoreEvent::DocumentReplaced { .. } => events2.borrow_mut().push("document"),
        NodeGraphStoreEvent::ViewChanged { .. } => events2.borrow_mut().push("view"),
        NodeGraphStoreEvent::GraphCommitted { .. } => events2.borrow_mut().push("graph"),
    });

    let mut vs = NodeGraphViewState::default();
    vs.pan = jellyflow_core::core::CanvasPoint { x: 10.0, y: 20.0 };
    vs.zoom = 1.5;
    store.replace_view_state(vs);

    assert_eq!(events.borrow().as_slice(), &["view"]);
}

#[test]
fn store_set_viewport_emits_exact_zoom_changes_below_projection_epsilon() {
    use std::cell::RefCell;
    use std::rc::Rc;

    let (g0, _a, _b, _out_port, _in_port, _eid) = make_graph();
    let mut store = make_store(g0);

    let pan = CanvasPoint { x: 10.0, y: 20.0 };
    store.set_viewport(pan, 1.0);

    let zooms: Rc<RefCell<Vec<f32>>> = Rc::new(RefCell::new(Vec::new()));
    let zooms2 = zooms.clone();
    store.subscribe(move |ev| {
        if let NodeGraphStoreEvent::ViewChanged { changes, .. } = ev {
            for change in changes {
                if let crate::runtime::events::ViewChange::Viewport { zoom, .. } = change {
                    zooms2.borrow_mut().push(*zoom);
                }
            }
        }
    });

    let zoom = 1.0 + 5.0e-7;
    store.set_viewport(pan, zoom);

    assert_eq!(zooms.borrow().as_slice(), &[zoom]);
}

#[test]
fn store_replace_document_emits_single_document_event_and_clears_history() {
    use std::cell::RefCell;
    use std::rc::Rc;

    let (g0, a, b, _out_port, _in_port, _eid) = make_graph();
    let replacement_node = g0.nodes.get(&b).expect("replacement node").clone();
    let mut store = make_store(g0);

    let from = store.graph().nodes.get(&a).expect("node a").pos;
    let tx = GraphTransaction::from_ops([GraphOp::SetNodePos {
        id: a,
        from,
        to: CanvasPoint {
            x: from.x + 10.0,
            y: from.y + 5.0,
        },
    }])
    .with_label("seed history");
    store.dispatch_transaction(&tx).expect("seed history");
    assert!(store.can_undo());

    let before_revision = store.graph_revision();
    let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
    let events2 = events.clone();
    type DocumentEventDetail = (GraphId, GraphId, u64, u64, Vec<NodeId>, bool);
    let details: Rc<RefCell<Option<DocumentEventDetail>>> = Rc::new(RefCell::new(None));
    let details2 = details.clone();
    store.subscribe(move |ev| match ev {
        NodeGraphStoreEvent::DocumentReplaced { before, after } => {
            events2.borrow_mut().push("document");
            *details2.borrow_mut() = Some((
                before.graph.graph_id,
                after.graph.graph_id,
                before.graph_revision,
                after.graph_revision,
                after.view_state.selected_nodes.clone(),
                after
                    .editor_config
                    .runtime_tuning
                    .only_render_visible_elements,
            ));
        }
        NodeGraphStoreEvent::ViewChanged { .. } => events2.borrow_mut().push("view"),
        NodeGraphStoreEvent::GraphCommitted { .. } => events2.borrow_mut().push("graph"),
    });

    let mut next_graph = Graph::new(GraphId::from_u128(2));
    next_graph.nodes.insert(b, replacement_node);
    let mut next_view_state = NodeGraphViewState {
        selected_nodes: vec![a, b],
        ..NodeGraphViewState::default()
    };
    next_view_state.pan = CanvasPoint { x: 8.0, y: 13.0 };
    next_view_state.zoom = 1.75;
    let mut next_editor_config = default_editor_config();
    next_editor_config
        .runtime_tuning
        .only_render_visible_elements = false;

    store.replace_document(
        next_graph.clone(),
        next_view_state,
        next_editor_config.clone(),
    );

    assert_eq!(events.borrow().as_slice(), &["document"]);
    let detail = details.borrow().clone().expect("document event detail");
    assert_eq!(detail.0, GraphId::from_u128(1));
    assert_eq!(detail.1, GraphId::from_u128(2));
    assert_eq!(detail.2, before_revision);
    assert!(detail.3 > detail.2);
    assert_eq!(detail.4, vec![b]);
    assert_eq!(
        detail.5,
        next_editor_config
            .runtime_tuning
            .only_render_visible_elements
    );
    assert_eq!(store.graph().graph_id, next_graph.graph_id);
    assert_eq!(store.view_state().selected_nodes, vec![b]);
    assert_eq!(store.editor_config(), next_editor_config);
    assert!(!store.can_undo());
    assert!(!store.can_redo());
}

#[test]
fn store_replace_graph_emits_document_event_and_preserves_history_policy() {
    use std::cell::RefCell;
    use std::rc::Rc;

    let (g0, a, b, _out_port, _in_port, _eid) = make_graph();
    let replacement_node = g0.nodes.get(&a).expect("replacement node").clone();
    let mut store = make_store(g0);
    store.set_selection(vec![b], Vec::new(), Vec::new());

    let from = store.graph().nodes.get(&a).expect("node a").pos;
    let tx = GraphTransaction::from_ops([GraphOp::SetNodePos {
        id: a,
        from,
        to: CanvasPoint {
            x: from.x + 10.0,
            y: from.y + 5.0,
        },
    }])
    .with_label("seed history");
    store.dispatch_transaction(&tx).expect("seed history");
    assert!(store.can_undo());

    let before_revision = store.graph_revision();
    let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
    let events2 = events.clone();
    let selected_after: Rc<RefCell<Option<Vec<NodeId>>>> = Rc::new(RefCell::new(None));
    let selected_after2 = selected_after.clone();
    store.subscribe(move |ev| match ev {
        NodeGraphStoreEvent::DocumentReplaced { before, after } => {
            events2.borrow_mut().push("document");
            assert_eq!(before.graph_revision, before_revision);
            assert!(after.graph_revision > before.graph_revision);
            *selected_after2.borrow_mut() = Some(after.view_state.selected_nodes.clone());
        }
        NodeGraphStoreEvent::ViewChanged { .. } => events2.borrow_mut().push("view"),
        NodeGraphStoreEvent::GraphCommitted { .. } => events2.borrow_mut().push("graph"),
    });

    let mut next_graph = Graph::new(GraphId::from_u128(3));
    next_graph.nodes.insert(a, replacement_node);
    store.replace_graph(next_graph);

    assert_eq!(events.borrow().as_slice(), &["document"]);
    assert_eq!(selected_after.borrow().clone(), Some(Vec::new()));
    assert!(store.can_undo());
}

#[test]
fn store_replace_editor_config_notifies_selectors_for_runtime_tuning_only_changes() {
    use std::cell::RefCell;
    use std::rc::Rc;

    let (g0, _a, _b, _out_port, _in_port, _eid) = make_graph();
    let mut store = make_store(g0);

    let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
    let events2 = events.clone();
    store.subscribe(move |ev| match ev {
        NodeGraphStoreEvent::DocumentReplaced { .. } => events2.borrow_mut().push("document"),
        NodeGraphStoreEvent::ViewChanged { .. } => events2.borrow_mut().push("view"),
        NodeGraphStoreEvent::GraphCommitted { .. } => events2.borrow_mut().push("graph"),
    });

    let runtime_flags: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
    let runtime_flags2 = runtime_flags.clone();
    store.subscribe_selector(
        |s| s.runtime_tuning.only_render_visible_elements,
        move |value| runtime_flags2.borrow_mut().push(*value),
    );

    let mut editor_config = store.editor_config();
    editor_config.runtime_tuning.only_render_visible_elements = false;
    store.replace_editor_config(editor_config);

    assert!(events.borrow().is_empty());
    assert_eq!(runtime_flags.borrow().as_slice(), &[false]);
    assert!(!store.runtime_tuning().only_render_visible_elements);
}

#[test]
fn store_update_editor_config_notifies_selectors_only_when_changed() {
    use std::cell::RefCell;
    use std::rc::Rc;

    let (g0, _a, _b, _out_port, _in_port, _eid) = make_graph();
    let mut store = make_store(g0);

    let runtime_flags: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
    let runtime_flags2 = runtime_flags.clone();
    store.subscribe_selector(
        |s| s.runtime_tuning.only_render_visible_elements,
        move |value| runtime_flags2.borrow_mut().push(*value),
    );

    store.update_editor_config(|_| {});
    assert!(runtime_flags.borrow().is_empty());

    store.update_editor_config(|config| {
        config.runtime_tuning.only_render_visible_elements = false;
    });
    assert_eq!(runtime_flags.borrow().as_slice(), &[false]);

    store.update_editor_config(|config| {
        config.runtime_tuning.only_render_visible_elements = false;
    });
    assert_eq!(runtime_flags.borrow().as_slice(), &[false]);
}

#[test]
fn store_update_view_state_notifies_selectors_for_draw_order_only_changes() {
    use std::cell::RefCell;
    use std::rc::Rc;

    let (g0, a, b, _out_port, _in_port, _eid) = make_graph();
    let mut store = make_store(g0);

    let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
    let events2 = events.clone();
    store.subscribe(move |ev| match ev {
        NodeGraphStoreEvent::DocumentReplaced { .. } => events2.borrow_mut().push("document"),
        NodeGraphStoreEvent::ViewChanged { .. } => events2.borrow_mut().push("view"),
        NodeGraphStoreEvent::GraphCommitted { .. } => events2.borrow_mut().push("graph"),
    });

    let draw_order_snapshots: Rc<RefCell<Vec<Vec<NodeId>>>> = Rc::new(RefCell::new(Vec::new()));
    let draw_order_snapshots2 = draw_order_snapshots.clone();
    store.subscribe_selector(
        |s| s.view_state.draw_order.clone(),
        move |value| draw_order_snapshots2.borrow_mut().push(value.clone()),
    );

    store.update_view_state(|s| {
        s.draw_order = vec![b, a];
    });

    assert!(events.borrow().is_empty());
    assert_eq!(draw_order_snapshots.borrow().as_slice(), &[vec![b, a]]);
    assert_eq!(store.view_state().draw_order.as_slice(), &[b, a]);
}

#[test]
fn store_update_view_state_notifies_selectors_for_edge_draw_order_only_changes() {
    use std::cell::RefCell;
    use std::rc::Rc;

    let (g0, _a, _b, _out_port, _in_port, eid) = make_graph();
    let mut store = make_store(g0);

    let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
    let events2 = events.clone();
    store.subscribe(move |ev| match ev {
        NodeGraphStoreEvent::DocumentReplaced { .. } => events2.borrow_mut().push("document"),
        NodeGraphStoreEvent::ViewChanged { .. } => events2.borrow_mut().push("view"),
        NodeGraphStoreEvent::GraphCommitted { .. } => events2.borrow_mut().push("graph"),
    });

    let edge_draw_order_snapshots: Rc<RefCell<Vec<Vec<EdgeId>>>> =
        Rc::new(RefCell::new(Vec::new()));
    let edge_draw_order_snapshots2 = edge_draw_order_snapshots.clone();
    store.subscribe_selector(
        |s| s.view_state.edge_draw_order.clone(),
        move |value| edge_draw_order_snapshots2.borrow_mut().push(value.clone()),
    );

    store.update_view_state(|s| {
        s.edge_draw_order = vec![eid];
    });

    assert!(events.borrow().is_empty());
    assert_eq!(edge_draw_order_snapshots.borrow().as_slice(), &[vec![eid]]);
    assert_eq!(store.view_state().edge_draw_order.as_slice(), &[eid]);
}