jellyflow-runtime 0.2.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use super::super::fixtures::default_editor_config;
use super::super::harness::{HarnessEvent, InteractionHarness};
use super::support::drag_fixture;

use crate::io::NodeGraphNodeOrigin;
use crate::runtime::drag::{
    NODE_DRAG_TRANSACTION_LABEL, NodeDragItem, NodeDragRequest, plan_node_drag,
};
use crate::runtime::store::NodeGraphStore;
use jellyflow_core::core::{CanvasPoint, CanvasRect, CanvasSize, NodeExtent, NodeOrigin};
use jellyflow_core::ops::GraphOp;

#[test]
fn single_node_drag_commits_set_node_pos_transaction_and_trace() {
    let fixture = drag_fixture();
    let mut harness = InteractionHarness::new("single node drag", fixture.graph);
    let target = CanvasPoint { x: 30.0, y: 40.0 };

    let plan = plan_node_drag(
        harness.store().graph(),
        harness.store().view_state(),
        &harness.store().resolved_interaction_state(),
        NodeDragRequest {
            node: fixture.enabled,
            to: target,
        },
    )
    .expect("enabled node drag plan");

    assert_eq!(plan.node, fixture.enabled);
    assert_eq!(plan.from, CanvasPoint { x: 10.0, y: 20.0 });
    assert_eq!(plan.to, target);
    assert_eq!(
        plan.transaction().label(),
        Some(NODE_DRAG_TRANSACTION_LABEL)
    );
    assert!(
        matches!(
            plan.transaction().ops(),
            [GraphOp::SetNodePos { id, from, to }]
                if *id == fixture.enabled
                    && *from == CanvasPoint { x: 10.0, y: 20.0 }
                    && *to == target
        ),
        "drag plan should be a single SetNodePos op: {:#?}",
        plan.transaction().ops(),
    );

    let outcome = harness
        .store_mut()
        .apply_node_drag(NodeDragRequest {
            node: fixture.enabled,
            to: target,
        })
        .expect("drag dispatch succeeds")
        .expect("drag dispatch commits");

    assert_eq!(
        outcome.committed().label(),
        Some(NODE_DRAG_TRANSACTION_LABEL)
    );
    assert_eq!(
        harness.store().graph().nodes[&fixture.enabled].pos,
        CanvasPoint { x: 30.0, y: 40.0 },
    );
    harness.assert_events(&[HarnessEvent::graph_commit(
        Some(NODE_DRAG_TRANSACTION_LABEL),
        ["set_node_pos"],
    )]);
}

#[test]
fn single_node_drag_skips_non_draggable_hidden_noop_missing_and_invalid_targets() {
    let fixture = drag_fixture();
    let store = NodeGraphStore::new(fixture.graph, Default::default(), default_editor_config());
    let interaction = store.resolved_interaction_state();

    for request in [
        NodeDragRequest {
            node: fixture.disabled,
            to: CanvasPoint { x: 30.0, y: 40.0 },
        },
        NodeDragRequest {
            node: fixture.hidden,
            to: CanvasPoint { x: 30.0, y: 40.0 },
        },
        NodeDragRequest {
            node: fixture.missing,
            to: CanvasPoint { x: 30.0, y: 40.0 },
        },
        NodeDragRequest {
            node: fixture.enabled,
            to: CanvasPoint { x: 10.0, y: 20.0 },
        },
        NodeDragRequest {
            node: fixture.enabled,
            to: CanvasPoint {
                x: f32::INFINITY,
                y: 40.0,
            },
        },
    ] {
        assert!(
            plan_node_drag(store.graph(), store.view_state(), &interaction, request).is_none(),
            "request should not produce a drag plan: {request:?}",
        );
    }
}

#[test]
fn single_node_drag_respects_global_nodes_draggable_policy_without_committing() {
    let fixture = drag_fixture();
    let mut editor_config = default_editor_config();
    editor_config.interaction.nodes_draggable = false;
    let mut harness = InteractionHarness::new("global drag disabled", fixture.graph);
    harness.store_mut().replace_editor_config(editor_config);

    let result = harness
        .store_mut()
        .apply_node_drag(NodeDragRequest {
            node: fixture.enabled,
            to: CanvasPoint { x: 30.0, y: 40.0 },
        })
        .expect("disabled drag does not error");

    assert!(result.is_none());
    assert_eq!(
        harness.store().graph().nodes[&fixture.enabled].pos,
        CanvasPoint { x: 10.0, y: 20.0 },
    );
    harness.assert_events(&[]);
}

#[test]
fn single_node_drag_clamps_per_node_rect_with_node_origin() {
    let mut fixture = drag_fixture();
    let node = fixture.graph.nodes.get_mut(&fixture.enabled).unwrap();
    node.size = Some(CanvasSize {
        width: 20.0,
        height: 10.0,
    });
    node.extent = Some(NodeExtent::Rect {
        rect: CanvasRect {
            origin: CanvasPoint { x: 0.0, y: 0.0 },
            size: CanvasSize {
                width: 100.0,
                height: 60.0,
            },
        },
    });

    let mut harness = InteractionHarness::new("single node per-node extent", fixture.graph);
    harness.store_mut().update_editor_config(|editor_config| {
        editor_config.interaction.node_origin = NodeGraphNodeOrigin { x: 0.5, y: 0.5 };
    });

    let plan = harness
        .store()
        .plan_node_drag(NodeDragRequest {
            node: fixture.enabled,
            to: CanvasPoint { x: 95.0, y: 55.0 },
        })
        .expect("per-node extent drag plan");

    assert_eq!(plan.to, CanvasPoint { x: 90.0, y: 55.0 });
    assert_eq!(
        plan.items(),
        &[NodeDragItem {
            node: fixture.enabled,
            from: CanvasPoint { x: 10.0, y: 20.0 },
            to: CanvasPoint { x: 90.0, y: 55.0 },
        }],
    );
}

#[test]
fn single_node_drag_uses_node_origin_override_for_extent_clamping() {
    let mut fixture = drag_fixture();
    let node = fixture.graph.nodes.get_mut(&fixture.enabled).unwrap();
    node.size = Some(CanvasSize {
        width: 20.0,
        height: 10.0,
    });
    node.origin = Some(NodeOrigin { x: 1.0, y: 1.0 });
    node.extent = Some(NodeExtent::Rect {
        rect: CanvasRect {
            origin: CanvasPoint { x: 0.0, y: 0.0 },
            size: CanvasSize {
                width: 100.0,
                height: 60.0,
            },
        },
    });

    let mut harness = InteractionHarness::new("single node per-node origin", fixture.graph);
    harness.store_mut().update_editor_config(|editor_config| {
        editor_config.interaction.node_origin = NodeGraphNodeOrigin { x: 0.5, y: 0.5 };
    });

    let plan = harness
        .store()
        .plan_node_drag(NodeDragRequest {
            node: fixture.enabled,
            to: CanvasPoint { x: 95.0, y: 55.0 },
        })
        .expect("per-node origin drag plan");

    assert_eq!(plan.to, CanvasPoint { x: 95.0, y: 55.0 });
    assert_eq!(
        plan.items(),
        &[NodeDragItem {
            node: fixture.enabled,
            from: CanvasPoint { x: 10.0, y: 20.0 },
            to: CanvasPoint { x: 95.0, y: 55.0 },
        }],
    );
}