chartflow 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. chartflow/__init__.py +28 -0
  2. chartflow/_verify_importexport.py +169 -0
  3. chartflow/edit/__init__.py +119 -0
  4. chartflow/edit/code_editor.py +1007 -0
  5. chartflow/edit/code_folder.py +523 -0
  6. chartflow/edit/completer.py +2652 -0
  7. chartflow/edit/context_analyzer.py +521 -0
  8. chartflow/edit/demo.py +469 -0
  9. chartflow/edit/line_number_area.py +548 -0
  10. chartflow/edit/minimap.py +581 -0
  11. chartflow/edit/python_editor.py +509 -0
  12. chartflow/edit/syntax_highlighter.py +291 -0
  13. chartflow/edit/test_editor.py +190 -0
  14. chartflow/flow/__init__.py +31 -0
  15. chartflow/flow/_demo.py +98 -0
  16. chartflow/flow/chart.py +1101 -0
  17. chartflow/flow/editor.py +375 -0
  18. chartflow/flow/flow.py +6 -0
  19. chartflow/flow/qchart.py +1250 -0
  20. chartflow/flow/test/__init__.py +0 -0
  21. chartflow/flow/test/test_chart.py +537 -0
  22. chartflow/flow/test_decorator_sync.py +102 -0
  23. chartflow/fsm/__init__.py +84 -0
  24. chartflow/fsm/decorators.py +395 -0
  25. chartflow/fsm/demo.py +381 -0
  26. chartflow/fsm/demo_pyqt6.py +881 -0
  27. chartflow/fsm/demo_tree.py +46 -0
  28. chartflow/fsm/logic.py +447 -0
  29. chartflow/fsm/meta.py +569 -0
  30. chartflow/fsm/scheduler.py +427 -0
  31. chartflow/fsm/stage.py +2079 -0
  32. chartflow/fsm/stdio.py +66 -0
  33. chartflow/fsm/test/__init__.py +7 -0
  34. chartflow/fsm/test/test_decorators.py +210 -0
  35. chartflow/fsm/test/test_events.py +202 -0
  36. chartflow/fsm/test/test_integration.py +596 -0
  37. chartflow/fsm/test/test_logic.py +371 -0
  38. chartflow/fsm/test/test_meta.py +192 -0
  39. chartflow/fsm/test/test_runtime_param.py +336 -0
  40. chartflow/fsm/test/test_scheduler.py +280 -0
  41. chartflow/fsm/test/test_stage.py +314 -0
  42. chartflow/fsm/test_behavior_events.py +263 -0
  43. chartflow/fsm/test_nested_parallel.py +80 -0
  44. chartflow/fsm/test_stage_spec.py +448 -0
  45. chartflow/fsm/utils.py +34 -0
  46. chartflow/node/__init__.py +66 -0
  47. chartflow/node/_node_base.py +727 -0
  48. chartflow/node/_node_interaction.py +488 -0
  49. chartflow/node/_node_interface.py +426 -0
  50. chartflow/node/_node_markers.py +648 -0
  51. chartflow/node/_node_ports.py +536 -0
  52. chartflow/node/_port_interface.py +111 -0
  53. chartflow/node/arrow_item.py +287 -0
  54. chartflow/node/color_utils.py +113 -0
  55. chartflow/node/connection_item.py +939 -0
  56. chartflow/node/demo.py +258 -0
  57. chartflow/node/demo_arrow_drag.py +46 -0
  58. chartflow/node/demo_decorator.py +63 -0
  59. chartflow/node/demo_ports.py +69 -0
  60. chartflow/node/enums.py +35 -0
  61. chartflow/node/example.py +84 -0
  62. chartflow/node/layout_utils.py +307 -0
  63. chartflow/node/main_window.py +196 -0
  64. chartflow/node/menu_bar.py +240 -0
  65. chartflow/node/node_canvas.py +1730 -0
  66. chartflow/node/node_item.py +754 -0
  67. chartflow/node/popmenu.py +472 -0
  68. chartflow/node/port_item.py +591 -0
  69. chartflow/node/qnode_editor.py +73 -0
  70. chartflow/node/selection_rect.py +53 -0
  71. chartflow/node/style_panel.py +615 -0
  72. chartflow/node/styles.py +63 -0
  73. chartflow/node/test_qnode.py +2336 -0
  74. chartflow/showcase.py +528 -0
  75. chartflow/theme.py +508 -0
  76. chartflow-0.1.dist-info/METADATA +23 -0
  77. chartflow-0.1.dist-info/RECORD +79 -0
  78. chartflow-0.1.dist-info/WHEEL +5 -0
  79. chartflow-0.1.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,537 @@
1
+ """
2
+ qflow.chart module tests
3
+
4
+ Tests for IChart interface and FlowChart implementation.
5
+ """
6
+
7
+ import unittest
8
+ from typing import Any
9
+
10
+ from chartflow.flow.chart import IChart, ChartNode, ChartEdge, ChartLogic
11
+ from chartflow.flow.qchart import QFlowChart
12
+ from chartflow.flow._demo import DemoChart
13
+ from chartflow.fsm.stage import Stage
14
+ from chartflow.fsm.logic import Logic
15
+
16
+
17
+ # Pre-defined Stage class for source extraction tests
18
+ class TestExtractStage(Stage):
19
+ """Test Stage for code extraction tests."""
20
+ NAME = "TestExtractStage"
21
+
22
+ def idle(self, inst: Any) -> str:
23
+ x = 1
24
+ y = 2
25
+ return "working"
26
+
27
+ def working(self, inst: Any) -> str:
28
+ return "idle"
29
+
30
+
31
+ class TestChartNode(unittest.TestCase):
32
+ """Test ChartNode dataclass."""
33
+
34
+ def testDefaultCreation(self) -> None:
35
+ """Test creating ChartNode with defaults."""
36
+ node = ChartNode(nid="n1", name="TestNode")
37
+ self.assertEqual(node.nid, "n1")
38
+ self.assertEqual(node.name, "TestNode")
39
+ self.assertEqual(node.ntype, "state")
40
+ self.assertEqual(node.shape, "rectangle")
41
+ self.assertEqual(node.data, {})
42
+ self.assertEqual(node.states, [])
43
+
44
+ def testFullCreation(self) -> None:
45
+ """Test creating ChartNode with all fields."""
46
+ node = ChartNode(
47
+ nid="n2",
48
+ name="FullNode",
49
+ ntype="logic",
50
+ shape="ellipse",
51
+ data={"key": "value"},
52
+ states=["idle", "active"]
53
+ )
54
+ self.assertEqual(node.nid, "n2")
55
+ self.assertEqual(node.name, "FullNode")
56
+ self.assertEqual(node.ntype, "logic")
57
+ self.assertEqual(node.shape, "ellipse")
58
+ self.assertEqual(node.data["key"], "value")
59
+ self.assertEqual(node.states, ["idle", "active"])
60
+
61
+
62
+ class TestChartEdge(unittest.TestCase):
63
+ """Test ChartEdge dataclass."""
64
+
65
+ def testDefaultCreation(self) -> None:
66
+ """Test creating ChartEdge with defaults."""
67
+ edge = ChartEdge(eid="e1", src="n1", dst="n2")
68
+ self.assertEqual(edge.eid, "e1")
69
+ self.assertEqual(edge.src, "n1")
70
+ self.assertEqual(edge.dst, "n2")
71
+ self.assertEqual(edge.label, "")
72
+ self.assertEqual(edge.etype, "transition")
73
+ self.assertFalse(edge.bidirectional)
74
+
75
+ def testFullCreation(self) -> None:
76
+ """Test creating ChartEdge with all fields."""
77
+ edge = ChartEdge(
78
+ eid="e2",
79
+ src="n1",
80
+ dst="n3",
81
+ label="condition",
82
+ etype="flow",
83
+ bidirectional=True
84
+ )
85
+ self.assertEqual(edge.eid, "e2")
86
+ self.assertEqual(edge.label, "condition")
87
+ self.assertEqual(edge.etype, "flow")
88
+ self.assertTrue(edge.bidirectional)
89
+
90
+
91
+ class TestChartLogic(unittest.TestCase):
92
+ """Test ChartLogic dataclass."""
93
+
94
+ def testDefaultCreation(self) -> None:
95
+ """Test creating ChartLogic with defaults."""
96
+ logic = ChartLogic(node_id="n1")
97
+ self.assertEqual(logic.node_id, "n1")
98
+ self.assertEqual(logic.logic_type, "Stage")
99
+ self.assertEqual(logic.priority, 0)
100
+ self.assertEqual(logic.decorators, [])
101
+ self.assertEqual(logic.states, {})
102
+ self.assertEqual(logic.listeners, [])
103
+
104
+ def testFullCreation(self) -> None:
105
+ """Test creating ChartLogic with all fields."""
106
+ logic = ChartLogic(
107
+ node_id="n2",
108
+ logic_type="Logic",
109
+ priority=10,
110
+ decorators=[{"type": "sequence"}],
111
+ states={"idle": "pass"},
112
+ listeners=[{"event": "tick"}]
113
+ )
114
+ self.assertEqual(logic.node_id, "n2")
115
+ self.assertEqual(logic.logic_type, "Logic")
116
+ self.assertEqual(logic.priority, 10)
117
+ self.assertEqual(len(logic.decorators), 1)
118
+
119
+
120
+ class TestIChart(unittest.TestCase):
121
+ """Test IChart base class."""
122
+
123
+ def testInit(self) -> None:
124
+ """Test IChart initialization."""
125
+ chart = QFlowChart()
126
+ self.assertEqual(chart.nodes, [])
127
+ self.assertEqual(chart.edges, [])
128
+ self.assertEqual(chart.logics, [])
129
+ self.assertIsNone(chart.startnode)
130
+
131
+ def testAddNode(self) -> None:
132
+ """Test adding nodes."""
133
+ chart = QFlowChart()
134
+ node = ChartNode(nid="n1", name="Node1")
135
+ chart.addNode(node)
136
+ self.assertEqual(len(chart.nodes), 1)
137
+ self.assertEqual(chart.getNode("n1"), node)
138
+
139
+ def testRemoveNode(self) -> None:
140
+ """Test removing nodes."""
141
+ chart = QFlowChart()
142
+ node = ChartNode(nid="n1", name="Node1")
143
+ chart.addNode(node)
144
+ removed = chart.removeNode("n1")
145
+ self.assertEqual(removed, node)
146
+ self.assertIsNone(chart.getNode("n1"))
147
+
148
+ def testAddEdge(self) -> None:
149
+ """Test adding edges."""
150
+ chart = QFlowChart()
151
+ edge = ChartEdge(eid="e1", src="n1", dst="n2")
152
+ chart.addEdge(edge)
153
+ self.assertEqual(len(chart.edges), 1)
154
+ self.assertEqual(chart.getEdge("e1"), edge)
155
+
156
+ def testRemoveEdge(self) -> None:
157
+ """Test removing edges."""
158
+ chart = QFlowChart()
159
+ edge = ChartEdge(eid="e1", src="n1", dst="n2")
160
+ chart.addEdge(edge)
161
+ removed = chart.removeEdge("e1")
162
+ self.assertEqual(removed, edge)
163
+ self.assertIsNone(chart.getEdge("e1"))
164
+
165
+ def testGetOutgoing(self) -> None:
166
+ """Test getting outgoing edges."""
167
+ chart = QFlowChart()
168
+ chart.addNode(ChartNode(nid="n1", name="Node1"))
169
+ chart.addNode(ChartNode(nid="n2", name="Node2"))
170
+ chart.addNode(ChartNode(nid="n3", name="Node3"))
171
+ edge1 = ChartEdge(eid="e1", src="n1", dst="n2")
172
+ edge2 = ChartEdge(eid="e2", src="n1", dst="n3")
173
+ edge3 = ChartEdge(eid="e3", src="n2", dst="n3")
174
+ chart.addEdge(edge1)
175
+ chart.addEdge(edge2)
176
+ chart.addEdge(edge3)
177
+
178
+ outgoing = chart.getOutgoing("n1")
179
+ self.assertEqual(len(outgoing), 2)
180
+ self.assertIn(edge1, outgoing)
181
+ self.assertIn(edge2, outgoing)
182
+
183
+ def testGetIncoming(self) -> None:
184
+ """Test getting incoming edges."""
185
+ chart = QFlowChart()
186
+ chart.addNode(ChartNode(nid="n1", name="Node1"))
187
+ chart.addNode(ChartNode(nid="n2", name="Node2"))
188
+ chart.addNode(ChartNode(nid="n3", name="Node3"))
189
+ edge1 = ChartEdge(eid="e1", src="n1", dst="n3")
190
+ edge2 = ChartEdge(eid="e2", src="n2", dst="n3")
191
+ chart.addEdge(edge1)
192
+ chart.addEdge(edge2)
193
+
194
+ incoming = chart.getIncoming("n3")
195
+ self.assertEqual(len(incoming), 2)
196
+
197
+ def testGetConnected(self) -> None:
198
+ """Test getting connected nodes."""
199
+ chart = QFlowChart()
200
+ chart.addNode(ChartNode(nid="n1", name="Node1"))
201
+ chart.addNode(ChartNode(nid="n2", name="Node2"))
202
+ chart.addNode(ChartNode(nid="n3", name="Node3"))
203
+ chart.addEdge(ChartEdge(eid="e1", src="n1", dst="n2"))
204
+ chart.addEdge(ChartEdge(eid="e2", src="n1", dst="n3", bidirectional=True))
205
+
206
+ connected = chart.getConnected("n1")
207
+ self.assertEqual(len(connected), 2)
208
+ self.assertIn("n2", connected)
209
+ self.assertIn("n3", connected)
210
+
211
+ def testStartnode(self) -> None:
212
+ """Test start node property."""
213
+ chart = QFlowChart()
214
+ chart.startnode = "entry"
215
+ self.assertEqual(chart.startnode, "entry")
216
+
217
+ def testClearChart(self) -> None:
218
+ """Test clearing chart."""
219
+ chart = QFlowChart()
220
+ chart.addNode(ChartNode(nid="n1", name="Node1"))
221
+ chart.addEdge(ChartEdge(eid="e1", src="n1", dst="n2"))
222
+ chart.startnode = "n1"
223
+
224
+ chart.clearChart()
225
+ self.assertEqual(chart.nodes, [])
226
+ self.assertEqual(chart.edges, [])
227
+ self.assertIsNone(chart.startnode)
228
+
229
+ def testValidChart(self) -> None:
230
+ """Test chart validation."""
231
+ chart = QFlowChart()
232
+ chart.addNode(ChartNode(nid="n1", name="Node1"))
233
+ chart.addNode(ChartNode(nid="n2", name="Node2"))
234
+ chart.addEdge(ChartEdge(eid="e1", src="n1", dst="n2"))
235
+
236
+ valid, msg = chart.validChart()
237
+ self.assertTrue(valid)
238
+ self.assertEqual(msg, "")
239
+
240
+ def testValidChartMissingStart(self) -> None:
241
+ """Test validation with missing start node."""
242
+ chart = QFlowChart()
243
+ chart.startnode = "missing"
244
+
245
+ valid, msg = chart.validChart()
246
+ self.assertFalse(valid)
247
+ self.assertIn("Start node", msg)
248
+
249
+ def testValidChartMissingEdgeSrc(self) -> None:
250
+ """Test validation with missing edge source."""
251
+ chart = QFlowChart()
252
+ chart.addNode(ChartNode(nid="n1", name="Node1"))
253
+ chart.addEdge(ChartEdge(eid="e1", src="missing", dst="n1"))
254
+
255
+ valid, msg = chart.validChart()
256
+ self.assertFalse(valid)
257
+ self.assertIn("source", msg)
258
+
259
+
260
+ class TestFlowChartJson(unittest.TestCase):
261
+ """Test QFlowChart JSON import/export."""
262
+
263
+ def testFromJson(self) -> None:
264
+ """Test loading from JSON."""
265
+ json_data = {
266
+ "nodes": [
267
+ {"id": "n1", "name": "Entry", "shape": "ELLIPSE", "type": "entry", "data": {}},
268
+ {"id": "n2", "name": "State1", "shape": "RECTANGLE", "type": "state", "data": {}}
269
+ ],
270
+ "connections": [
271
+ {"source_id": "n1", "target_id": "n2", "is_bidirectional": False, "connection_type": "FLOW"}
272
+ ],
273
+ "start_node_id": "n1"
274
+ }
275
+
276
+ chart = QFlowChart()
277
+ chart.fromJson(json_data)
278
+
279
+ self.assertEqual(len(chart.nodes), 2)
280
+ self.assertEqual(len(chart.edges), 1)
281
+ self.assertEqual(chart.startnode, "n1")
282
+ self.assertEqual(chart.getNode("n1").name, "Entry")
283
+
284
+ def testToJson(self) -> None:
285
+ """Test exporting to JSON."""
286
+ chart = QFlowChart()
287
+ chart.addNode(ChartNode(nid="n1", name="Entry", ntype="entry", shape="ellipse"))
288
+ chart.addNode(ChartNode(nid="n2", name="State1", ntype="state", shape="rectangle"))
289
+ chart.addEdge(ChartEdge(eid="e1", src="n1", dst="n2", etype="flow"))
290
+ chart.startnode = "n1"
291
+
292
+ json_data = chart.toJson()
293
+
294
+ self.assertEqual(len(json_data["nodes"]), 2)
295
+ self.assertEqual(len(json_data["connections"]), 1)
296
+ self.assertEqual(json_data["start_node_id"], "n1")
297
+
298
+ def testRoundTripJson(self) -> None:
299
+ """Test JSON round-trip conversion."""
300
+ original = QFlowChart()
301
+ original.addNode(ChartNode(nid="n1", name="Entry", ntype="entry"))
302
+ original.addNode(ChartNode(nid="n2", name="Process", ntype="state"))
303
+ original.addEdge(ChartEdge(eid="e1", src="n1", dst="n2"))
304
+ original.startnode = "n1"
305
+
306
+ json_data = original.toJson()
307
+ restored = QFlowChart()
308
+ restored.fromJson(json_data)
309
+
310
+ self.assertEqual(len(restored.nodes), 2)
311
+ self.assertEqual(len(restored.edges), 1)
312
+ self.assertEqual(restored.startnode, "n1")
313
+
314
+
315
+ class TestFlowChartStage(unittest.TestCase):
316
+ """Test QFlowChart Stage conversion."""
317
+
318
+ def testToStage(self) -> None:
319
+ """Test converting to Stage instance."""
320
+ chart = DemoChart()
321
+ stage = chart.toStage()
322
+
323
+ self.assertIsInstance(stage, Stage)
324
+
325
+ def testFromStage(self) -> None:
326
+ """Test loading from Stage instance."""
327
+ class TestStage(Stage):
328
+ NAME = "TestStage"
329
+
330
+ def idle(self, inst: Any) -> str:
331
+ return "working"
332
+
333
+ def working(self, inst: Any) -> str:
334
+ return "idle"
335
+
336
+ original = TestStage()
337
+ chart = QFlowChart()
338
+ chart.fromStage(original)
339
+
340
+ self.assertGreater(len(chart.nodes), 0)
341
+
342
+
343
+ class TestFlowChartLogic(unittest.TestCase):
344
+ """Test QFlowChart Logic conversion."""
345
+
346
+ def testToLogic(self) -> None:
347
+ """Test converting to Logic instance."""
348
+ chart = DemoChart()
349
+ logic = chart.toLogic()
350
+
351
+ self.assertIsInstance(logic, Logic)
352
+
353
+ def testFromLogic(self) -> None:
354
+ """Test loading from Logic instance."""
355
+ class TestLogic(Logic):
356
+ NAME = "TestLogic"
357
+ PRIORITY = 5
358
+
359
+ def idle(self, inst: Any) -> str:
360
+ return "active"
361
+
362
+ def active(self, inst: Any) -> str:
363
+ return "idle"
364
+
365
+ original = TestLogic()
366
+ chart = QFlowChart()
367
+ chart.fromLogic(original)
368
+
369
+ self.assertGreater(len(chart.nodes), 0)
370
+ # Check priority metadata
371
+ for node in chart.nodes:
372
+ self.assertEqual(node.data.get("priority"), 5)
373
+
374
+
375
+ class TestExtractNodesCompatibility(unittest.TestCase):
376
+ """Test QFlowChart compatibility with extractNodes/_importDict format."""
377
+
378
+ def testFromStageExtractsCode(self) -> None:
379
+ """Test fromStage extracts function body code."""
380
+ # Use a pre-defined class from test file for reliable source extraction
381
+ stage = TestExtractStage()
382
+ chart = QFlowChart()
383
+ chart.fromStage(stage)
384
+
385
+ # Check code is extracted (code field exists)
386
+ idle_node = chart.getNode("state_0")
387
+ self.assertIsNotNone(idle_node)
388
+ self.assertIn("code", idle_node.data)
389
+ # Verify return statement is in code
390
+ self.assertIn('return "working"', idle_node.data["code"])
391
+ # Verify body statements are extracted
392
+ self.assertIn("x = 1", idle_node.data["code"])
393
+ self.assertIn("y = 2", idle_node.data["code"])
394
+
395
+ def testFromStageExtractsTargets(self) -> None:
396
+ """Test fromStage extracts return targets."""
397
+ class TestStage(Stage):
398
+ NAME = "TestStage"
399
+
400
+ def idle(self, inst: Any) -> str:
401
+ return "working"
402
+
403
+ def working(self, inst: Any) -> str:
404
+ return "idle"
405
+
406
+ stage = TestStage()
407
+ chart = QFlowChart()
408
+ chart.fromStage(stage)
409
+
410
+ idle_node = chart.getNode("state_0")
411
+ self.assertIn("targets", idle_node.data)
412
+ self.assertEqual(idle_node.data["targets"], ["working"])
413
+
414
+ working_node = chart.getNode("state_1")
415
+ self.assertEqual(working_node.data["targets"], ["idle"])
416
+
417
+ def testFromStageCreatesEdgesFromTargets(self) -> None:
418
+ """Test fromStage creates edges from extracted targets."""
419
+ class TestStage(Stage):
420
+ NAME = "TestStage"
421
+
422
+ def idle(self, inst: Any) -> str:
423
+ return "working"
424
+
425
+ def working(self, inst: Any) -> str:
426
+ return "idle"
427
+
428
+ stage = TestStage()
429
+ chart = QFlowChart()
430
+ chart.fromStage(stage)
431
+
432
+ # Should have edges: idle -> working, working -> idle
433
+ self.assertEqual(len(chart.edges), 2)
434
+
435
+ def testFromJsonExtractNodesFormat(self) -> None:
436
+ """Test fromJson can load extractNodes format."""
437
+ extract_nodes_data = {
438
+ "idle": {
439
+ "code": "x = 1\\nreturn 'working'",
440
+ "targets": ["working"],
441
+ "decorates": ["listen"]
442
+ },
443
+ "working": {
444
+ "code": "return 'idle'",
445
+ "targets": ["idle"],
446
+ "decorates": []
447
+ }
448
+ }
449
+
450
+ chart = QFlowChart()
451
+ chart.fromJson(extract_nodes_data)
452
+
453
+ self.assertEqual(len(chart.nodes), 2)
454
+
455
+ idle_node = chart.getNode("state_0")
456
+ self.assertEqual(idle_node.name, "idle")
457
+ self.assertEqual(idle_node.data["code"], "x = 1\\nreturn 'working'")
458
+ self.assertEqual(idle_node.data["targets"], ["working"])
459
+ self.assertEqual(idle_node.data["decorates"], ["listen"])
460
+
461
+ def testToJsonOutputsNodeData(self) -> None:
462
+ """Test toJson outputs node data including code/targets/decorates."""
463
+ chart = QFlowChart()
464
+ node = ChartNode(
465
+ nid="n1",
466
+ name="idle",
467
+ ntype="state",
468
+ data={
469
+ "code": "return 'working'",
470
+ "targets": ["working"],
471
+ "decorates": ["listen"]
472
+ }
473
+ )
474
+ chart.addNode(node)
475
+
476
+ json_data = chart.toJson()
477
+
478
+ self.assertEqual(len(json_data["nodes"]), 1)
479
+ self.assertEqual(json_data["nodes"][0]["data"]["code"], "return 'working'")
480
+ self.assertEqual(json_data["nodes"][0]["data"]["targets"], ["working"])
481
+ self.assertEqual(json_data["nodes"][0]["data"]["decorates"], ["listen"])
482
+
483
+ def testExtractNodesRoundTrip(self) -> None:
484
+ """Test extractNodes -> fromJson round trip preserves data."""
485
+ original_data = {
486
+ "idle": {
487
+ "code": "x = 1\\ny = 2\\nreturn 'working'",
488
+ "targets": ["working"],
489
+ "decorates": []
490
+ },
491
+ "working": {
492
+ "code": "return 'idle'",
493
+ "targets": ["idle"],
494
+ "decorates": ["sequence"]
495
+ }
496
+ }
497
+
498
+ chart = QFlowChart()
499
+ chart.fromJson(original_data)
500
+
501
+ # Verify nodes created correctly
502
+ self.assertEqual(len(chart.nodes), 2)
503
+
504
+ # Verify edges created from targets
505
+ self.assertEqual(len(chart.edges), 2)
506
+
507
+ # Get node by name
508
+ idle_node = None
509
+ for node in chart.nodes:
510
+ if node.name == "idle":
511
+ idle_node = node
512
+ break
513
+
514
+ self.assertIsNotNone(idle_node)
515
+ self.assertEqual(idle_node.data["code"], "x = 1\\ny = 2\\nreturn 'working'")
516
+
517
+
518
+ class TestDemoChart(unittest.TestCase):
519
+ """Test DemoChart factory function."""
520
+
521
+ def testDemoChart(self) -> None:
522
+ """Test creating demo chart."""
523
+ chart = DemoChart()
524
+
525
+ self.assertEqual(len(chart.nodes), 4)
526
+ self.assertEqual(len(chart.edges), 4)
527
+ self.assertEqual(chart.startnode, "entry")
528
+
529
+ def testDemoChartValid(self) -> None:
530
+ """Test demo chart is valid."""
531
+ chart = DemoChart()
532
+ valid, msg = chart.validChart()
533
+ self.assertTrue(valid, msg)
534
+
535
+
536
+ if __name__ == "__main__":
537
+ unittest.main()
@@ -0,0 +1,102 @@
1
+ """Test decorator synchronization from Stage/Logic to qnode."""
2
+
3
+ from PyQt6.QtGui import QColor
4
+
5
+
6
+ import sys
7
+ import unittest
8
+ from PyQt6.QtWidgets import QApplication
9
+ from PyQt6.QtCore import QPointF
10
+
11
+ from chartflow.fsm import Stage, Logic, recursive
12
+ from chartflow.flow import QFlowChart
13
+ from chartflow.node import QNodeCanvas, QNodeItem, NodeShape
14
+
15
+
16
+ class TestStage(Stage):
17
+ """Test stage with recursive entry state."""
18
+ NAME = "TestStage"
19
+
20
+ @recursive
21
+ def entry(self, inst, c, k):
22
+ return None
23
+
24
+ def normal(self, inst, c, k):
25
+ return None
26
+
27
+
28
+ class TestLogic(Logic):
29
+ """Test logic with recursive and normal states."""
30
+ NAME = "TestLogic"
31
+
32
+ @recursive
33
+ def entry(self, inst, c, k, *refs):
34
+ return None
35
+
36
+ def process(self, inst, c, k, *refs):
37
+ return None
38
+
39
+
40
+ class TestDecoratorSync(unittest.TestCase):
41
+ """Test decorator synchronization from Stage/Logic to qnode nodes."""
42
+
43
+ @classmethod
44
+ def setUpClass(cls):
45
+ cls.app = QApplication.instance()
46
+ if cls.app is None:
47
+ cls.app = QApplication(sys.argv)
48
+
49
+ def testStageRecursiveDecorator(self):
50
+ """Test that recursive state gets decorator in QFlowChart."""
51
+ chart = QFlowChart()
52
+ stage = TestStage()
53
+ chart.setStage(stage)
54
+
55
+ # Check that entry node has recursive decorator
56
+ entry_node = chart._nodemap.get("entry")
57
+ self.assertIsNotNone(entry_node)
58
+ self.assertEqual(entry_node.decorator, "recursive")
59
+
60
+ # Check that normal node does not have decorator
61
+ normal_node = chart._nodemap.get("normal")
62
+ self.assertIsNotNone(normal_node)
63
+ self.assertIsNone(normal_node.decorator)
64
+
65
+ def testLogicRecursiveDecorator(self):
66
+ """Test that recursive state gets decorator in QFlowChart from Logic."""
67
+ chart = QFlowChart()
68
+ logic = TestLogic()
69
+ chart.setStage(logic)
70
+
71
+ # Check that entry node has recursive decorator
72
+ entry_node = chart._nodemap.get("entry")
73
+ self.assertIsNotNone(entry_node)
74
+ self.assertEqual(entry_node.decorator, "recursive")
75
+
76
+ # Check that process node does not have decorator
77
+ process_node = chart._nodemap.get("process")
78
+ self.assertIsNotNone(process_node)
79
+ self.assertIsNone(process_node.decorator)
80
+
81
+ def testDecoratorColorApplied(self):
82
+ """Test that decorator color is actually applied."""
83
+
84
+ chart = QFlowChart()
85
+ stage = TestStage()
86
+ chart.setStage(stage)
87
+
88
+ entry_node = chart._nodemap.get("entry")
89
+ self.assertIsNotNone(entry_node)
90
+
91
+ # Get decorator color
92
+ color = entry_node._getDecoratorStrokeColor()
93
+ self.assertIsNotNone(color)
94
+
95
+ # Should be violet for recursive
96
+ self.assertEqual(color.red(), 238)
97
+ self.assertEqual(color.green(), 130)
98
+ self.assertEqual(color.blue(), 238)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ unittest.main()