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.
- {flowproof-0.23.0 → flowproof-0.23.2}/Cargo.lock +7 -7
- {flowproof-0.23.0 → flowproof-0.23.2}/Cargo.toml +1 -1
- {flowproof-0.23.0 → flowproof-0.23.2}/PKG-INFO +1 -1
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/lib.rs +3 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/web.rs +403 -25
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/a11y_capture.rs +8 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/drag_spike.rs +6 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/network_idle_e2e.rs +8 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/shared_browser_shutdown_e2e.rs +2 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/static_area_e2e.rs +4 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/author.rs +92 -1
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/agent_flow.rs +165 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/lib.rs +6 -18
- flowproof-0.23.2/crates/flowproof-cli/tests/agent_conversation_anthropic_e2e.rs +264 -0
- flowproof-0.23.2/crates/flowproof-cli/tests/agent_conversation_streaming_e2e.rs +373 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/examples_resolve.rs +57 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/web_e2e.rs +181 -0
- flowproof-0.23.2/crates/flowproof-trace/schema/cassette-v1.schema.json +127 -0
- flowproof-0.23.2/crates/flowproof-trace/tests/cassette_conformance.rs +170 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/__init__.py +1 -1
- {flowproof-0.23.0 → flowproof-0.23.2}/pyproject.toml +1 -1
- {flowproof-0.23.0 → flowproof-0.23.2}/README.md +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/Cargo.toml +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/env_block.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/fiori.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/sap_com.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/src/vision.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/side_effect_http_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/Cargo.toml +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/agent_steps.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/clarify.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/doc_author.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/doc_formats/hp_alm.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/doc_formats.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/draft_assembly.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/heal.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/lib.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/llm.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/recorder.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/repair.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/rules.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/src/spec.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/Cargo.toml +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/skills/flowproof-config/SKILL.md +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/capture.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/config.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/doctor.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/main.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/suite.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/suite_checkpoint.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/suite_inputs.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/suite_journal.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/src/update_check.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/a11y_selector_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/config_cli_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/config_seed_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/config_skill_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/doctor_ai_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/documented_flags.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/falsifiability_side_effect.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/fiori_field_commit_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_checkpoint.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/suite_policy.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/Cargo.toml +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/app.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/backend.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/gdi.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/lib.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/mock.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/oob.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/recording.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/redact.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/surface.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/visual.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-driver/src/window.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-python/Cargo.toml +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-python/src/lib.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/Cargo.toml +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/src/lib.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/src/report.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/src/runrecord.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/Cargo.toml +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/schema/side-effect-v1.schema.json +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/captures.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/cassette.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/egress.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/format.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/lib.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/secret.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/secret_scan.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/side_effect.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/substitution.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/src/toolcalls.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/tests/fixtures/multi.trace.jsonl +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/crates/flowproof-trace/tests/side_effect_conformance.rs +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/cli.py +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/flow.py +0 -0
- {flowproof-0.23.0 → flowproof-0.23.2}/flowproof/mcp_server.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
+
version = "0.23.2"
|
|
26
26
|
edition = "2021"
|
|
27
27
|
license = "Apache-2.0"
|
|
28
28
|
repository = "https://github.com/automators-com/flowproof"
|
|
@@ -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
|
|
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
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
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
|
-
|
|
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
|
|
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();
|