Skip to main content

jellyflow_runtime/runtime/conformance/scenario/
behavior.rs

1use serde::{Deserialize, Serialize};
2
3use crate::io::NodeGraphKeyCode;
4use crate::runtime::connection::{CONNECT_EDGE_TRANSACTION_LABEL, ConnectEdgeRequest};
5use crate::runtime::delete::DELETE_SELECTION_TRANSACTION_LABEL;
6use crate::runtime::drag::NODE_DRAG_TRANSACTION_LABEL;
7use crate::runtime::drag::PointerGestureClaim;
8use crate::runtime::events::{
9    ConnectEnd, ConnectEndOutcome, ConnectStart, NodeDragEnd, NodeDragEndOutcome, NodeDragStart,
10    NodeDragUpdate, NodeGraphGestureEvent, NodeResizeEnd, NodeResizeEndOutcome, NodeResizeStart,
11    NodeResizeUpdate, ViewportMove, ViewportMoveEnd, ViewportMoveEndOutcome, ViewportMoveKind,
12    ViewportMoveStart,
13};
14use crate::runtime::measurement::NodeMeasurement;
15use crate::runtime::rendering::RenderingQueryResult;
16use crate::runtime::resize::NODE_RESIZE_TRANSACTION_LABEL;
17use crate::runtime::resize::NodePointerResizeRequest;
18use crate::runtime::selection::{NodePointerDownInput, SelectionBoxInput};
19use crate::runtime::viewport::{ViewportDragPanInput, ViewportGestureContext, ViewportTransform};
20use crate::runtime::xyflow::callbacks::{ConnectionChange, EdgeConnection};
21use jellyflow_core::core::{CanvasPoint, CanvasSize, EdgeId, GroupId, NodeId};
22use keyboard_types::Code as KeyCode;
23
24use super::action::{ConformanceAction, ConformanceLayoutFactsExpectation};
25use super::suite::ConformanceScenario;
26use super::trace::{ConformanceCallbackEvent, ConformanceTraceEvent, ConformanceViewChange};
27
28/// High-level conformance behavior that expands to runtime actions and expected trace events.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
31pub enum ConformanceBehavior {
32    NodeDragSession(ConformanceNodeDragSessionContract),
33    ConnectEdgeSession(ConformanceConnectEdgeSessionContract),
34    NodeResizeSession(ConformanceNodeResizeSessionContract),
35    SelectionBox(ConformanceSelectionBoxContract),
36    DeleteSelection(ConformanceDeleteSelectionContract),
37    DeleteSelectionDuringNodeDrag(ConformanceDeleteSelectionDuringNodeDragContract),
38    NodePointerDownSelection(ConformanceNodePointerDownSelectionContract),
39    ViewportDragPanSession(ConformanceViewportDragPanSessionContract),
40    RenderingQuery(ConformanceRenderingQueryContract),
41    LayoutFacts(ConformanceLayoutFactsContract),
42}
43
44impl ConformanceBehavior {
45    pub fn node_drag_session(contract: ConformanceNodeDragSessionContract) -> Self {
46        Self::NodeDragSession(contract)
47    }
48
49    pub fn connect_edge_session(contract: ConformanceConnectEdgeSessionContract) -> Self {
50        Self::ConnectEdgeSession(contract)
51    }
52
53    pub fn node_resize_session(contract: ConformanceNodeResizeSessionContract) -> Self {
54        Self::NodeResizeSession(contract)
55    }
56
57    pub fn selection_box(contract: ConformanceSelectionBoxContract) -> Self {
58        Self::SelectionBox(contract)
59    }
60
61    pub fn delete_selection(contract: ConformanceDeleteSelectionContract) -> Self {
62        Self::DeleteSelection(contract)
63    }
64
65    pub fn delete_selection_during_node_drag(
66        contract: ConformanceDeleteSelectionDuringNodeDragContract,
67    ) -> Self {
68        Self::DeleteSelectionDuringNodeDrag(contract)
69    }
70
71    pub fn node_pointer_down_selection(
72        contract: ConformanceNodePointerDownSelectionContract,
73    ) -> Self {
74        Self::NodePointerDownSelection(contract)
75    }
76
77    pub fn viewport_drag_pan_session(contract: ConformanceViewportDragPanSessionContract) -> Self {
78        Self::ViewportDragPanSession(contract)
79    }
80
81    pub fn rendering_query(contract: ConformanceRenderingQueryContract) -> Self {
82        Self::RenderingQuery(contract)
83    }
84
85    pub fn layout_facts(contract: ConformanceLayoutFactsContract) -> Self {
86        Self::LayoutFacts(contract)
87    }
88
89    pub(crate) fn actions(&self) -> Vec<ConformanceAction> {
90        match self {
91            Self::NodeDragSession(contract) => vec![contract.action()],
92            Self::ConnectEdgeSession(contract) => vec![contract.action()],
93            Self::NodeResizeSession(contract) => vec![contract.action()],
94            Self::SelectionBox(contract) => vec![contract.action()],
95            Self::DeleteSelection(contract) => vec![contract.action()],
96            Self::DeleteSelectionDuringNodeDrag(contract) => contract.actions(),
97            Self::NodePointerDownSelection(contract) => vec![contract.action()],
98            Self::ViewportDragPanSession(contract) => vec![contract.action()],
99            Self::RenderingQuery(contract) => vec![contract.action()],
100            Self::LayoutFacts(contract) => contract.actions(),
101        }
102    }
103
104    pub(crate) fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
105        match self {
106            Self::NodeDragSession(contract) => contract.expected_trace(),
107            Self::ConnectEdgeSession(contract) => contract.expected_trace(),
108            Self::NodeResizeSession(contract) => contract.expected_trace(),
109            Self::SelectionBox(contract) => contract.expected_trace(),
110            Self::DeleteSelection(contract) => contract.expected_trace(),
111            Self::DeleteSelectionDuringNodeDrag(contract) => contract.expected_trace(),
112            Self::NodePointerDownSelection(contract) => contract.expected_trace(),
113            Self::ViewportDragPanSession(contract) => contract.expected_trace(),
114            Self::RenderingQuery(contract) => contract.expected_trace(),
115            Self::LayoutFacts(contract) => contract.expected_trace(),
116        }
117    }
118}
119
120/// Behavior contract for a committed node drag session.
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct ConformanceNodeDragSessionContract {
123    pub primary: NodeId,
124    pub nodes: Vec<NodeId>,
125    pub start: CanvasPoint,
126    pub to: CanvasPoint,
127    pub commit_op_kinds: Vec<String>,
128}
129
130impl ConformanceNodeDragSessionContract {
131    pub fn new(primary: NodeId, start: CanvasPoint, to: CanvasPoint) -> Self {
132        Self {
133            primary,
134            nodes: vec![primary],
135            start,
136            to,
137            commit_op_kinds: vec!["set_node_pos".to_owned()],
138        }
139    }
140
141    pub fn with_nodes(mut self, nodes: impl IntoIterator<Item = NodeId>) -> Self {
142        self.nodes = nodes.into_iter().collect();
143        self
144    }
145
146    pub fn with_commit_op_kinds(
147        mut self,
148        op_kinds: impl IntoIterator<Item = impl Into<String>>,
149    ) -> Self {
150        self.commit_op_kinds = op_kinds.into_iter().map(Into::into).collect();
151        self
152    }
153
154    fn action(&self) -> ConformanceAction {
155        ConformanceAction::apply_node_drag_session(self.primary, self.start, self.to)
156    }
157
158    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
159        let start = NodeDragStart {
160            primary: self.primary,
161            nodes: self.nodes.clone(),
162            pointer: self.start,
163        };
164        let update = NodeDragUpdate {
165            primary: self.primary,
166            nodes: self.nodes.clone(),
167            pointer: self.to,
168        };
169        let end = NodeDragEnd {
170            primary: self.primary,
171            nodes: self.nodes.clone(),
172            pointer: self.to,
173            outcome: NodeDragEndOutcome::Committed,
174        };
175
176        vec![
177            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::NodeDragStart(start.clone())),
178            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeDragStart(start)),
179            ConformanceTraceEvent::graph_commit(
180                Some(NODE_DRAG_TRANSACTION_LABEL),
181                self.commit_op_kinds.iter().map(String::as_str),
182            ),
183            ConformanceTraceEvent::callback(ConformanceCallbackEvent::GraphCommit {
184                label: Some(NODE_DRAG_TRANSACTION_LABEL.to_owned()),
185            }),
186            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeEdgeChanges {
187                nodes: self.nodes.len(),
188                edges: 0,
189            }),
190            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodesChange {
191                count: self.nodes.len(),
192            }),
193            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::NodeDragUpdate(update.clone())),
194            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeDrag(update)),
195            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::NodeDragEnd(end.clone())),
196            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeDragEnd(end)),
197        ]
198    }
199}
200
201/// Behavior contract for a committed connect-edge session.
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203pub struct ConformanceConnectEdgeSessionContract {
204    pub start: ConnectStart,
205    pub request: ConnectEdgeRequest,
206    pub connection: EdgeConnection,
207}
208
209impl ConformanceConnectEdgeSessionContract {
210    pub fn new(
211        start: ConnectStart,
212        request: ConnectEdgeRequest,
213        connection: EdgeConnection,
214    ) -> Self {
215        Self {
216            start,
217            request,
218            connection,
219        }
220    }
221
222    fn action(&self) -> ConformanceAction {
223        ConformanceAction::apply_connect_edge_session(self.start.clone(), self.request)
224    }
225
226    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
227        let end = ConnectEnd {
228            kind: self.start.kind.clone(),
229            mode: self.start.mode,
230            target: Some(self.request.to),
231            outcome: ConnectEndOutcome::Committed,
232        };
233
234        vec![
235            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::ConnectStart(self.start.clone())),
236            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ConnectStart(
237                self.start.clone(),
238            )),
239            ConformanceTraceEvent::graph_commit(Some(CONNECT_EDGE_TRANSACTION_LABEL), ["add_edge"]),
240            ConformanceTraceEvent::callback(ConformanceCallbackEvent::GraphCommit {
241                label: Some(CONNECT_EDGE_TRANSACTION_LABEL.to_owned()),
242            }),
243            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeEdgeChanges {
244                nodes: 0,
245                edges: 1,
246            }),
247            ConformanceTraceEvent::callback(ConformanceCallbackEvent::EdgesChange { count: 1 }),
248            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ConnectionChange(
249                ConnectionChange::Connected(self.connection),
250            )),
251            ConformanceTraceEvent::callback(ConformanceCallbackEvent::Connect(self.connection)),
252            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::ConnectEnd(end.clone())),
253            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ConnectEnd(end)),
254        ]
255    }
256}
257
258/// Behavior contract for a committed pointer-driven node resize session.
259#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
260pub struct ConformanceNodeResizeSessionContract {
261    pub request: NodePointerResizeRequest,
262    pub update: NodeResizeUpdate,
263    pub commit_op_kinds: Vec<String>,
264}
265
266impl ConformanceNodeResizeSessionContract {
267    pub fn new(request: NodePointerResizeRequest, update: NodeResizeUpdate) -> Self {
268        Self {
269            request,
270            update,
271            commit_op_kinds: vec!["set_node_size".to_owned()],
272        }
273    }
274
275    pub fn with_commit_op_kinds(
276        mut self,
277        op_kinds: impl IntoIterator<Item = impl Into<String>>,
278    ) -> Self {
279        self.commit_op_kinds = op_kinds.into_iter().map(Into::into).collect();
280        self
281    }
282
283    fn action(&self) -> ConformanceAction {
284        ConformanceAction::apply_node_pointer_resize_session(self.request)
285    }
286
287    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
288        let start = NodeResizeStart {
289            node: self.request.node,
290            direction: self.request.direction,
291            pointer: self.request.start,
292        };
293        let end = NodeResizeEnd {
294            node: self.request.node,
295            direction: self.request.direction,
296            pointer: self.request.current,
297            outcome: NodeResizeEndOutcome::Committed,
298        };
299
300        vec![
301            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::NodeResizeStart(start.clone())),
302            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeResizeStart(start)),
303            ConformanceTraceEvent::graph_commit(
304                Some(NODE_RESIZE_TRANSACTION_LABEL),
305                self.commit_op_kinds.iter().map(String::as_str),
306            ),
307            ConformanceTraceEvent::callback(ConformanceCallbackEvent::GraphCommit {
308                label: Some(NODE_RESIZE_TRANSACTION_LABEL.to_owned()),
309            }),
310            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeEdgeChanges {
311                nodes: 1,
312                edges: 0,
313            }),
314            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodesChange { count: 1 }),
315            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::NodeResizeUpdate(
316                self.update.clone(),
317            )),
318            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeResize(
319                self.update.clone(),
320            )),
321            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::NodeResizeEnd(end.clone())),
322            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeResizeEnd(end)),
323        ]
324    }
325}
326
327/// Behavior contract for applying a marquee selection box and observing selection callbacks.
328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
329pub struct ConformanceSelectionBoxContract {
330    pub input: SelectionBoxInput,
331    pub nodes: Vec<NodeId>,
332    pub edges: Vec<EdgeId>,
333    #[serde(default, skip_serializing_if = "Vec::is_empty")]
334    pub groups: Vec<GroupId>,
335}
336
337impl ConformanceSelectionBoxContract {
338    pub fn new(
339        input: SelectionBoxInput,
340        nodes: impl IntoIterator<Item = NodeId>,
341        edges: impl IntoIterator<Item = EdgeId>,
342    ) -> Self {
343        Self {
344            input,
345            nodes: nodes.into_iter().collect(),
346            edges: edges.into_iter().collect(),
347            groups: Vec::new(),
348        }
349    }
350
351    pub fn with_groups(mut self, groups: impl IntoIterator<Item = GroupId>) -> Self {
352        self.groups = groups.into_iter().collect();
353        self
354    }
355
356    fn action(&self) -> ConformanceAction {
357        ConformanceAction::apply_selection_box(self.input)
358    }
359
360    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
361        selection_trace_events(&self.nodes, &self.edges, &self.groups)
362    }
363}
364
365fn selection_trace_events(
366    nodes: &[NodeId],
367    edges: &[EdgeId],
368    groups: &[GroupId],
369) -> Vec<ConformanceTraceEvent> {
370    vec![
371        ConformanceTraceEvent::selection(nodes.to_vec(), edges.to_vec(), groups.to_vec()),
372        ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
373            changes: vec![ConformanceViewChange::Selection {
374                nodes: nodes.to_vec(),
375                edges: edges.to_vec(),
376                groups: groups.to_vec(),
377            }],
378        }),
379        ConformanceTraceEvent::callback(ConformanceCallbackEvent::SelectionChange {
380            nodes: nodes.to_vec(),
381            edges: edges.to_vec(),
382            groups: groups.to_vec(),
383        }),
384    ]
385}
386
387fn usize_is_zero(value: &usize) -> bool {
388    *value == 0
389}
390
391fn default_delete_commit_op_kinds(nodes: usize, edges: usize) -> Vec<String> {
392    if nodes > 0 {
393        return vec!["remove_node".to_owned()];
394    }
395
396    if edges > 0 {
397        return vec!["remove_edge".to_owned()];
398    }
399
400    Vec::new()
401}
402
403/// Behavior contract for committing a delete-selection action and observing delete callbacks.
404#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
405pub struct ConformanceDeleteSelectionContract {
406    #[serde(default, skip_serializing_if = "Option::is_none")]
407    pub key: Option<NodeGraphKeyCode>,
408    pub nodes: usize,
409    pub edges: usize,
410    #[serde(default, skip_serializing_if = "usize_is_zero")]
411    pub groups: usize,
412    #[serde(default, skip_serializing_if = "usize_is_zero")]
413    pub sticky_notes: usize,
414    pub commit_op_kinds: Vec<String>,
415    #[serde(default, skip_serializing_if = "Vec::is_empty")]
416    pub disconnected: Vec<EdgeConnection>,
417}
418
419impl ConformanceDeleteSelectionContract {
420    pub fn new(nodes: usize, edges: usize) -> Self {
421        Self {
422            key: None,
423            nodes,
424            edges,
425            groups: 0,
426            sticky_notes: 0,
427            commit_op_kinds: default_delete_commit_op_kinds(nodes, edges),
428            disconnected: Vec::new(),
429        }
430    }
431
432    pub fn for_key(mut self, key: KeyCode) -> Self {
433        self.key = Some(NodeGraphKeyCode(key));
434        self
435    }
436
437    pub fn with_commit_op_kinds(
438        mut self,
439        op_kinds: impl IntoIterator<Item = impl Into<String>>,
440    ) -> Self {
441        self.commit_op_kinds = op_kinds.into_iter().map(Into::into).collect();
442        self
443    }
444
445    pub fn with_deleted_groups(mut self, groups: usize) -> Self {
446        self.groups = groups;
447        self
448    }
449
450    pub fn with_deleted_sticky_notes(mut self, sticky_notes: usize) -> Self {
451        self.sticky_notes = sticky_notes;
452        self
453    }
454
455    pub fn with_disconnected(
456        mut self,
457        disconnected: impl IntoIterator<Item = EdgeConnection>,
458    ) -> Self {
459        self.disconnected = disconnected.into_iter().collect();
460        self
461    }
462
463    fn action(&self) -> ConformanceAction {
464        match self.key {
465            Some(key) => ConformanceAction::apply_delete_selection_for_key(key.0),
466            None => ConformanceAction::apply_delete_selection(),
467        }
468    }
469
470    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
471        let mut trace = vec![
472            ConformanceTraceEvent::graph_commit(
473                Some(DELETE_SELECTION_TRANSACTION_LABEL),
474                self.commit_op_kinds.iter().map(String::as_str),
475            ),
476            ConformanceTraceEvent::callback(ConformanceCallbackEvent::GraphCommit {
477                label: Some(DELETE_SELECTION_TRANSACTION_LABEL.to_owned()),
478            }),
479            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeEdgeChanges {
480                nodes: self.nodes,
481                edges: self.edges,
482            }),
483        ];
484
485        if self.nodes > 0 {
486            trace.push(ConformanceTraceEvent::callback(
487                ConformanceCallbackEvent::NodesChange { count: self.nodes },
488            ));
489        }
490
491        if self.edges > 0 {
492            trace.push(ConformanceTraceEvent::callback(
493                ConformanceCallbackEvent::EdgesChange { count: self.edges },
494            ));
495        }
496
497        for connection in self.disconnected.iter().copied() {
498            trace.push(ConformanceTraceEvent::callback(
499                ConformanceCallbackEvent::ConnectionChange(ConnectionChange::Disconnected(
500                    connection,
501                )),
502            ));
503            trace.push(ConformanceTraceEvent::callback(
504                ConformanceCallbackEvent::Disconnect(connection),
505            ));
506        }
507
508        if self.nodes > 0 {
509            trace.push(ConformanceTraceEvent::callback(
510                ConformanceCallbackEvent::NodesDelete { count: self.nodes },
511            ));
512        }
513
514        if self.edges > 0 {
515            trace.push(ConformanceTraceEvent::callback(
516                ConformanceCallbackEvent::EdgesDelete { count: self.edges },
517            ));
518        }
519
520        if self.groups > 0 {
521            trace.push(ConformanceTraceEvent::callback(
522                ConformanceCallbackEvent::GroupsDelete { count: self.groups },
523            ));
524        }
525
526        if self.sticky_notes > 0 {
527            trace.push(ConformanceTraceEvent::callback(
528                ConformanceCallbackEvent::StickyNotesDelete {
529                    count: self.sticky_notes,
530                },
531            ));
532        }
533
534        trace.push(ConformanceTraceEvent::callback(
535            ConformanceCallbackEvent::Delete {
536                nodes: self.nodes,
537                edges: self.edges,
538                groups: self.groups,
539                sticky_notes: self.sticky_notes,
540            },
541        ));
542        trace.extend(selection_trace_events(&[], &[], &[]));
543
544        trace
545    }
546}
547
548/// Behavior contract for deleting the active selection mid-drag and then ending the drag as canceled.
549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
550pub struct ConformanceDeleteSelectionDuringNodeDragContract {
551    pub start: NodeDragStart,
552    pub end: NodeDragEnd,
553    pub delete: ConformanceDeleteSelectionContract,
554}
555
556impl ConformanceDeleteSelectionDuringNodeDragContract {
557    pub fn new(
558        start: NodeDragStart,
559        end: NodeDragEnd,
560        delete: ConformanceDeleteSelectionContract,
561    ) -> Self {
562        Self { start, end, delete }
563    }
564
565    fn actions(&self) -> Vec<ConformanceAction> {
566        vec![
567            ConformanceAction::emit_gesture(NodeGraphGestureEvent::NodeDragStart(
568                self.start.clone(),
569            )),
570            self.delete.action(),
571            ConformanceAction::emit_gesture(NodeGraphGestureEvent::NodeDragEnd(self.end.clone())),
572        ]
573    }
574
575    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
576        let mut trace = vec![
577            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::NodeDragStart(
578                self.start.clone(),
579            )),
580            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeDragStart(
581                self.start.clone(),
582            )),
583        ];
584        trace.extend(self.delete.expected_trace());
585        trace.extend([
586            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::NodeDragEnd(self.end.clone())),
587            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeDragEnd(
588                self.end.clone(),
589            )),
590        ]);
591        trace
592    }
593}
594
595/// Behavior contract for a node pointer-down selection update and drag claim assertion.
596#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
597pub struct ConformanceNodePointerDownSelectionContract {
598    pub input: NodePointerDownInput,
599    pub expected_claim: PointerGestureClaim,
600    pub nodes: Vec<NodeId>,
601    pub edges: Vec<EdgeId>,
602    #[serde(default, skip_serializing_if = "Vec::is_empty")]
603    pub groups: Vec<GroupId>,
604}
605
606impl ConformanceNodePointerDownSelectionContract {
607    pub fn new(
608        input: NodePointerDownInput,
609        expected_claim: PointerGestureClaim,
610        nodes: impl IntoIterator<Item = NodeId>,
611        edges: impl IntoIterator<Item = EdgeId>,
612    ) -> Self {
613        Self {
614            input,
615            expected_claim,
616            nodes: nodes.into_iter().collect(),
617            edges: edges.into_iter().collect(),
618            groups: Vec::new(),
619        }
620    }
621
622    pub fn with_groups(mut self, groups: impl IntoIterator<Item = GroupId>) -> Self {
623        self.groups = groups.into_iter().collect();
624        self
625    }
626
627    fn action(&self) -> ConformanceAction {
628        ConformanceAction::apply_node_pointer_down_expect_claim(
629            self.input.node,
630            self.input.multi_selection_active,
631            self.input.screen_delta,
632            self.expected_claim,
633        )
634    }
635
636    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
637        selection_trace_events(&self.nodes, &self.edges, &self.groups)
638    }
639}
640
641/// Behavior contract for an accepted viewport drag-pan session.
642#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
643pub struct ConformanceViewportDragPanSessionContract {
644    pub context: ViewportGestureContext,
645    pub input: ViewportDragPanInput,
646    pub start: ViewportTransform,
647    pub end: ViewportTransform,
648}
649
650impl ConformanceViewportDragPanSessionContract {
651    pub fn new(
652        context: ViewportGestureContext,
653        input: ViewportDragPanInput,
654        start: ViewportTransform,
655        end: ViewportTransform,
656    ) -> Self {
657        Self {
658            context,
659            input,
660            start,
661            end,
662        }
663    }
664
665    fn action(&self) -> ConformanceAction {
666        ConformanceAction::apply_viewport_drag_pan_session(self.context, self.input)
667    }
668
669    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
670        let start = ViewportMoveStart {
671            kind: ViewportMoveKind::PanDrag,
672            pan: self.start.pan,
673            zoom: self.start.zoom,
674        };
675        let update = ViewportMove {
676            kind: ViewportMoveKind::PanDrag,
677            pan: self.end.pan,
678            zoom: self.end.zoom,
679        };
680        let end = ViewportMoveEnd {
681            kind: ViewportMoveKind::PanDrag,
682            pan: self.end.pan,
683            zoom: self.end.zoom,
684            outcome: ViewportMoveEndOutcome::Ended,
685        };
686
687        vec![
688            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::ViewportMoveStart(start)),
689            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMoveStart(start)),
690            ConformanceTraceEvent::viewport(self.end.pan, self.end.zoom),
691            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
692                changes: vec![ConformanceViewChange::Viewport {
693                    pan: self.end.pan,
694                    zoom: self.end.zoom,
695                }],
696            }),
697            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
698                pan: self.end.pan,
699                zoom: self.end.zoom,
700            }),
701            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::ViewportMove(update)),
702            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMove(update)),
703            ConformanceTraceEvent::gesture(NodeGraphGestureEvent::ViewportMoveEnd(end)),
704            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMoveEnd(end)),
705        ]
706    }
707}
708
709/// Behavior contract for reading renderer-facing order and visibility in one store query.
710#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
711pub struct ConformanceRenderingQueryContract {
712    pub viewport_size: CanvasSize,
713    pub expected: RenderingQueryResult,
714}
715
716impl ConformanceRenderingQueryContract {
717    pub fn new(viewport_size: CanvasSize, expected: RenderingQueryResult) -> Self {
718        Self {
719            viewport_size,
720            expected,
721        }
722    }
723
724    fn action(&self) -> ConformanceAction {
725        ConformanceAction::assert_rendering_query(self.viewport_size, self.expected.clone())
726    }
727
728    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
729        Vec::new()
730    }
731}
732
733/// Behavior contract for reporting measurements once and reading derived layout facts.
734#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
735pub struct ConformanceLayoutFactsContract {
736    pub measurements: Vec<NodeMeasurement>,
737    pub viewport_size: CanvasSize,
738    pub expected: ConformanceLayoutFactsExpectation,
739}
740
741impl ConformanceLayoutFactsContract {
742    pub fn new(
743        measurements: impl IntoIterator<Item = NodeMeasurement>,
744        viewport_size: CanvasSize,
745        expected: ConformanceLayoutFactsExpectation,
746    ) -> Self {
747        Self {
748            measurements: measurements.into_iter().collect(),
749            viewport_size,
750            expected,
751        }
752    }
753
754    fn actions(&self) -> Vec<ConformanceAction> {
755        self.measurements
756            .iter()
757            .cloned()
758            .map(ConformanceAction::report_node_measurement)
759            .chain([ConformanceAction::assert_layout_facts(
760                self.viewport_size,
761                self.expected.clone(),
762            )])
763            .collect()
764    }
765
766    fn expected_trace(&self) -> Vec<ConformanceTraceEvent> {
767        Vec::new()
768    }
769}
770
771impl ConformanceScenario {
772    pub fn with_node_drag_session_contract(
773        self,
774        contract: ConformanceNodeDragSessionContract,
775    ) -> Self {
776        self.with_behavior(ConformanceBehavior::node_drag_session(contract))
777    }
778
779    pub fn with_connect_edge_session_contract(
780        self,
781        contract: ConformanceConnectEdgeSessionContract,
782    ) -> Self {
783        self.with_behavior(ConformanceBehavior::connect_edge_session(contract))
784    }
785
786    pub fn with_node_resize_session_contract(
787        self,
788        contract: ConformanceNodeResizeSessionContract,
789    ) -> Self {
790        self.with_behavior(ConformanceBehavior::node_resize_session(contract))
791    }
792
793    pub fn with_selection_box_contract(self, contract: ConformanceSelectionBoxContract) -> Self {
794        self.with_behavior(ConformanceBehavior::selection_box(contract))
795    }
796
797    pub fn with_delete_selection_contract(
798        self,
799        contract: ConformanceDeleteSelectionContract,
800    ) -> Self {
801        self.with_behavior(ConformanceBehavior::delete_selection(contract))
802    }
803
804    pub fn with_delete_selection_during_node_drag_contract(
805        self,
806        contract: ConformanceDeleteSelectionDuringNodeDragContract,
807    ) -> Self {
808        self.with_behavior(ConformanceBehavior::delete_selection_during_node_drag(
809            contract,
810        ))
811    }
812
813    pub fn with_node_pointer_down_selection_contract(
814        self,
815        contract: ConformanceNodePointerDownSelectionContract,
816    ) -> Self {
817        self.with_behavior(ConformanceBehavior::node_pointer_down_selection(contract))
818    }
819
820    pub fn with_viewport_drag_pan_session_contract(
821        self,
822        contract: ConformanceViewportDragPanSessionContract,
823    ) -> Self {
824        self.with_behavior(ConformanceBehavior::viewport_drag_pan_session(contract))
825    }
826
827    pub fn with_rendering_query_contract(
828        self,
829        contract: ConformanceRenderingQueryContract,
830    ) -> Self {
831        self.with_behavior(ConformanceBehavior::rendering_query(contract))
832    }
833
834    pub fn with_layout_facts_contract(self, contract: ConformanceLayoutFactsContract) -> Self {
835        self.with_behavior(ConformanceBehavior::layout_facts(contract))
836    }
837}