mobster-cli 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
mobile_agent/wda/text.py
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
"""WebDriverAgent text entry."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
import unicodedata
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from ..state import truncated_field_value, validate_input_text
|
|
10
|
+
from ..transport import Deadline, NotSent, TransportError
|
|
11
|
+
from .guards import DriverRejection, finite_positive, grounded_target
|
|
12
|
+
from .observe import WDA_FRESH_SECONDS, WDA_READ_MARGIN_SECONDS, element_id
|
|
13
|
+
|
|
14
|
+
# Apps where a message composer sends on Return: a line break typed there as a key would send the text before it.
|
|
15
|
+
CHAT_APPS = frozenset({
|
|
16
|
+
"com.apple.MobileSMS", "net.whatsapp.WhatsApp", "com.facebook.Messenger", "ph.telegra.Telegraph",
|
|
17
|
+
"org.whispersystems.signal", "com.tinyspeck.chatlyio", "com.hammerandchisel.discord", "com.burbn.instagram",
|
|
18
|
+
"com.atebits.Tweetie2", "com.linkedin.LinkedIn", "com.toyopagroup.picaboo", "jp.naver.line", "com.tencent.xin",
|
|
19
|
+
"com.microsoft.skype.teams", "com.google.chat", "com.cardify.tinder", "com.hinge.app", "com.bumble.app"})
|
|
20
|
+
CHAT_FIELD = re.compile(r"\b(?:message|imessage|text message|chat|reply|comment|write a|say something|send)\b|"
|
|
21
|
+
r"(?:\.\.\.|…)$", re.I)
|
|
22
|
+
# Focusing a field raises the keyboard within ~0.5 s on device.
|
|
23
|
+
WDA_KEYBOARD_WAIT_SECONDS = 2.0
|
|
24
|
+
# With a hardware keyboard known, the focusing tap's keyboard is read this long after the tap
|
|
25
|
+
# (not for WDA_KEYBOARD_WAIT_SECONDS / 2 of reads: 1-2 reads of ~1 s on a long simulator list).
|
|
26
|
+
WDA_PARKED_FOCUS_SECONDS = .25
|
|
27
|
+
# Longest text a write takes (a document body); typing keeps validate_input_text's limits.
|
|
28
|
+
WDA_WRITE_MAX_CHARS = 20000
|
|
29
|
+
# The press that opens the edit menu (Paste), and how long its menu is looked for.
|
|
30
|
+
WDA_PASTE_PRESS_MS = 600
|
|
31
|
+
WDA_PASTE_MENU_SECONDS = 3.0
|
|
32
|
+
|
|
33
|
+
# Smart punctuation a keyboard may type for plain ASCII (a typed read-back is compared without it).
|
|
34
|
+
_TYPOGRAPHIC = str.maketrans({"‘": "'", "’": "'", "“": '"', "”": '"', "–": "-", "—": "-", "\u00a0": " "})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NoKeyboardFocus(TransportError):
|
|
38
|
+
"""A focusing tap was made and no keyboard came up: nothing was typed."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WheelNotSet(ValueError):
|
|
42
|
+
"""A picker wheel didn't reach the value it was given; it is left where it shows (the message says what)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# A picker wheel's value: the spellings tried (WDA.set_value) and the steps toward it, read after each.
|
|
46
|
+
WHEEL_SPELLINGS = 6
|
|
47
|
+
WHEEL_STEPS = 40
|
|
48
|
+
_WHEEL_NUMBER = re.compile(r"^\s*(\d+)")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def wheel_number(text):
|
|
52
|
+
"""The number a wheel's value or a wanted item starts with ("25 min" -> 25, "05" -> 5), else None."""
|
|
53
|
+
found = _WHEEL_NUMBER.match(text or "")
|
|
54
|
+
return int(found.group(1)) if found else None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def wheel_shows(shown, wanted):
|
|
58
|
+
"""Whether a wheel showing ``shown`` is at ``wanted``."""
|
|
59
|
+
if not isinstance(shown, str) or not shown.strip() or not wanted:
|
|
60
|
+
return False
|
|
61
|
+
a, b = wheel_number(shown), wheel_number(wanted)
|
|
62
|
+
if a is not None or b is not None:
|
|
63
|
+
return a == b
|
|
64
|
+
shown_words, wanted_words = shown.casefold().split(), wanted.casefold().split()
|
|
65
|
+
return shown_words == wanted_words or shown_words[:1] == wanted_words[:1]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def wheel_items(wanted, shown):
|
|
69
|
+
"""The spellings of ``wanted`` a wheel may list, given the value it shows."""
|
|
70
|
+
out = [wanted]
|
|
71
|
+
bare = re.match(r"(\d+|AM|PM)\b", wanted, re.IGNORECASE)
|
|
72
|
+
if bare:
|
|
73
|
+
out.append(bare.group(1))
|
|
74
|
+
number = wheel_number(wanted)
|
|
75
|
+
if number is not None:
|
|
76
|
+
out += [str(number), f"{number:02d}"]
|
|
77
|
+
unit = " ".join((shown or "").split()[1:]) if wheel_number(shown) is not None else ""
|
|
78
|
+
if unit:
|
|
79
|
+
out += [f"{number} {unit}", f"{number:02d} {unit}"]
|
|
80
|
+
if unit.isalpha(): # "1 hour" on a wheel at "0 hours"
|
|
81
|
+
out.append(f"{number} {unit[:-1] if unit.endswith('s') else unit + 's'}")
|
|
82
|
+
seen, items = set(), []
|
|
83
|
+
for item in out:
|
|
84
|
+
if item and item not in seen:
|
|
85
|
+
seen.add(item)
|
|
86
|
+
items.append(item)
|
|
87
|
+
return items
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class FieldNotCleared(TransportError):
|
|
91
|
+
"""A field known to hold text wasn't shown to be emptied. Typing now could add to it, so the write stops here."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WriteInterrupted(TransportError):
|
|
95
|
+
"""A compound write stopped after a request that can change the field. Its last request's answer says nothing about
|
|
96
|
+
the ones before it, so the whole write is unknown, however it ended. Connection recovery doesn't restart it, and a
|
|
97
|
+
further attempt the model asks for goes through the same recheck as any unknown outcome."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class WDAText:
|
|
101
|
+
"""The text part of WDA."""
|
|
102
|
+
_write_sent = False # a request of this write that can change the field may have reached WDA
|
|
103
|
+
# (WDA url, bundle) where a long press offered no Paste: later writes there type at once (_paste).
|
|
104
|
+
_paste_missing = set()
|
|
105
|
+
# WDA URLs whose keyboard stayed parked after a focusing tap (hardware-keyboard mode): no
|
|
106
|
+
# software keyboard will animate in, so waiting for one is skipped. A visible one clears it.
|
|
107
|
+
_parked_keyboards = set()
|
|
108
|
+
|
|
109
|
+
def _sends(self, request, *args, **kwargs):
|
|
110
|
+
"""A request that can change a field. Only NotSent proves it never left."""
|
|
111
|
+
try:
|
|
112
|
+
answer = request(*args, **kwargs)
|
|
113
|
+
except NotSent:
|
|
114
|
+
raise
|
|
115
|
+
except Exception:
|
|
116
|
+
self._write_sent = True
|
|
117
|
+
raise
|
|
118
|
+
self._write_sent = True
|
|
119
|
+
return answer
|
|
120
|
+
|
|
121
|
+
def _unique_element(self, role, name, deadline):
|
|
122
|
+
"""WDA's id for the one ``role`` element named ``name``, else None, so the caller types another way."""
|
|
123
|
+
name = name.replace("\\", "\\\\").replace("'", "\\'")
|
|
124
|
+
# ``name`` is WDA's identifier-or-label. ``identifier`` is not a predicate attribute: WDA 16.12
|
|
125
|
+
# rejected the query, so until 24 Sep this path never ran and every field was focused by a tap.
|
|
126
|
+
try:
|
|
127
|
+
found = self.call("POST", "/elements", {"using": "predicate string", "value":
|
|
128
|
+
f"type == 'XCUIElementType{role}' AND (name == '{name}' OR label == '{name}')"},
|
|
129
|
+
min(5, deadline.remaining()))
|
|
130
|
+
except TransportError:
|
|
131
|
+
return None
|
|
132
|
+
return element_id(found[0]) if isinstance(found, list) and len(found) == 1 else None
|
|
133
|
+
|
|
134
|
+
def _type_text(self, operation, target, snapshot, text, deadline, multiline=None):
|
|
135
|
+
"""TYPE / TYPE_SUBMIT into ``target``."""
|
|
136
|
+
if multiline is None:
|
|
137
|
+
multiline = target.role == "TextView"
|
|
138
|
+
grounded_target(operation, target, snapshot)
|
|
139
|
+
validate_input_text(text, multiline=multiline and operation == "TYPE")
|
|
140
|
+
self._check_current(snapshot, deadline)
|
|
141
|
+
focused = self._clear_stale_query(target, deadline)
|
|
142
|
+
keyboard = getattr(snapshot, "keyboard", "") or ("visible" if "SUBMIT" in target.actions else "")
|
|
143
|
+
if focused:
|
|
144
|
+
# The clear left keyboard focus in the field: no focusing tap, no keyboard wait.
|
|
145
|
+
keyboard = "visible"
|
|
146
|
+
elif keyboard == "visible" and self._focused_elsewhere(target, snapshot, deadline):
|
|
147
|
+
# The keyboard belongs to another field: keystrokes would go there (iOSWorld mem-048,
|
|
148
|
+
# 24 Sep: a whole note body typed into its title). Focus has to move first.
|
|
149
|
+
keyboard = ""
|
|
150
|
+
# A visible keyboard keeps the one-call path; otherwise focus must move, and the element
|
|
151
|
+
# itself moves it reliably (a coordinate tap left mem-041's focus in the title).
|
|
152
|
+
if keyboard != "visible" and self._type_into_element(target, text, deadline,
|
|
153
|
+
submit=operation == "TYPE_SUBMIT", multiline=multiline):
|
|
154
|
+
return None
|
|
155
|
+
if keyboard != "visible":
|
|
156
|
+
# No keyboard yet: focus the field with a tap. Many iOS fields
|
|
157
|
+
# (Safari's address bar, search bars) swap in a different
|
|
158
|
+
# editing field on focus, so an element-bound type fails as
|
|
159
|
+
# "stale element"; keystrokes go to whatever holds focus.
|
|
160
|
+
x, y = target.center
|
|
161
|
+
if multiline:
|
|
162
|
+
# A text view's centre puts the cursor mid-text and keystrokes insert there;
|
|
163
|
+
# its last line puts it at the end, where added text belongs.
|
|
164
|
+
left, top, w, h = target.rect
|
|
165
|
+
x, y = left + w * .9, top + h * .92
|
|
166
|
+
self._sends(self._tap, x * snapshot.width, y * snapshot.height, deadline)
|
|
167
|
+
if not self._await_keyboard(deadline):
|
|
168
|
+
raise NoKeyboardFocus("The field did not take keyboard focus; nothing was typed")
|
|
169
|
+
self._dispatching()
|
|
170
|
+
# Return is sent by the driver, never carried in model-generated text.
|
|
171
|
+
keys = [text, "\n"] if operation == "TYPE_SUBMIT" else [text]
|
|
172
|
+
self._frame_mark(operation)
|
|
173
|
+
self._sends(self.call, "POST", "/wda/keys", {"value": keys}, deadline.remaining())
|
|
174
|
+
self._frame_dispatched()
|
|
175
|
+
|
|
176
|
+
def _clear_stale_query(self, target, deadline):
|
|
177
|
+
"""Empty a search field holding an earlier query; only search fields, as others may hold the person's text."""
|
|
178
|
+
return target.role == "SearchField" and self._clear_field(target, deadline)
|
|
179
|
+
|
|
180
|
+
def _focused_elsewhere(self, target, snapshot, deadline):
|
|
181
|
+
"""True when WDA reports keyboard focus outside ``target``'s frame; unknown counts as not elsewhere."""
|
|
182
|
+
if sum(e.editable for e in snapshot.elements) < 2:
|
|
183
|
+
return False
|
|
184
|
+
try:
|
|
185
|
+
identifier = element_id(self.call("GET", "/element/active", None, min(2, deadline.remaining())) or {})
|
|
186
|
+
if not identifier:
|
|
187
|
+
return False
|
|
188
|
+
rect = self.call("GET", f"/element/{quote(identifier, safe='')}/rect", None, min(2, deadline.remaining()))
|
|
189
|
+
cx = (rect["x"] + rect["width"] / 2) / snapshot.width
|
|
190
|
+
cy = (rect["y"] + rect["height"] / 2) / snapshot.height
|
|
191
|
+
except (TransportError, TimeoutError, KeyError, TypeError, ZeroDivisionError):
|
|
192
|
+
return False
|
|
193
|
+
x, y, w, h = target.rect
|
|
194
|
+
return not (x - .01 <= cx <= x + w + .01 and y - .01 <= cy <= y + h + .01)
|
|
195
|
+
|
|
196
|
+
def _type_into_element(self, target, text, deadline, submit=False, multiline=False):
|
|
197
|
+
"""Type through the element itself when it is unique; False: nothing was typed and the caller uses its own path.
|
|
198
|
+
A focusing tap by coordinates can leave focus in the last field: mem-041 typed a note body into its title."""
|
|
199
|
+
filled = (target.value or "").strip() and (target.value or "").strip() != (target.label or "").strip()
|
|
200
|
+
# A multi-line text field (SwiftUI's vertical TextField) is a text view here: CloudSlides' body
|
|
201
|
+
# took the typed text mid-way (cloudslides-002, 4 of 4 runs, 25 Sep).
|
|
202
|
+
if target.role not in ("TextField", "TextView") or (target.role == "TextView" or multiline) and filled:
|
|
203
|
+
return False
|
|
204
|
+
identifier = target.label and self._unique_element(target.role, target.label, deadline)
|
|
205
|
+
if not identifier:
|
|
206
|
+
return False
|
|
207
|
+
self._dispatching()
|
|
208
|
+
self._frame_mark("TYPE")
|
|
209
|
+
# Once sent, typing may have happened even if WDA never answers. Only the
|
|
210
|
+
# read-only lookup above may fall back to another input route.
|
|
211
|
+
self._sends(self.call, "POST", f"/element/{quote(identifier, safe='')}/value",
|
|
212
|
+
{"value": text + "\n" if submit else text}, deadline.remaining())
|
|
213
|
+
self._frame_dispatched()
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
def set_value(self, target, value, timeout=10):
|
|
217
|
+
"""Select ``value`` ("6", "AM") on the wheel nearest the observed one, read back; WheelNotSet when it can't."""
|
|
218
|
+
deadline = Deadline(timeout)
|
|
219
|
+
found = self.call("POST", "/elements", {"using": "class name", "value": f"XCUIElementType{target.role}"},
|
|
220
|
+
min(5, deadline.remaining()))
|
|
221
|
+
ids = [i for i in (element_id(e) for e in (found if isinstance(found, list) else [])[:12]) if i]
|
|
222
|
+
if not ids:
|
|
223
|
+
raise DriverRejection("unsupported_or_stale_target")
|
|
224
|
+
cx, cy = target.center
|
|
225
|
+
best, best_distance = None, None
|
|
226
|
+
width, height = self._screen_sizes.get(self.url) or (None, None)
|
|
227
|
+
if not width:
|
|
228
|
+
current = self.observe(timeout=min(5, deadline.remaining()))
|
|
229
|
+
width, height = current.width, current.height
|
|
230
|
+
for identifier in ids:
|
|
231
|
+
rect = self.call("GET", f"/element/{quote(identifier, safe='')}/rect", timeout=min(3, deadline.remaining()))
|
|
232
|
+
if not isinstance(rect, dict) or not width:
|
|
233
|
+
continue
|
|
234
|
+
x = (rect.get("x", 0) + rect.get("width", 0) / 2) / width
|
|
235
|
+
y = (rect.get("y", 0) + rect.get("height", 0) / 2) / height
|
|
236
|
+
distance = (x - cx) ** 2 + (y - cy) ** 2
|
|
237
|
+
if best_distance is None or distance < best_distance:
|
|
238
|
+
best, best_distance = identifier, distance
|
|
239
|
+
best = best or (ids[0] if len(ids) == 1 else None)
|
|
240
|
+
if best is None:
|
|
241
|
+
raise DriverRejection("unsupported_or_stale_target")
|
|
242
|
+
self._dispatching()
|
|
243
|
+
wanted = " ".join(str(value).split())
|
|
244
|
+
path = f"/element/{quote(best, safe='')}"
|
|
245
|
+
shown = self._wheel_value(path, deadline)
|
|
246
|
+
if wheel_shows(shown, wanted):
|
|
247
|
+
return
|
|
248
|
+
# WDA ignores a value that is not one of the wheel's items, silently. Items are spelled per picker: an alarm's
|
|
249
|
+
# minutes are "00".."59" ("0" didn't move it: iOSWorld multi-087), a timer's are "7 min" ("7" didn't: verify
|
|
250
|
+
# runs, 12 Oct), the model often names one as the wheel shows it ("6 o'clock", "45 minutes": clock-001). Each
|
|
251
|
+
# spelling is tried, read back after each.
|
|
252
|
+
for attempt in wheel_items(wanted, shown)[:WHEEL_SPELLINGS]:
|
|
253
|
+
self.call("POST", path + "/value", {"value": attempt}, deadline.remaining())
|
|
254
|
+
shown = self._wheel_value(path, deadline)
|
|
255
|
+
if wheel_shows(shown, wanted):
|
|
256
|
+
return
|
|
257
|
+
# No spelling took: the wheel is stepped one item at a time toward the value (WDA's picker-wheel select), and
|
|
258
|
+
# read after every step.
|
|
259
|
+
goal = wheel_number(wanted)
|
|
260
|
+
orders = ["previous", "next"] if goal is None else []
|
|
261
|
+
for _ in range(WHEEL_STEPS):
|
|
262
|
+
if deadline.remaining() <= 1:
|
|
263
|
+
break
|
|
264
|
+
now = wheel_number(shown)
|
|
265
|
+
if goal is not None:
|
|
266
|
+
if now is None or now == goal:
|
|
267
|
+
break
|
|
268
|
+
order = "next" if now < goal else "previous"
|
|
269
|
+
elif orders:
|
|
270
|
+
order = orders.pop(0)
|
|
271
|
+
else:
|
|
272
|
+
break
|
|
273
|
+
self.call("POST", f"/wda/pickerwheel/{quote(best, safe='')}/select", {"order": order, "offset": 0.15},
|
|
274
|
+
deadline.remaining())
|
|
275
|
+
before, shown = shown, self._wheel_value(path, deadline)
|
|
276
|
+
if wheel_shows(shown, wanted):
|
|
277
|
+
return
|
|
278
|
+
if shown == before:
|
|
279
|
+
break # the wheel didn't move: at its end, or the step isn't supported
|
|
280
|
+
# Fails closed: the caller learns the wheel isn't at the value, and a chain stops here (a 25-minute timer
|
|
281
|
+
# started after SET_TEXT "12" returned as if it had worked, verify runs of 12 Oct).
|
|
282
|
+
raise WheelNotSet(f"the wheel shows {str(shown)[:40]!r}, not {wanted[:40]!r}: none of its items matched")
|
|
283
|
+
|
|
284
|
+
def _wheel_value(self, path, deadline):
|
|
285
|
+
try:
|
|
286
|
+
shown = self.call("GET", path + "/attribute/value", timeout=min(3, max(.5, deadline.remaining())))
|
|
287
|
+
except TransportError:
|
|
288
|
+
return None
|
|
289
|
+
return shown if isinstance(shown, str) else None
|
|
290
|
+
|
|
291
|
+
def clear_text(self, target, timeout=10):
|
|
292
|
+
"""Empty an editable field (a caller replacing its text: a label, a title). Best effort."""
|
|
293
|
+
self._clear_field(target, Deadline(timeout))
|
|
294
|
+
|
|
295
|
+
def replace_text(self, target, text, timeout=10):
|
|
296
|
+
"""Replace a text field's content through one element reference; False: nothing typed, the caller types."""
|
|
297
|
+
validate_input_text(text, multiline=target.role == "TextView")
|
|
298
|
+
query = (target.value or "").strip()
|
|
299
|
+
filled = bool(query) and query != (target.label or "").strip()
|
|
300
|
+
# A search field is typed into by keystrokes, never through the element (Safari swaps in
|
|
301
|
+
# another field on focus): only a filled one, whose clear leaves keyboard focus in it.
|
|
302
|
+
if target.role not in ("TextField", "TextView", "SearchField") or not target.label or (
|
|
303
|
+
target.role == "SearchField" and not filled):
|
|
304
|
+
return False
|
|
305
|
+
deadline = Deadline(timeout)
|
|
306
|
+
identifier = self._unique_element(target.role, target.label, deadline)
|
|
307
|
+
if not identifier:
|
|
308
|
+
return False
|
|
309
|
+
path = f"/element/{quote(identifier, safe='')}"
|
|
310
|
+
self._dispatching()
|
|
311
|
+
if filled:
|
|
312
|
+
self.call("POST", path + "/clear", {}, min(5, deadline.remaining()))
|
|
313
|
+
self._frame_mark("TYPE")
|
|
314
|
+
if target.role == "SearchField":
|
|
315
|
+
self._sends(self.call, "POST", "/wda/keys", {"value": [text]}, deadline.remaining())
|
|
316
|
+
else:
|
|
317
|
+
self.call("POST", path + "/value", {"value": text}, deadline.remaining())
|
|
318
|
+
self._frame_dispatched()
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
def _clear_field(self, target, deadline):
|
|
322
|
+
query = (target.value or "").strip()
|
|
323
|
+
if (target.role not in {"SearchField", "TextField", "TextView"} or not query
|
|
324
|
+
or query == (target.label or "").strip()):
|
|
325
|
+
return False
|
|
326
|
+
def literal(text):
|
|
327
|
+
return text.replace("\\", "\\\\").replace("'", "\\'")
|
|
328
|
+
name = (target.label or "").strip()
|
|
329
|
+
kind = f"type == 'XCUIElementType{target.role}'"
|
|
330
|
+
# WDA re-finds an element through the query that found it. Found by its value, the field
|
|
331
|
+
# was gone once emptied: /clear emptied it, then failed after ~2.7 s ("not present in the
|
|
332
|
+
# current view anymore"; 83 s of mem-015's 276 s, 24 Sep). By name, or as the only field of
|
|
333
|
+
# its kind, the same clear takes ~0.4 s; the value finds it only when neither is unique. A long
|
|
334
|
+
# value is read truncated ("head … tail", state._wda_text): its head finds it (an exact match
|
|
335
|
+
# found nothing, and SET_TEXT appended to a note body: iOSWorld mem-004, 24 Sep).
|
|
336
|
+
by_value = (f"{kind} AND value == '{literal(query)}'" if " … " not in query and len(query) < 500
|
|
337
|
+
else f"{kind} AND value BEGINSWITH '{literal(query.split(' … ', 1)[0][:150])}'")
|
|
338
|
+
queries = ([f"{kind} AND (name == '{literal(name)}' OR label == '{literal(name)}')"] if name else []) + [
|
|
339
|
+
kind, by_value]
|
|
340
|
+
for predicate in queries:
|
|
341
|
+
try:
|
|
342
|
+
found = self.call("POST", "/elements", {"using": "predicate string", "value": predicate},
|
|
343
|
+
min(5, deadline.remaining()))
|
|
344
|
+
except TransportError:
|
|
345
|
+
continue
|
|
346
|
+
ids = [i for i in (element_id(e) for e in (found if isinstance(found, list) else [])) if i]
|
|
347
|
+
if len(ids) == 1:
|
|
348
|
+
self._dispatching()
|
|
349
|
+
self._sends(self.call, "POST", f"/element/{quote(ids[0], safe='')}/clear", {},
|
|
350
|
+
min(5, deadline.remaining()))
|
|
351
|
+
# A unique field was cleared and focused by XCTest.
|
|
352
|
+
return True
|
|
353
|
+
if ids and predicate == by_value:
|
|
354
|
+
# Several fields hold this text: clearing the first could empty the wrong one.
|
|
355
|
+
raise DriverRejection("unsupported_or_stale_target")
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
def _await_keyboard(self, deadline, seconds=None):
|
|
359
|
+
"""True once a software keyboard is up, or, when none appears, a hardware one is attached.
|
|
360
|
+
|
|
361
|
+
A parked keyboard (off screen: simulators, a paired keyboard) still takes keystrokes
|
|
362
|
+
in the focused field. Measured on iOSWorld's simulators (24 Sep): 3 of 4 ran in
|
|
363
|
+
hardware-keyboard mode and every TYPE failed "did not take keyboard focus" although
|
|
364
|
+
/wda/keys typed fine. A visible keyboard is still preferred while it may be animating in.
|
|
365
|
+
"""
|
|
366
|
+
tapped = time.monotonic()
|
|
367
|
+
end = tapped + (WDA_KEYBOARD_WAIT_SECONDS if seconds is None else seconds)
|
|
368
|
+
parked = False
|
|
369
|
+
hardware = self.url in self._parked_keyboards
|
|
370
|
+
if hardware:
|
|
371
|
+
# No software keyboard will come: one read, once focus has had time to move.
|
|
372
|
+
time.sleep(max(0.0, min(tapped + WDA_PARKED_FOCUS_SECONDS - time.monotonic(),
|
|
373
|
+
deadline.end - time.monotonic() - WDA_READ_MARGIN_SECONDS)))
|
|
374
|
+
first = True
|
|
375
|
+
# At least one read: a slow host's focus sleep can outlast the window (CI read nothing, 25 Sep).
|
|
376
|
+
while (first or time.monotonic() < end) and deadline.end - time.monotonic() > WDA_READ_MARGIN_SECONDS:
|
|
377
|
+
first = False
|
|
378
|
+
current = self.observe(timeout=deadline.remaining())
|
|
379
|
+
keyboard = getattr(current, "keyboard", "") or (
|
|
380
|
+
"visible" if any("SUBMIT" in e.actions for e in current.elements) else "")
|
|
381
|
+
if keyboard == "visible":
|
|
382
|
+
self._parked_keyboards.discard(self.url)
|
|
383
|
+
return True
|
|
384
|
+
parked = parked or keyboard == "parked"
|
|
385
|
+
if parked and (hardware or time.monotonic() > end - WDA_KEYBOARD_WAIT_SECONDS / 2):
|
|
386
|
+
self._parked_keyboards.add(self.url)
|
|
387
|
+
return True
|
|
388
|
+
if parked:
|
|
389
|
+
self._parked_keyboards.add(self.url)
|
|
390
|
+
return parked
|
|
391
|
+
|
|
392
|
+
def write_text(self, target, snapshot, text, append=False, submit=False, timeout=30):
|
|
393
|
+
"""Write ``text`` into ``target`` and prove it by reading the whole field back.
|
|
394
|
+
An append is typed at the end of the field's text and never empties it; a replace whose read-back differs is
|
|
395
|
+
repaired once (emptied, written again). An append's read-back is reported as it is.
|
|
396
|
+
Failing before anything that can change the field keeps its own error, so a caller may resend. Failing after is
|
|
397
|
+
WriteInterrupted, whatever the last request said."""
|
|
398
|
+
self._write_sent = False
|
|
399
|
+
try:
|
|
400
|
+
return self._write_whole(target, snapshot, text, append, submit, timeout)
|
|
401
|
+
except Exception as error:
|
|
402
|
+
if not self._write_sent:
|
|
403
|
+
raise
|
|
404
|
+
raise WriteInterrupted("The write stopped partway; request outcome unknown; not retried") from error
|
|
405
|
+
|
|
406
|
+
def _write_whole(self, target, snapshot, text, append, submit, timeout):
|
|
407
|
+
deadline = Deadline(timeout)
|
|
408
|
+
grounded_target("TYPE", target, snapshot)
|
|
409
|
+
rank = self._write_rank = self._rank(snapshot, target)
|
|
410
|
+
if (not isinstance(text, str) or len(text) > WDA_WRITE_MAX_CHARS or append and not text.strip()
|
|
411
|
+
or any(unicodedata.category(c) in {"Cc", "Cs", "Zl", "Zp"} and c not in "\n\t" for c in text)):
|
|
412
|
+
raise ValueError("TYPE requires bounded text without control characters")
|
|
413
|
+
text = text.replace("\t", " ")
|
|
414
|
+
shown = target.text if target.placeholder else (target.value or "")
|
|
415
|
+
multiline = target.role == "TextView" or "\n" in shown
|
|
416
|
+
# A chat composer sends on Return: its line breaks are pasted, or become spaces, never typed as keys.
|
|
417
|
+
chat = (getattr(snapshot, "bundle_id", "") in CHAT_APPS
|
|
418
|
+
or bool(CHAT_FIELD.search(f"{target.label or ''} {target.placeholder or ''}")))
|
|
419
|
+
breaks = "\n" in text and (not multiline or chat)
|
|
420
|
+
old = self._field_text(target, snapshot, deadline) if append else ""
|
|
421
|
+
# An empty field reads as its placeholder, which not every field names (SwiftUI's vertical
|
|
422
|
+
# TextField does not): an earlier text is known only when it cannot be a placeholder.
|
|
423
|
+
known = old is not None and (not old or target.role == "TextView" or bool(target.placeholder)
|
|
424
|
+
or "\n" in old or len(old) >= 60)
|
|
425
|
+
# An append is typed at the end of what the field holds, never pasted: a paste writes the whole field, so the
|
|
426
|
+
# field is emptied first, and a paste or retype that then fails leaves it empty (iOSWorld multi-087, 5 Nov 2026:
|
|
427
|
+
# a packing list in Notes was emptied and saved so).
|
|
428
|
+
paste = bool(text) and not append and (len(text) >= self.paste_min_chars or breaks) and old is not None
|
|
429
|
+
receipt = {"method": "clear" if not text else "paste" if paste else "type", "repaired": False,
|
|
430
|
+
"flattened": False}
|
|
431
|
+
current, field = snapshot, target
|
|
432
|
+
if not append:
|
|
433
|
+
if self._emptied(field, current, deadline):
|
|
434
|
+
current, field = self._reread(field, deadline)
|
|
435
|
+
expected = (old or "") + text if append else text
|
|
436
|
+
if paste and not self._paste(field, current, expected, deadline):
|
|
437
|
+
receipt["method"] = "type"
|
|
438
|
+
if receipt["method"] == "type":
|
|
439
|
+
typed = text
|
|
440
|
+
if breaks:
|
|
441
|
+
# Never Return in a field not known to take line breaks: it may submit.
|
|
442
|
+
typed, receipt["flattened"] = " ".join(typed.split("\n")), True
|
|
443
|
+
expected = " ".join(expected.split("\n"))
|
|
444
|
+
self._type_chunks(field, current, typed, deadline, multiline)
|
|
445
|
+
value, current = self._read_back(target, deadline, rank)
|
|
446
|
+
matches = self._fits(value, expected, text, append and not known, breaks, field)
|
|
447
|
+
if not matches and value is not None and breaks and self._same_text(value, " ".join(expected.split("\n"))):
|
|
448
|
+
receipt["flattened"], matches = True, True # a one-line field: its line breaks became spaces
|
|
449
|
+
if not text and not matches and value is not None and value != shown:
|
|
450
|
+
# Emptied, yet it reads as something: emptied again, what stays is its placeholder.
|
|
451
|
+
field = self._same_field(current, target, rank) or field
|
|
452
|
+
self._emptied(field, current, deadline)
|
|
453
|
+
again, current = self._read_back(target, deadline, rank)
|
|
454
|
+
if again == value:
|
|
455
|
+
receipt["placeholder"], value, matches = value, "", True
|
|
456
|
+
elif not matches and value is not None and not append:
|
|
457
|
+
# A replace is repaired once: emptied and written again. An append never is: its field holds the person's
|
|
458
|
+
# text, and emptying it to rewrite the whole lost a note when the rewrite didn't land (multi-087: Notes'
|
|
459
|
+
# list formatting changed the added lines, the read-back differed, and the note was left empty). Its
|
|
460
|
+
# read-back is reported as it is.
|
|
461
|
+
receipt["repaired"] = True
|
|
462
|
+
field = self._same_field(current, target, rank) or field
|
|
463
|
+
if self._emptied(field, current, deadline):
|
|
464
|
+
current, field = self._reread(field, deadline)
|
|
465
|
+
if not self._paste(field, current, expected, deadline):
|
|
466
|
+
self._type_chunks(field, None, " ".join(expected.split("\n")) if breaks else expected,
|
|
467
|
+
deadline, multiline)
|
|
468
|
+
value, current = self._read_back(target, deadline, rank)
|
|
469
|
+
matches = self._fits(value, expected, text, False, breaks, field)
|
|
470
|
+
# Return only on a field proven to hold what was written: a garbled search runs, a garbled message sends
|
|
471
|
+
# (9 Oct: Safari searched "Grace Hopperen.m.wikipedia.org/wiki/IOS 26"). Fails closed.
|
|
472
|
+
receipt["submitted"] = bool(submit and matches)
|
|
473
|
+
if submit and matches:
|
|
474
|
+
self._dispatching()
|
|
475
|
+
self._frame_mark("SUBMIT")
|
|
476
|
+
self._sends(self.call, "POST", "/wda/keys", {"value": ["\n"]}, deadline.remaining())
|
|
477
|
+
self._frame_dispatched()
|
|
478
|
+
receipt.update(value=value, expected=expected, matches=bool(matches),
|
|
479
|
+
chars=None if value is None else len(value))
|
|
480
|
+
return receipt
|
|
481
|
+
|
|
482
|
+
def _fits(self, value, expected, text, unknown_start, breaks, field):
|
|
483
|
+
"""The field's text read back is what was written."""
|
|
484
|
+
if self._same_text(value, expected, field=field):
|
|
485
|
+
return True
|
|
486
|
+
if unknown_start and value is not None:
|
|
487
|
+
return self._same_text(value, text, suffix=True) or breaks and self._same_text(
|
|
488
|
+
value, " ".join(text.split("\n")), suffix=True)
|
|
489
|
+
return False
|
|
490
|
+
|
|
491
|
+
@staticmethod
|
|
492
|
+
def _same_text(value, expected, suffix=False, field=None):
|
|
493
|
+
"""``value`` (a field read back) is ``expected``."""
|
|
494
|
+
if value is None:
|
|
495
|
+
return False
|
|
496
|
+
if not expected and field is not None and field.placeholder and value == field.placeholder:
|
|
497
|
+
return True
|
|
498
|
+
|
|
499
|
+
def plain(text):
|
|
500
|
+
return text.translate(_TYPOGRAPHIC).replace("\r\n", "\n")
|
|
501
|
+
return (value == expected or plain(value) == plain(expected)
|
|
502
|
+
or suffix and plain(value).endswith(plain(expected)))
|
|
503
|
+
|
|
504
|
+
@staticmethod
|
|
505
|
+
def _rank(snapshot, target):
|
|
506
|
+
"""(fields of ``target``'s kind on ``snapshot``, its place among them)."""
|
|
507
|
+
fields = [e for e in snapshot.elements if e.editable and e.role == target.role]
|
|
508
|
+
return len(fields), next((i for i, e in enumerate(fields) if e == target), None)
|
|
509
|
+
|
|
510
|
+
def _same_field(self, snapshot, target, rank=None):
|
|
511
|
+
"""``target`` on a later ``snapshot``."""
|
|
512
|
+
fields = [e for e in snapshot.elements if e.editable and e.role == target.role]
|
|
513
|
+
same = [e for e in fields if e.locator == target.locator]
|
|
514
|
+
if len(same) == 1:
|
|
515
|
+
return same[0]
|
|
516
|
+
if target.label:
|
|
517
|
+
named = [e for e in fields if e.label == target.label]
|
|
518
|
+
if len(named) == 1:
|
|
519
|
+
return named[0]
|
|
520
|
+
if rank is not None and rank[1] is not None and len(fields) == rank[0]:
|
|
521
|
+
return fields[rank[1]]
|
|
522
|
+
|
|
523
|
+
def distance(e):
|
|
524
|
+
return abs(e.rect[0] - target.rect[0]) + abs(e.rect[1] - target.rect[1])
|
|
525
|
+
near = min(fields, key=distance, default=None)
|
|
526
|
+
return near if near is not None and distance(near) < .08 else None
|
|
527
|
+
|
|
528
|
+
def _field_text(self, target, snapshot, deadline):
|
|
529
|
+
"""The field's whole text now, or None when it cannot be read."""
|
|
530
|
+
if target.placeholder and target.value == target.placeholder:
|
|
531
|
+
return ""
|
|
532
|
+
if not truncated_field_value(target.value):
|
|
533
|
+
return target.value or ""
|
|
534
|
+
return self._active_text(target, snapshot, deadline, focus=True)
|
|
535
|
+
|
|
536
|
+
def _active_id(self, target, snapshot, deadline, focus=False):
|
|
537
|
+
"""WDA's id for the field holding keyboard focus when it is ``target`` (their frames agree), else None."""
|
|
538
|
+
for attempt in range(2 if focus else 1):
|
|
539
|
+
try:
|
|
540
|
+
identifier = element_id(self.call("GET", "/element/active", None, min(3, deadline.remaining())) or {})
|
|
541
|
+
if identifier:
|
|
542
|
+
rect = self.call("GET", f"/element/{quote(identifier, safe='')}/rect", None,
|
|
543
|
+
min(3, deadline.remaining()))
|
|
544
|
+
if self._frame_agrees(rect, target, snapshot):
|
|
545
|
+
return identifier
|
|
546
|
+
except (TransportError, KeyError, TypeError):
|
|
547
|
+
pass
|
|
548
|
+
if attempt == 0 and focus:
|
|
549
|
+
x, y = target.center
|
|
550
|
+
self._sends(self._tap, x * snapshot.width, y * snapshot.height, deadline)
|
|
551
|
+
time.sleep(WDA_PARKED_FOCUS_SECONDS)
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
def _active_text(self, target, snapshot, deadline, focus=False):
|
|
555
|
+
identifier = self._active_id(target, snapshot, deadline, focus)
|
|
556
|
+
if identifier is None:
|
|
557
|
+
return None
|
|
558
|
+
path = f"/element/{quote(identifier, safe='')}/attribute/"
|
|
559
|
+
try:
|
|
560
|
+
value = self.call("GET", path + "value", None, min(3, deadline.remaining()))
|
|
561
|
+
if value and target.role != "TextView":
|
|
562
|
+
placeholder = self.call("GET", path + "placeholderValue", None, min(3, deadline.remaining()))
|
|
563
|
+
if placeholder and value == placeholder:
|
|
564
|
+
return ""
|
|
565
|
+
except (TransportError, TypeError):
|
|
566
|
+
return None
|
|
567
|
+
return value if isinstance(value, str) else ""
|
|
568
|
+
|
|
569
|
+
@staticmethod
|
|
570
|
+
def _frame_agrees(rect, target, snapshot):
|
|
571
|
+
"""A WDA rect (points) is ``target``'s frame."""
|
|
572
|
+
if not isinstance(rect, dict):
|
|
573
|
+
return False
|
|
574
|
+
try:
|
|
575
|
+
ax, ay, aw, ah = (float(rect[k]) / d for k, d in (("x", snapshot.width), ("y", snapshot.height),
|
|
576
|
+
("width", snapshot.width), ("height", snapshot.height)))
|
|
577
|
+
except (KeyError, TypeError, ValueError):
|
|
578
|
+
return False
|
|
579
|
+
tx, ty, tw, th = target.rect
|
|
580
|
+
slack = .01
|
|
581
|
+
return (ax - slack <= tx + tw / 2 <= ax + aw + slack and ay - slack <= ty + th / 2 <= ay + ah + slack
|
|
582
|
+
or tx - slack <= ax + aw / 2 <= tx + tw + slack and ty - slack <= ay + ah / 2 <= ty + th + slack)
|
|
583
|
+
|
|
584
|
+
def _emptied(self, target, snapshot, deadline):
|
|
585
|
+
"""True once ``target`` was emptied, False when it already was (and is left alone)."""
|
|
586
|
+
text = target.text if target.placeholder else (target.value or "")
|
|
587
|
+
if not text.strip() or text.strip() == (target.label or "").strip() and not target.placeholder:
|
|
588
|
+
return False
|
|
589
|
+
self._dispatching()
|
|
590
|
+
# A named field is cleared by its name; an unnamed one (CloudSlides' slide body, one of several
|
|
591
|
+
# text fields) through the field holding focus: asking WDA for every field of its kind took 1.8 s.
|
|
592
|
+
if not (target.label and self._clear_field(target, deadline)):
|
|
593
|
+
identifier = self._active_id(target, snapshot, deadline, focus=True)
|
|
594
|
+
if identifier is None:
|
|
595
|
+
raise FieldNotCleared("The field's text couldn't be cleared; no more was written")
|
|
596
|
+
# A lost answer may follow a sent clear: the error stops the write rather than typing over it.
|
|
597
|
+
self._sends(self.call, "POST", f"/element/{quote(identifier, safe='')}/clear", {},
|
|
598
|
+
min(8, deadline.remaining()))
|
|
599
|
+
return True
|
|
600
|
+
|
|
601
|
+
def _paste(self, target, snapshot, text, deadline):
|
|
602
|
+
"""Paste ``text`` through the empty field's edit menu (pressed twice at most); False when no Paste showed."""
|
|
603
|
+
where = (self.url, getattr(snapshot, "bundle_id", None))
|
|
604
|
+
if where in self._paste_missing:
|
|
605
|
+
# This app offered no Paste before: the second miss cost another ~12 s for nothing (the frontier trace
|
|
606
|
+
# review measured 15 s writes on the phone, 9 Oct). Typing goes on as the fallback always did.
|
|
607
|
+
return False
|
|
608
|
+
try:
|
|
609
|
+
self.call("POST", "/wda/setPasteboard", {"content": base64.b64encode(text.encode()).decode(),
|
|
610
|
+
"contentType": "plaintext"}, min(5, deadline.remaining()))
|
|
611
|
+
except TransportError:
|
|
612
|
+
return False
|
|
613
|
+
# An Allow Paste that follows is Mobster's own paste of this text (system_dialogs answers it as such).
|
|
614
|
+
from .system_dialogs import note_paste
|
|
615
|
+
note_paste(self)
|
|
616
|
+
left, top, width, height = target.rect
|
|
617
|
+
x = left + width / 2
|
|
618
|
+
y = top + min(height / 2, 22 / snapshot.height) # the first line: a long press on text moves the caret
|
|
619
|
+
self._dispatching()
|
|
620
|
+
self._frame_mark("TYPE")
|
|
621
|
+
try:
|
|
622
|
+
for attempt in range(2):
|
|
623
|
+
if attempt:
|
|
624
|
+
self._sends(self._tap, x * snapshot.width, y * snapshot.height, deadline)
|
|
625
|
+
time.sleep(WDA_PARKED_FOCUS_SECONDS)
|
|
626
|
+
self._sends(self._pointer, [
|
|
627
|
+
{"type": "pointerMove", "duration": 0, "x": round(x * snapshot.width),
|
|
628
|
+
"y": round(y * snapshot.height)},
|
|
629
|
+
{"type": "pointerDown"}, {"type": "pause", "duration": WDA_PASTE_PRESS_MS},
|
|
630
|
+
{"type": "pointerUp"}], deadline)
|
|
631
|
+
end = time.monotonic() + WDA_PASTE_MENU_SECONDS
|
|
632
|
+
while time.monotonic() < end and deadline.end - time.monotonic() > WDA_READ_MARGIN_SECONDS:
|
|
633
|
+
# One query, not a screen read: a read may also ask WDA which layers show (up to 3 s
|
|
634
|
+
# a read in CloudSlides' editor, 26 Sep).
|
|
635
|
+
try:
|
|
636
|
+
found = self.call("POST", "/elements", {"using": "predicate string", "value":
|
|
637
|
+
"type IN {'XCUIElementTypeMenuItem', 'XCUIElementTypeButton'} "
|
|
638
|
+
"AND label == 'Paste'"}, min(3, deadline.remaining()))
|
|
639
|
+
identifier = element_id(found[0]) if isinstance(found, list) and found else None
|
|
640
|
+
rect = identifier and self.call("GET", f"/element/{quote(identifier, safe='')}/rect", None,
|
|
641
|
+
min(3, deadline.remaining()))
|
|
642
|
+
except TransportError:
|
|
643
|
+
identifier = rect = None
|
|
644
|
+
if isinstance(rect, dict) and all(finite_positive(rect.get(k)) or rect.get(k) == 0
|
|
645
|
+
for k in ("x", "y", "width", "height")):
|
|
646
|
+
self._sends(self._tap, rect["x"] + rect["width"] / 2, rect["y"] + rect["height"] / 2, deadline)
|
|
647
|
+
return True
|
|
648
|
+
time.sleep(.1)
|
|
649
|
+
self._paste_missing.add(where)
|
|
650
|
+
return False
|
|
651
|
+
finally:
|
|
652
|
+
self._frame_dispatched()
|
|
653
|
+
|
|
654
|
+
def _reread(self, target, deadline):
|
|
655
|
+
"""(a new read, ``target`` on it)."""
|
|
656
|
+
fresh = self.observe(timeout=min(5, deadline.remaining()))
|
|
657
|
+
return fresh, self._same_field(fresh, target, self._write_rank) or target
|
|
658
|
+
|
|
659
|
+
def _type_chunks(self, target, snapshot, text, deadline, multiline):
|
|
660
|
+
"""Type ``text`` (a known multi-line field takes its line breaks), in chunks typing accepts."""
|
|
661
|
+
limit = 4000 if multiline else 1000
|
|
662
|
+
chunks = [text[i:i + limit] for i in range(0, len(text), limit)] or [""]
|
|
663
|
+
fresh = snapshot
|
|
664
|
+
if fresh is None or time.monotonic() - fresh.captured_at > WDA_FRESH_SECONDS:
|
|
665
|
+
fresh, target = self._reread(target, deadline)
|
|
666
|
+
first = chunks[0]
|
|
667
|
+
if first.strip() and self._active_id(target, fresh, deadline) is not None:
|
|
668
|
+
# The field holds focus (it was just emptied through it): keystrokes, no focusing tap. A tap on
|
|
669
|
+
# an emptied CloudSlides body left it without focus every second write (F2, 26 Sep).
|
|
670
|
+
self._dispatching()
|
|
671
|
+
self._frame_mark("TYPE")
|
|
672
|
+
self._sends(self.call, "POST", "/wda/keys", {"value": [first]}, deadline.remaining())
|
|
673
|
+
self._frame_dispatched()
|
|
674
|
+
elif first.strip():
|
|
675
|
+
try:
|
|
676
|
+
self._type_text("TYPE", target, fresh, first, deadline, multiline=multiline)
|
|
677
|
+
except NoKeyboardFocus:
|
|
678
|
+
# The focusing tap was made but no keyboard was seen (none shows with a hardware keyboard
|
|
679
|
+
# on some screens): keystrokes go where focus is, so only when the field holds it.
|
|
680
|
+
if self._active_id(target, fresh, deadline) is None:
|
|
681
|
+
raise
|
|
682
|
+
self._frame_mark("TYPE")
|
|
683
|
+
self._sends(self.call, "POST", "/wda/keys", {"value": [first]}, deadline.remaining())
|
|
684
|
+
self._frame_dispatched()
|
|
685
|
+
for chunk in chunks[1:]:
|
|
686
|
+
self._sends(self.call, "POST", "/wda/keys", {"value": [chunk]}, deadline.remaining())
|
|
687
|
+
|
|
688
|
+
def _read_back(self, target, deadline, rank=None):
|
|
689
|
+
"""(the field's whole text, the read)."""
|
|
690
|
+
fresh = self.observe(timeout=min(5, deadline.remaining()))
|
|
691
|
+
field = self._same_field(fresh, target, rank)
|
|
692
|
+
if field is not None and not truncated_field_value(field.value):
|
|
693
|
+
return (field.text if field.placeholder else field.value or ""), fresh
|
|
694
|
+
return self._active_text(field or target, fresh, deadline), fresh
|