@@ -868,3 +868,153 @@ def test_invoke_with_text_content(self):
868868 assert "s1" in definition .states
869869 invoke_def = definition .states ["s1" ].invocations [0 ]
870870 assert "some text content" in invoke_def .content
871+
872+ def test_invoke_with_content_expr (self ):
873+ """<invoke> <content expr="..."> is parsed as dynamic content."""
874+ scxml = """
875+ <scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
876+ <state id="s1">
877+ <invoke type="http://www.w3.org/TR/scxml/">
878+ <content expr="'dynamic'"/>
879+ </invoke>
880+ </state>
881+ </scxml>
882+ """
883+ definition = parse_scxml (scxml )
884+ invoke_def = definition .states ["s1" ].invocations [0 ]
885+ assert invoke_def .content == "'dynamic'"
886+
887+ def test_invoke_with_inline_scxml_no_namespace (self ):
888+ """<invoke> <content> with inline <scxml> (no namespace) is parsed."""
889+ scxml = """
890+ <scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
891+ <state id="s1">
892+ <invoke type="http://www.w3.org/TR/scxml/">
893+ <content><scxml><final id="f"/></scxml></content>
894+ </invoke>
895+ </state>
896+ </scxml>
897+ """
898+ definition = parse_scxml (scxml )
899+ invoke_def = definition .states ["s1" ].invocations [0 ]
900+ assert "<final" in invoke_def .content
901+
902+ def test_invoke_with_empty_content (self ):
903+ """<invoke> with empty <content/> results in content=None."""
904+ scxml = """
905+ <scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
906+ <state id="s1">
907+ <invoke type="http://www.w3.org/TR/scxml/">
908+ <content/>
909+ </invoke>
910+ </state>
911+ </scxml>
912+ """
913+ definition = parse_scxml (scxml )
914+ invoke_def = definition .states ["s1" ].invocations [0 ]
915+ assert invoke_def .content is None
916+
917+ def test_invoke_with_finalize_block (self ):
918+ """<invoke> with <finalize> block is parsed."""
919+ scxml = """
920+ <scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
921+ <state id="s1">
922+ <invoke type="http://www.w3.org/TR/scxml/">
923+ <content>child content</content>
924+ <finalize>
925+ <log label="finalized"/>
926+ </finalize>
927+ </invoke>
928+ </state>
929+ </scxml>
930+ """
931+ definition = parse_scxml (scxml )
932+ invoke_def = definition .states ["s1" ].invocations [0 ]
933+ assert invoke_def .finalize is not None
934+ assert len (invoke_def .finalize .actions ) == 1
935+
936+
937+ class TestParserAssignEdgeCases :
938+ def test_assign_without_children_or_text (self ):
939+ """<assign> with neither children nor text results in expr=None."""
940+ scxml = """
941+ <scxml xmlns="http://www.w3.org/2005/07/scxml" initial="s1">
942+ <datamodel>
943+ <data id="mydata" expr="0"/>
944+ </datamodel>
945+ <state id="s1">
946+ <onentry>
947+ <assign location="mydata"/>
948+ </onentry>
949+ <transition event="error.execution" target="err"/>
950+ </state>
951+ <final id="err"/>
952+ </scxml>
953+ """
954+ definition = parse_scxml (scxml )
955+ assert "s1" in definition .states
956+
957+
958+ class TestSCXMLInvokerResolveContentAbsolutePath :
959+ def test_resolve_content_absolute_path (self , tmp_path ):
960+ """_resolve_content with absolute src path doesn't prepend base_dir."""
961+ scxml_file = tmp_path / "child.scxml"
962+ scxml_file .write_text ("<scxml/>" )
963+
964+ defn = InvokeDefinition (src = str (scxml_file ))
965+ invoker = _make_invoker (definition = defn , base_dir = "/some/other/dir" )
966+
967+ result = invoker ._resolve_content (Mock ())
968+ assert result == "<scxml/>"
969+
970+
971+ class TestSCXMLInvokerEvaluateParamsNoExprNoLocation :
972+ def test_param_without_expr_or_location_skipped (self ):
973+ """_evaluate_params skips params with neither expr nor location."""
974+ defn = InvokeDefinition (
975+ params = [Param (name = "p1" , expr = None , location = None )],
976+ )
977+ invoker = _make_invoker (definition = defn )
978+ machine = Mock (model = type ("M" , (), {})())
979+ machine .model .__dict__ = {}
980+
981+ result = invoker ._evaluate_params (machine )
982+ assert result == {}
983+
984+
985+ class TestInvokeInitMachineNone :
986+ def test_invoke_init_without_machine_is_noop (self ):
987+ """invoke_init does nothing when machine is not in kwargs."""
988+ from statemachine .io .scxml .actions import create_invoke_init_callable
989+
990+ callback = create_invoke_init_callable ()
991+ # Call without machine kwarg — should not raise
992+ callback ()
993+
994+
995+ class TestInvokeCallableWrapperRunInstance :
996+ def test_run_with_instance_not_class (self ):
997+ """_InvokeCallableWrapper.run() works with an instance (not a class)."""
998+ from statemachine .invoke import _InvokeCallableWrapper
999+
1000+ class Handler :
1001+ def run (self , ctx ):
1002+ return "result"
1003+
1004+ handler_instance = Handler ()
1005+ wrapper = _InvokeCallableWrapper (handler_instance )
1006+ assert not wrapper ._is_class
1007+
1008+ ctx = Mock ()
1009+ result = wrapper .run (ctx )
1010+ assert result == "result"
1011+ assert wrapper ._instance is handler_instance
1012+
1013+
1014+ class TestOrderedSetStr :
1015+ def test_str_representation (self ):
1016+ """OrderedSet.__str__ returns a set-like string."""
1017+ from statemachine .orderedset import OrderedSet
1018+
1019+ os = OrderedSet ([1 , 2 , 3 ])
1020+ assert str (os ) == "{1, 2, 3}"
0 commit comments