flowproof 0.23.0__tar.gz → 0.23.2__tar.gz

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 (163) hide show
  1. {flowproof-0.23.0 → flowproof-0.23.2}/Cargo.lock +7 -7
  2. {flowproof-0.23.0 → flowproof-0.23.2}/Cargo.toml +1 -1
  3. {flowproof-0.23.0 → flowproof-0.23.2}/PKG-INFO +1 -1
  4. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/lib.rs +3 -0
  5. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/web.rs +403 -25
  6. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/a11y_capture.rs +8 -0
  7. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/drag_spike.rs +6 -0
  8. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/network_idle_e2e.rs +8 -0
  9. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/shared_browser_shutdown_e2e.rs +2 -0
  10. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/static_area_e2e.rs +4 -0
  11. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/author.rs +92 -1
  12. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/agent_flow.rs +165 -0
  13. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/lib.rs +6 -18
  14. flowproof-0.23.2/crates/flowproof-cli/tests/agent_conversation_anthropic_e2e.rs +264 -0
  15. flowproof-0.23.2/crates/flowproof-cli/tests/agent_conversation_streaming_e2e.rs +373 -0
  16. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/examples_resolve.rs +57 -0
  17. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/web_e2e.rs +181 -0
  18. flowproof-0.23.2/crates/flowproof-trace/schema/cassette-v1.schema.json +127 -0
  19. flowproof-0.23.2/crates/flowproof-trace/tests/cassette_conformance.rs +170 -0
  20. {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/__init__.py +1 -1
  21. {flowproof-0.23.0 → flowproof-0.23.2}/pyproject.toml +1 -1
  22. {flowproof-0.23.0 → flowproof-0.23.2}/README.md +0 -0
  23. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/Cargo.toml +0 -0
  24. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
  25. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
  26. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress.rs +0 -0
  27. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
  28. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
  29. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
  30. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/env_block.rs +0 -0
  31. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
  32. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
  33. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
  34. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
  35. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
  36. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
  37. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
  38. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
  39. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/fiori.rs +0 -0
  40. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
  41. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
  42. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
  43. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
  44. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/sap_com.rs +0 -0
  45. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/vision.rs +0 -0
  46. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
  47. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/side_effect_http_e2e.rs +0 -0
  48. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +0 -0
  49. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/Cargo.toml +0 -0
  50. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/agent_steps.rs +0 -0
  51. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/clarify.rs +0 -0
  52. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/doc_author.rs +0 -0
  53. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/doc_formats/hp_alm.rs +0 -0
  54. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/doc_formats.rs +0 -0
  55. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/draft_assembly.rs +0 -0
  56. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/heal.rs +0 -0
  57. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/lib.rs +0 -0
  58. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/llm.rs +0 -0
  59. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/recorder.rs +0 -0
  60. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/repair.rs +0 -0
  61. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/rules.rs +0 -0
  62. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/spec.rs +0 -0
  63. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
  64. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/Cargo.toml +0 -0
  65. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/skills/flowproof-config/SKILL.md +0 -0
  66. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/capture.rs +0 -0
  67. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/config.rs +0 -0
  68. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/doctor.rs +0 -0
  69. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/main.rs +0 -0
  70. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/suite.rs +0 -0
  71. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/suite_checkpoint.rs +0 -0
  72. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/suite_inputs.rs +0 -0
  73. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/suite_journal.rs +0 -0
  74. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/update_check.rs +0 -0
  75. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/a11y_selector_e2e.rs +0 -0
  76. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
  77. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
  78. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
  79. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
  80. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
  81. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
  82. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/config_cli_e2e.rs +0 -0
  83. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/config_seed_e2e.rs +0 -0
  84. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/config_skill_e2e.rs +0 -0
  85. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
  86. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/doctor_ai_e2e.rs +0 -0
  87. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
  88. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/documented_flags.rs +0 -0
  89. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
  90. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
  91. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
  92. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
  93. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
  94. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
  95. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
  96. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
  97. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
  98. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_side_effect.rs +0 -0
  99. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/fiori_field_commit_e2e.rs +0 -0
  100. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
  101. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
  102. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
  103. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
  104. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
  105. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
  106. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
  107. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
  108. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
  109. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
  110. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
  111. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
  112. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
  113. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
  114. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
  115. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_checkpoint.rs +0 -0
  116. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
  117. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
  118. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
  119. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_policy.rs +0 -0
  120. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
  121. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
  122. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
  123. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/Cargo.toml +0 -0
  124. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/app.rs +0 -0
  125. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/backend.rs +0 -0
  126. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/gdi.rs +0 -0
  127. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/lib.rs +0 -0
  128. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/mock.rs +0 -0
  129. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/oob.rs +0 -0
  130. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/recording.rs +0 -0
  131. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/redact.rs +0 -0
  132. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/surface.rs +0 -0
  133. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/visual.rs +0 -0
  134. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/window.rs +0 -0
  135. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-python/Cargo.toml +0 -0
  136. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-python/src/lib.rs +0 -0
  137. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/Cargo.toml +0 -0
  138. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/src/lib.rs +0 -0
  139. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/src/report.rs +0 -0
  140. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/src/runrecord.rs +0 -0
  141. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
  142. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/Cargo.toml +0 -0
  143. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/schema/side-effect-v1.schema.json +0 -0
  144. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
  145. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/captures.rs +0 -0
  146. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/cassette.rs +0 -0
  147. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
  148. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/egress.rs +0 -0
  149. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/format.rs +0 -0
  150. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/lib.rs +0 -0
  151. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/secret.rs +0 -0
  152. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/secret_scan.rs +0 -0
  153. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/side_effect.rs +0 -0
  154. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/substitution.rs +0 -0
  155. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/toolcalls.rs +0 -0
  156. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/tests/fixtures/multi.trace.jsonl +0 -0
  157. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
  158. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
  159. {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/tests/side_effect_conformance.rs +0 -0
  160. {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/cli.py +0 -0
  161. {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/flow.py +0 -0
  162. {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/mcp_server.py +0 -0
  163. {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/py.typed +0 -0
@@ -930,7 +930,7 @@ dependencies = [
930
930
 
931
931
  [[package]]
932
932
  name = "flowproof-adapters"
933
- version = "0.23.0"
933
+ version = "0.23.2"
934
934
  dependencies = [
935
935
  "anyhow",
936
936
  "flowproof-driver",
@@ -949,7 +949,7 @@ dependencies = [
949
949
 
950
950
  [[package]]
951
951
  name = "flowproof-agent"
952
- version = "0.23.0"
952
+ version = "0.23.2"
953
953
  dependencies = [
954
954
  "chrono",
955
955
  "flowproof-driver",
@@ -967,7 +967,7 @@ dependencies = [
967
967
 
968
968
  [[package]]
969
969
  name = "flowproof-cli"
970
- version = "0.23.0"
970
+ version = "0.23.2"
971
971
  dependencies = [
972
972
  "ab_glyph",
973
973
  "chrono",
@@ -992,7 +992,7 @@ dependencies = [
992
992
 
993
993
  [[package]]
994
994
  name = "flowproof-driver"
995
- version = "0.23.0"
995
+ version = "0.23.2"
996
996
  dependencies = [
997
997
  "calamine",
998
998
  "flowproof-trace",
@@ -1008,7 +1008,7 @@ dependencies = [
1008
1008
 
1009
1009
  [[package]]
1010
1010
  name = "flowproof-python"
1011
- version = "0.23.0"
1011
+ version = "0.23.2"
1012
1012
  dependencies = [
1013
1013
  "flowproof-agent",
1014
1014
  "flowproof-cli",
@@ -1020,7 +1020,7 @@ dependencies = [
1020
1020
 
1021
1021
  [[package]]
1022
1022
  name = "flowproof-replay"
1023
- version = "0.23.0"
1023
+ version = "0.23.2"
1024
1024
  dependencies = [
1025
1025
  "chrono",
1026
1026
  "flowproof-agent",
@@ -1034,7 +1034,7 @@ dependencies = [
1034
1034
 
1035
1035
  [[package]]
1036
1036
  name = "flowproof-trace"
1037
- version = "0.23.0"
1037
+ version = "0.23.2"
1038
1038
  dependencies = [
1039
1039
  "jsonschema",
1040
1040
  "regex",
@@ -22,7 +22,7 @@ members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproo
22
22
  exclude = ["spike/windows-containment"]
23
23
 
24
24
  [workspace.package]
25
- version = "0.23.0"
25
+ version = "0.23.2"
26
26
  edition = "2021"
27
27
  license = "Apache-2.0"
28
28
  repository = "https://github.com/automators-com/flowproof"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowproof
3
- Version: 0.23.0
3
+ Version: 0.23.2
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -100,6 +100,9 @@ pub use web::WebAppDriver;
100
100
  #[cfg(feature = "web")]
101
101
  pub use web::shutdown_shared_browser;
102
102
 
103
+ #[cfg(feature = "web")]
104
+ pub use web::SharedBrowserGuard;
105
+
103
106
  #[derive(Debug, thiserror::Error)]
104
107
  pub enum AdapterError {
105
108
  #[error("adapter '{0}' is not implemented yet")]
@@ -22,6 +22,126 @@ use crate::AdapterError;
22
22
 
23
23
  const FIND_TIMEOUT: Duration = Duration::from_secs(5);
24
24
 
25
+ /// WebGUI table controls use a separate draggable scrollbar. Native
26
+ /// scrollIntoView scrolls their overflow:hidden body without updating the
27
+ /// header or SAP's column position, so the next click edits the wrong cell.
28
+ /// Move the rendered scrollbar instead; never call SAP application internals.
29
+ fn scroll_element_into_view(element: &headless_chrome::Element<'_>) -> anyhow::Result<()> {
30
+ const PLAN: &str = r#"function() { return JSON.stringify((() => {
31
+ if (!document.querySelector('#webguiPage0, #webguiform0')) return {kind:'native'};
32
+ const pane=this.closest('[id$="-mrss-cont-none"]');
33
+ if (!pane) return {kind:'native'};
34
+ const prefix=pane.id.slice(0,-'-mrss-cont-none'.length);
35
+ const bar=document.getElementById(prefix+'_hscroll-bar');
36
+ const thumb=document.getElementById(prefix+'_hscroll-hdl');
37
+ const content=document.getElementById(pane.id+'-content');
38
+ if (!bar || !thumb || !content) return {kind:'native'};
39
+ pane.scrollIntoView({block:'nearest',inline:'nearest',behavior:'instant'});
40
+ const p=pane.getBoundingClientRect(),r=this.getBoundingClientRect();
41
+ if (r.left>=p.left-1 && r.right<=p.right+1) return {kind:'ready'};
42
+ const b=bar.getBoundingClientRect(),h=thumb.getBoundingClientRect();
43
+ const width=content.getBoundingClientRect().width;
44
+ if (b.width<=h.width || h.width<=0 || h.height<=0 || width<=p.width)
45
+ return {kind:'unavailable'};
46
+ const travel=b.width-h.width;
47
+ const delta=(r.left+r.width/2)-(p.left+p.width/2);
48
+ const current=h.left-b.left;
49
+ const next=Math.max(0,Math.min(travel,current+delta*travel/(width-p.width)));
50
+ if (Math.abs(next-current)<1) return {kind:'unavailable'};
51
+ return {kind:'drag',x:h.left+h.width/2,y:h.top+h.height/2,
52
+ toX:b.left+next+h.width/2};
53
+ })()); }"#;
54
+ for _ in 0..6 {
55
+ let result = element.call_js_fn(PLAN, vec![], false)?;
56
+ let raw = result
57
+ .value
58
+ .and_then(|v| v.as_str().map(str::to_owned))
59
+ .ok_or_else(|| anyhow::anyhow!("no grid scroll plan"))?;
60
+ let plan: serde_json::Value = serde_json::from_str(&raw)?;
61
+ match plan["kind"].as_str() {
62
+ Some("native") => {
63
+ element.scroll_into_view()?;
64
+ return Ok(());
65
+ }
66
+ Some("ready") => return Ok(()),
67
+ Some("drag") => {}
68
+ _ => anyhow::bail!("SAP grid's horizontal scrollbar cannot expose the target cell"),
69
+ }
70
+ let x = plan["x"]
71
+ .as_f64()
72
+ .ok_or_else(|| anyhow::anyhow!("no scrollbar x"))?;
73
+ let y = plan["y"]
74
+ .as_f64()
75
+ .ok_or_else(|| anyhow::anyhow!("no scrollbar y"))?;
76
+ let to_x = plan["toX"]
77
+ .as_f64()
78
+ .ok_or_else(|| anyhow::anyhow!("no scrollbar destination"))?;
79
+ let send = |kind, at_x, pressed| {
80
+ element.parent.call_method(Input::DispatchMouseEvent {
81
+ Type: kind,
82
+ x: at_x,
83
+ y,
84
+ button: Some(if pressed {
85
+ Input::MouseButton::Left
86
+ } else {
87
+ Input::MouseButton::None
88
+ }),
89
+ buttons: Some(if pressed { 1 } else { 0 }),
90
+ click_count: Some(1),
91
+ modifiers: None,
92
+ timestamp: None,
93
+ force: None,
94
+ tangential_pressure: None,
95
+ tilt_x: None,
96
+ tilt_y: None,
97
+ twist: None,
98
+ delta_x: None,
99
+ delta_y: None,
100
+ pointer_Type: None,
101
+ })
102
+ };
103
+ send(Input::DispatchMouseEventTypeOption::MouseMoved, x, false)?;
104
+ send(Input::DispatchMouseEventTypeOption::MousePressed, x, true)?;
105
+ for step in 1..=8 {
106
+ send(
107
+ Input::DispatchMouseEventTypeOption::MouseMoved,
108
+ x + (to_x - x) * f64::from(step) / 8.0,
109
+ true,
110
+ )?;
111
+ }
112
+ // MouseReleased still names the released button, but no button is held.
113
+ element.parent.call_method(Input::DispatchMouseEvent {
114
+ Type: Input::DispatchMouseEventTypeOption::MouseReleased,
115
+ x: to_x,
116
+ y,
117
+ button: Some(Input::MouseButton::Left),
118
+ buttons: Some(0),
119
+ click_count: Some(1),
120
+ modifiers: None,
121
+ timestamp: None,
122
+ force: None,
123
+ tangential_pressure: None,
124
+ tilt_x: None,
125
+ tilt_y: None,
126
+ twist: None,
127
+ delta_x: None,
128
+ delta_y: None,
129
+ pointer_Type: None,
130
+ })?;
131
+ std::thread::sleep(Duration::from_millis(50));
132
+ }
133
+ anyhow::bail!("SAP grid cell did not enter the viewport after scrolling")
134
+ }
135
+
136
+ /// Element::click performs its own native scroll, even after an actionability
137
+ /// check. Keep all click paths (including the click before typing) on the same
138
+ /// scrollbar-aware path as double-click and explicit scrolling.
139
+ fn click_element(element: &headless_chrome::Element<'_>) -> anyhow::Result<()> {
140
+ scroll_element_into_view(element)?;
141
+ element.parent.click_point(element.get_midpoint()?)?;
142
+ Ok(())
143
+ }
144
+
25
145
  /// Read a checkbox-like control's state from an element that may be the
26
146
  /// control itself OR a wrapper around it. Covers the three shapes real apps
27
147
  /// use: a native `input[type=checkbox|radio]`, an ARIA widget carrying
@@ -985,20 +1105,120 @@ pub fn shutdown_shared_browser() {
985
1105
  }
986
1106
  }
987
1107
 
1108
+ /// Reference count of live `SharedBrowserGuard`s. `cargo test` runs `#[test]`
1109
+ /// functions concurrently by default, and the shared browser is one
1110
+ /// process-global static (see `shared_browser`) - a guard in test A that
1111
+ /// unconditionally killed it on drop would race test B, still mid-launch on
1112
+ /// the very same browser, and that race is not hypothetical: it reproduced
1113
+ /// live the first time this existed (network_idle_e2e's two tests, run
1114
+ /// together, one killing the shared browser out from under the other -
1115
+ /// "Unable to make method calls because underlying connection is closed").
1116
+ /// Only the LAST guard to drop may actually shut anything down.
1117
+ static GUARD_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1118
+
1119
+ /// Hold one of these (via [`SharedBrowserGuard::new`]) for the lifetime of
1120
+ /// anything that might launch the shared browser. The last one dropped
1121
+ /// calls `shutdown_shared_browser` for you - including on an early return
1122
+ /// or a panic, unlike a plain call at the end of a function body.
1123
+ /// `flowproof-cli::run_cli` holds one for the whole CLI process (where
1124
+ /// there is only ever one); any test that constructs a `WebAppDriver` on
1125
+ /// the shared path (the default - see `should_share_browser`) should hold
1126
+ /// one too, or it leaks a full Chrome process tree exactly like the CLI
1127
+ /// used to before this existed. Concurrent tests each holding their own
1128
+ /// guard are safe BECAUSE of the reference count above: the browser
1129
+ /// outlives every guard still alive, not just the one that happens to drop
1130
+ /// first.
1131
+ #[must_use = "holds the shared browser's cleanup for as long as this value lives - binding it to `_` is correct (that is what runs the guard for the rest of the scope); dropping it immediately with `drop(SharedBrowserGuard::new())` defeats the point"]
1132
+ pub struct SharedBrowserGuard(());
1133
+
1134
+ impl SharedBrowserGuard {
1135
+ pub fn new() -> Self {
1136
+ GUARD_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1137
+ Self(())
1138
+ }
1139
+ }
1140
+
1141
+ impl Default for SharedBrowserGuard {
1142
+ fn default() -> Self {
1143
+ Self::new()
1144
+ }
1145
+ }
1146
+
1147
+ impl Drop for SharedBrowserGuard {
1148
+ fn drop(&mut self) {
1149
+ if GUARD_COUNT.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) == 1 {
1150
+ shutdown_shared_browser();
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ #[cfg(test)]
1156
+ mod shared_browser_guard_tests {
1157
+ use super::{SharedBrowserGuard, GUARD_COUNT};
1158
+ use std::sync::atomic::Ordering;
1159
+
1160
+ /// The race this reference count exists to prevent, reproduced live the
1161
+ /// first time `SharedBrowserGuard` existed without it: two concurrent
1162
+ /// `cargo test` threads share the one process-global browser
1163
+ /// (`network_idle_e2e`'s two tests, run together - one guard's
1164
+ /// unconditional drop killed the shared browser mid-launch on the
1165
+ /// other, "Unable to make method calls because underlying connection is
1166
+ /// closed"). A held sibling guard must keep the count above zero, so a
1167
+ /// guard going out of scope while another is still alive must NOT be
1168
+ /// the one that triggers a shutdown.
1169
+ #[test]
1170
+ fn a_held_sibling_guard_keeps_the_count_above_zero() {
1171
+ let before = GUARD_COUNT.load(Ordering::SeqCst);
1172
+ let first = SharedBrowserGuard::new();
1173
+ assert_eq!(GUARD_COUNT.load(Ordering::SeqCst), before + 1);
1174
+ {
1175
+ let _second = SharedBrowserGuard::new();
1176
+ assert_eq!(
1177
+ GUARD_COUNT.load(Ordering::SeqCst),
1178
+ before + 2,
1179
+ "two live guards must both be counted"
1180
+ );
1181
+ }
1182
+ // The second guard is gone, but `first` is still held - the count
1183
+ // must reflect exactly that, not have raced to zero.
1184
+ assert_eq!(
1185
+ GUARD_COUNT.load(Ordering::SeqCst),
1186
+ before + 1,
1187
+ "one guard still alive must leave the count above zero"
1188
+ );
1189
+ drop(first);
1190
+ assert_eq!(
1191
+ GUARD_COUNT.load(Ordering::SeqCst),
1192
+ before,
1193
+ "the last guard dropping must return the count to its starting point"
1194
+ );
1195
+ }
1196
+ }
1197
+
988
1198
  /// Send an unconditional, immediate kill to a process by OS PID. Best
989
1199
  /// effort: a PID that is already gone is not an error, and there is
990
1200
  /// nothing useful to do with one that resists a kill signal/call.
991
1201
  fn force_kill_pid(pid: u32) {
1202
+ // Both this process's stdout/stderr are inherited by default, and the
1203
+ // common case here is a PID that is already gone (the graceful CDP
1204
+ // close above usually worked) - `kill`/`taskkill` printing "No such
1205
+ // process" to OUR stderr on every routine success was real noise,
1206
+ // observed leaking into flowproof's own error reports when this ran
1207
+ // from inside a record/run invocation. Discard both explicitly.
992
1208
  #[cfg(unix)]
993
1209
  {
994
1210
  let _ = std::process::Command::new("kill")
995
1211
  .args(["-9", &pid.to_string()])
1212
+ .stdout(std::process::Stdio::null())
1213
+ .stderr(std::process::Stdio::null())
996
1214
  .status();
997
1215
  }
998
1216
  #[cfg(windows)]
999
1217
  {
1000
1218
  let _ = std::process::Command::new("taskkill")
1001
1219
  .args(["/F", "/PID", &pid.to_string()])
1220
+ .stdout(std::process::Stdio::null())
1221
+ .stderr(std::process::Stdio::null())
1002
1222
  .status();
1003
1223
  }
1004
1224
  }
@@ -1225,7 +1445,7 @@ impl WebAppDriver {
1225
1445
  let locator = Self::locator(selector)?;
1226
1446
  let got = self.with_element(&locator, &format!("locating [{selector}]"), |element| {
1227
1447
  if scroll {
1228
- element.scroll_into_view()?;
1448
+ scroll_element_into_view(element)?;
1229
1449
  }
1230
1450
  let v = element.call_js_fn(
1231
1451
  r#"function() {
@@ -1900,6 +2120,82 @@ fn is_transport_fault(message: &str) -> bool {
1900
2120
  || m.contains("unable to make method calls")
1901
2121
  }
1902
2122
 
2123
+ /// Retry a fallible operation exactly once if its first failure looks like
2124
+ /// a transient CDP transport fault - the same one-shot recovery
2125
+ /// `with_element` already gives element-scoped calls (re-finding the
2126
+ /// element and retrying its own op inline), generalized here for a call
2127
+ /// that talks to the tab directly and has nothing to re-resolve. Pure
2128
+ /// control flow, no CDP calls of its own, so it is unit-testable with a
2129
+ /// mock closure rather than only provable against a real, flaky browser.
2130
+ fn retry_once_on_transport_fault<T>(
2131
+ mut attempt: impl FnMut() -> Result<T, String>,
2132
+ ) -> Result<T, String> {
2133
+ match attempt() {
2134
+ Ok(value) => Ok(value),
2135
+ Err(e) if is_transport_fault(&e) => {
2136
+ std::thread::sleep(Duration::from_millis(300));
2137
+ attempt()
2138
+ }
2139
+ Err(e) => Err(e),
2140
+ }
2141
+ }
2142
+
2143
+ #[cfg(test)]
2144
+ mod transport_retry_tests {
2145
+ use super::retry_once_on_transport_fault;
2146
+ use std::cell::Cell;
2147
+
2148
+ /// docs/fiori-reliability/FINDINGS.md:
2149
+ /// medium-02-change-info-record-readonly.flow.yaml failed live with
2150
+ /// "probing an iframe: Unable to make method calls because underlying
2151
+ /// connection is closed" - `probe_frame`'s CDP call had no retry at all,
2152
+ /// unlike every `with_element`-routed call, so a single transient CDP
2153
+ /// hiccup killed the whole flow with zero chance to recover.
2154
+ #[test]
2155
+ fn a_transport_fault_recovers_on_the_second_attempt() {
2156
+ let calls = Cell::new(0);
2157
+ let result = retry_once_on_transport_fault(|| {
2158
+ calls.set(calls.get() + 1);
2159
+ if calls.get() == 1 {
2160
+ Err("Unable to make method calls because underlying connection is closed".into())
2161
+ } else {
2162
+ Ok("recovered")
2163
+ }
2164
+ });
2165
+ assert_eq!(result, Ok("recovered"));
2166
+ assert_eq!(calls.get(), 2, "must retry exactly once, not loop");
2167
+ }
2168
+
2169
+ #[test]
2170
+ fn a_transport_fault_that_never_clears_still_fails_after_one_retry() {
2171
+ let calls = Cell::new(0);
2172
+ let result: Result<(), String> = retry_once_on_transport_fault(|| {
2173
+ calls.set(calls.get() + 1);
2174
+ Err("the event waited for never came".into())
2175
+ });
2176
+ assert_eq!(result, Err("the event waited for never came".to_string()));
2177
+ assert_eq!(
2178
+ calls.get(),
2179
+ 2,
2180
+ "one retry, then give up - not an infinite loop"
2181
+ );
2182
+ }
2183
+
2184
+ /// A non-transport error (element genuinely missing, a real assertion
2185
+ /// mismatch) must propagate immediately - this retry exists for a
2186
+ /// flaky CONNECTION, not as a generic "try again" for any failure.
2187
+ #[test]
2188
+ fn a_non_transport_error_is_never_retried() {
2189
+ let calls = Cell::new(0);
2190
+ let result: Result<(), String> = retry_once_on_transport_fault(|| {
2191
+ calls.set(calls.get() + 1);
2192
+ Err("element not found".into())
2193
+ });
2194
+ assert_eq!(result, Err("element not found".to_string()));
2195
+ assert_eq!(calls.get(), 1, "a non-transport error must not be retried");
2196
+ }
2197
+ }
2198
+
1903
2199
  /// How a [`UiaSelector`] resolves on a page: a CSS selector or a text
1904
2200
  /// anchor, optionally narrowed to the nth match (1-based).
1905
2201
  struct WebLocator {
@@ -2173,13 +2469,21 @@ impl WebAppDriver {
2173
2469
  op = serde_json::Value::from(op),
2174
2470
  arg = arg,
2175
2471
  );
2176
- let status = self
2177
- .tab()?
2178
- .evaluate(&call, false)
2179
- .map_err(|e| web_err("acting inside an iframe", e))?
2180
- .value
2181
- .and_then(|v| v.as_str().map(str::to_string))
2182
- .unwrap_or_default();
2472
+ // Same gap `probe_frame` had (see docs/fiori-reliability/FINDINGS.md):
2473
+ // this talks to the tab directly, with no element to re-resolve, so
2474
+ // it never went through with_element's retry either - one transient
2475
+ // CDP hiccup here (this is the exact call `type_text`'s framed path
2476
+ // uses for every field inside an iframe) killed the whole flow.
2477
+ let status = retry_once_on_transport_fault(|| {
2478
+ self.tab()
2479
+ .map_err(|e| e.to_string())?
2480
+ .evaluate(&call, false)
2481
+ .map_err(|e| e.to_string())
2482
+ })
2483
+ .map_err(|e| web_err("acting inside an iframe", e))?
2484
+ .value
2485
+ .and_then(|v| v.as_str().map(str::to_string))
2486
+ .unwrap_or_default();
2183
2487
  let frame = &query.frame;
2184
2488
  match status.as_str() {
2185
2489
  "cross_origin" => Err(cross_origin(frame)),
@@ -2893,7 +3197,7 @@ impl AppDriver for WebAppDriver {
2893
3197
  // and clicking a zero-sized input does nothing - so the helper
2894
3198
  // hands back whichever ancestor is actually clickable.
2895
3199
  self.with_element(&locator, &format!("clicking [{selector}]"), |element| {
2896
- element.click().map(|_| ())
3200
+ click_element(element)
2897
3201
  })?;
2898
3202
  }
2899
3203
  // Verify it took: a click that lands on a disabled or intercepted
@@ -3069,6 +3373,11 @@ impl AppDriver for WebAppDriver {
3069
3373
  if (!el) {{ return 'missing'; }}
3070
3374
  if (el.disabled === true || el.getAttribute('aria-disabled') === 'true'
3071
3375
  || el.closest('fieldset[disabled]')) {{ return 'disabled'; }}
3376
+ // The composed gate uses the custom scrollbar-aware scroll
3377
+ // path. Native scrolling here would corrupt SAP's grid before
3378
+ // replay even dispatches its click.
3379
+ if (document.querySelector('#webguiPage0, #webguiform0')
3380
+ && el.closest('[id$=\"-mrss-cont-none\"]')) {{ return 'sap_grid'; }}
3072
3381
  const a = el.getBoundingClientRect();
3073
3382
  await new Promise(tick => setTimeout(tick, {interval}));
3074
3383
  const b = el.getBoundingClientRect();
@@ -3093,6 +3402,7 @@ impl AppDriver for WebAppDriver {
3093
3402
  .map_err(|e| web_err(&format!("actionability of [{target}]"), e))?;
3094
3403
  let verdict = value.value.and_then(|v| v.as_str().map(str::to_string));
3095
3404
  Ok(match verdict.as_deref() {
3405
+ Some("sap_grid") => return flowproof_driver::composed_actionability_gate(self, target),
3096
3406
  Some("ok") => None,
3097
3407
  Some("disabled") => Some("disabled".into()),
3098
3408
  Some("unstable") => Some("unstable (still moving/animating)".into()),
@@ -3285,11 +3595,34 @@ impl AppDriver for WebAppDriver {
3285
3595
  match to {
3286
3596
  // Bring an in-DOM element into the viewport.
3287
3597
  ScrollTo::IntoView => {
3598
+ let sap = self
3599
+ .tab()?
3600
+ .evaluate(
3601
+ "!!document.querySelector('#webguiPage0, #webguiform0')",
3602
+ false,
3603
+ )
3604
+ .map_err(|e| web_err("checking the scroll surface", e))?
3605
+ .value
3606
+ .and_then(|v| v.as_bool())
3607
+ .unwrap_or(false);
3608
+ if sap {
3609
+ self.scene()?;
3610
+ }
3611
+ self.with_element(
3612
+ &locator,
3613
+ "scrolling the target",
3614
+ scroll_element_into_view,
3615
+ )?;
3616
+ // SAP may replace the table after scrolling/validation.
3617
+ // Settle that response and inspect a freshly resolved
3618
+ // element, not the detached pre-scroll handle.
3619
+ if sap {
3620
+ self.scene()?;
3621
+ }
3288
3622
  let value = self.with_element(
3289
3623
  &locator,
3290
3624
  &format!("scrolling [{sel}] into view"),
3291
3625
  |element| {
3292
- element.scroll_into_view()?;
3293
3626
  element.call_js_fn(
3294
3627
  r#"function() {
3295
3628
  const r = this.getBoundingClientRect();
@@ -3442,7 +3775,7 @@ impl AppDriver for WebAppDriver {
3442
3775
  // returns null - and the gate blocks a click that would have
3443
3776
  // worked. A whole settings form under the fold was
3444
3777
  // untestable this way (field report, round 3).
3445
- element.scroll_into_view()?;
3778
+ scroll_element_into_view(element)?;
3446
3779
  // Playwright's obscured check: does elementFromPoint at the
3447
3780
  // element's center resolve to it (or a relative)? A toast or
3448
3781
  // modal backdrop on top makes the click land elsewhere.
@@ -3571,7 +3904,7 @@ impl AppDriver for WebAppDriver {
3571
3904
  }
3572
3905
  let locator = Self::locator(selector)?;
3573
3906
  self.with_element(&locator, &format!("clicking [{selector}]"), |element| {
3574
- element.click().map(|_| ())
3907
+ click_element(element)
3575
3908
  })
3576
3909
  }
3577
3910
 
@@ -3643,7 +3976,7 @@ impl AppDriver for WebAppDriver {
3643
3976
  &locator,
3644
3977
  &format!("right-clicking [{selector}]"),
3645
3978
  |element| {
3646
- element.scroll_into_view()?;
3979
+ scroll_element_into_view(element)?;
3647
3980
  let point = element.get_midpoint()?;
3648
3981
  for kind in [
3649
3982
  Input::DispatchMouseEventTypeOption::MousePressed,
@@ -3680,8 +4013,11 @@ impl AppDriver for WebAppDriver {
3680
4013
  &locator,
3681
4014
  &format!("double-clicking [{selector}]"),
3682
4015
  |element| {
3683
- element.scroll_into_view()?;
4016
+ scroll_element_into_view(element)?;
3684
4017
  let point = element.get_midpoint()?;
4018
+ // Match a real pointer interaction (and ordinary click): grid
4019
+ // editors may choose their active cell during pointer motion.
4020
+ tab.move_mouse_to_point(point)?;
3685
4021
  // A real `dblclick` is two full press/release pairs at the
3686
4022
  // same point; Chromium raises the DOM `dblclick` on the
3687
4023
  // SECOND release when its click_count reaches 2. Mirrors the
@@ -3725,7 +4061,7 @@ impl AppDriver for WebAppDriver {
3725
4061
  // even when the move landed on an occluder, so hover VERIFIES.
3726
4062
  let hovered =
3727
4063
  self.with_element(&locator, &format!("hovering [{selector}]"), |element| {
3728
- element.scroll_into_view()?;
4064
+ scroll_element_into_view(element)?;
3729
4065
  // NOT-OBSCURED gate: hovering an obscured element is
3730
4066
  // meaningless because the occluder receives the `mouseover`,
3731
4067
  // not our target. Same Playwright-style `elementFromPoint`
@@ -3829,13 +4165,23 @@ impl AppDriver for WebAppDriver {
3829
4165
  id = js(&query.inner_id),
3830
4166
  text = js(&query.inner_text),
3831
4167
  );
3832
- let status = self
3833
- .tab()?
3834
- .evaluate(&call, false)
3835
- .map_err(|e| web_err("probing an iframe", e))?
3836
- .value
3837
- .and_then(|v| v.as_str().map(str::to_string))
3838
- .unwrap_or_default();
4168
+ // ONE retry on a transport fault, same recovery `with_element` gives
4169
+ // element-scoped calls (see medium-02-change-info-record-readonly's
4170
+ // real "probing an iframe: ... underlying connection is closed"
4171
+ // failure in docs/fiori-reliability/FINDINGS.md): this call talks to
4172
+ // the tab directly, with no element to re-resolve, so it never went
4173
+ // through that retry at all - any transient CDP hiccup here killed
4174
+ // the whole flow on the first blip, with zero chance to recover.
4175
+ let status = retry_once_on_transport_fault(|| {
4176
+ self.tab()
4177
+ .map_err(|e| e.to_string())?
4178
+ .evaluate(&call, false)
4179
+ .map_err(|e| e.to_string())
4180
+ })
4181
+ .map_err(|e| web_err("probing an iframe", e))?
4182
+ .value
4183
+ .and_then(|v| v.as_str().map(str::to_string))
4184
+ .unwrap_or_default();
3839
4185
  if status == "cross_origin" {
3840
4186
  return Ok(FrameProbe::CrossOrigin);
3841
4187
  }
@@ -3988,6 +4334,11 @@ impl AppDriver for WebAppDriver {
3988
4334
  // The two cases are now different answers: `not_select` is the
3989
4335
  // genuine fall-through to typing, `no_option` is a failure.
3990
4336
  const SELECT_COMMIT_JS: &str = r#"function(wanted) {
4337
+ if ((this.tagName === 'INPUT' || this.tagName === 'TEXTAREA')
4338
+ && this.id !== 'ToolbarOkCode'
4339
+ && document.querySelector('#webguiPage0, #webguiform0')) {
4340
+ return 'sap_input';
4341
+ }
3991
4342
  if (this.tagName !== 'SELECT') { return 'not_select'; }
3992
4343
  const w = String(wanted).trim();
3993
4344
  const options = Array.from(this.options);
@@ -4035,6 +4386,12 @@ impl AppDriver for WebAppDriver {
4035
4386
  value, then by visible text; nothing was selected"
4036
4387
  )));
4037
4388
  }
4389
+ if status == "sap_input" {
4390
+ // Focusing a WebGUI editor can replace it asynchronously. Finish
4391
+ // that transition before selecting/typing, then resolve it again.
4392
+ self.with_element(&locator, "focusing the SAP editor", click_element)?;
4393
+ self.scene()?;
4394
+ }
4038
4395
  self.with_element(&locator, &format!("typing into [{selector}]"), |element| {
4039
4396
  // Select what the field holds before the keystrokes land: typing
4040
4397
  // over a selection replaces, so the field ends up reading `text`
@@ -4050,7 +4407,11 @@ impl AppDriver for WebAppDriver {
4050
4407
  // `type_into`, which clicks again and would collapse the
4051
4408
  // selection back to a caret - the keystrokes would append, which
4052
4409
  // is the accident this contract removes.
4053
- element.click()?;
4410
+ if status == "sap_input" {
4411
+ element.call_js_fn("function() { this.focus(); }", vec![], false)?;
4412
+ } else {
4413
+ click_element(element)?;
4414
+ }
4054
4415
  element
4055
4416
  .call_js_fn(
4056
4417
  r#"function() {
@@ -4061,7 +4422,24 @@ impl AppDriver for WebAppDriver {
4061
4422
  )
4062
4423
  .map(|_| ())?;
4063
4424
  element.parent.type_str(text).map(|_| ())
4064
- })
4425
+ })?;
4426
+ if status == "sap_input" {
4427
+ // WebGUI commits fields on blur. The next click can otherwise
4428
+ // race the response that replaces the form and erase its input.
4429
+ // Match the framed SAP path, then reuse the existing network and
4430
+ // scene settling rather than adding a fixed sleep to every field.
4431
+ self.press_key("Tab", &[])?;
4432
+ self.scene()?;
4433
+ // Type leaves the edited field focused. Otherwise a following
4434
+ // Enter could activate the next toolbar/section button instead
4435
+ // of validating the form. Re-resolve after SAP replaces controls.
4436
+ self.with_element(
4437
+ &locator,
4438
+ "restoring the edited SAP field's focus",
4439
+ |element| element.call_js_fn("function() { this.focus(); }", vec![], false),
4440
+ )?;
4441
+ }
4442
+ Ok(())
4065
4443
  }
4066
4444
 
4067
4445
  fn click_at(
@@ -4081,7 +4459,7 @@ impl AppDriver for WebAppDriver {
4081
4459
  &locator,
4082
4460
  &format!("locating the click point in [{selector}]"),
4083
4461
  |element| {
4084
- element.scroll_into_view()?;
4462
+ scroll_element_into_view(element)?;
4085
4463
  let got = element.call_js_fn(
4086
4464
  r#"function(xp, yp) {
4087
4465
  const r = this.getBoundingClientRect();