agent-device 0.17.2 → 0.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +2 -2
  2. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.apk → agent-device-android-multitouch-helper-0.17.4.apk} +0 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.4.apk.sha256 +1 -0
  4. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.manifest.json → agent-device-android-multitouch-helper-0.17.4.manifest.json} +4 -4
  5. package/android-snapshot-helper/README.md +3 -0
  6. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.apk → agent-device-android-snapshot-helper-0.17.4.apk} +0 -0
  7. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.4.apk.sha256 +1 -0
  8. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.manifest.json → agent-device-android-snapshot-helper-0.17.4.manifest.json} +6 -6
  9. package/dist/src/123.js +1 -1
  10. package/dist/src/1231.js +1 -1
  11. package/dist/src/1352.js +1 -1
  12. package/dist/src/221.js +2 -2
  13. package/dist/src/2403.js +3 -3
  14. package/dist/src/2415.js +37 -29
  15. package/dist/src/4012.js +4 -0
  16. package/dist/src/5611.js +1 -0
  17. package/dist/src/5827.js +1 -0
  18. package/dist/src/6088.js +1 -0
  19. package/dist/src/6133.js +1 -0
  20. package/dist/src/7847.js +1 -1
  21. package/dist/src/9471.js +1 -1
  22. package/dist/src/9542.js +6 -3
  23. package/dist/src/9616.js +2 -1
  24. package/dist/src/9818.js +1 -1
  25. package/dist/src/android-snapshot-helper.d.ts +0 -1
  26. package/dist/src/android.js +5 -5
  27. package/dist/src/apple.js +1 -1
  28. package/dist/src/apps.js +10 -10
  29. package/dist/src/args.js +53 -14
  30. package/dist/src/batch.js +1 -1
  31. package/dist/src/cli-output.js +30 -0
  32. package/dist/src/cli.js +15 -16
  33. package/dist/src/command-surface.js +1 -1
  34. package/dist/src/contracts.d.ts +52 -0
  35. package/dist/src/contracts.js +1 -1
  36. package/dist/src/find.js +1 -1
  37. package/dist/src/generic.js +5 -5
  38. package/dist/src/index.d.ts +127 -8
  39. package/dist/src/index.js +1 -1
  40. package/dist/src/input-actions.js +1 -1
  41. package/dist/src/interaction.js +1 -1
  42. package/dist/src/react-native.js +1 -1
  43. package/dist/src/record-trace-recording.js +3 -3
  44. package/dist/src/selector-runtime.js +1 -1
  45. package/dist/src/selectors.d.ts +14 -0
  46. package/dist/src/selectors.js +1 -1
  47. package/dist/src/server.js +2 -2
  48. package/dist/src/session.js +8 -10
  49. package/dist/src/snapshot.js +2 -2
  50. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
  51. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +14 -0
  52. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +297 -0
  53. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +327 -0
  54. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +122 -173
  55. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +153 -5
  56. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +171 -0
  57. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +0 -10
  58. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +64 -18
  59. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +200 -0
  60. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +450 -0
  61. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +82 -178
  62. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +425 -0
  63. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +77 -6
  64. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +10 -1
  65. package/ios-runner/README.md +5 -5
  66. package/ios-runner/RUNNER_PROTOCOL.md +0 -1
  67. package/package.json +3 -3
  68. package/server.json +2 -2
  69. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.2.apk.sha256 +0 -1
  70. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.2.apk.sha256 +0 -1
  71. package/dist/src/1534.js +0 -1
  72. package/dist/src/1620.js +0 -1
  73. package/dist/src/1644.js +0 -2
  74. package/dist/src/2284.js +0 -1
  75. package/dist/src/2474.js +0 -1
  76. package/dist/src/2672.js +0 -1
  77. package/dist/src/3393.js +0 -1
  78. package/dist/src/5898.js +0 -1
  79. package/dist/src/695.js +0 -1
  80. package/dist/src/7455.js +0 -1
  81. package/dist/src/8173.js +0 -27
  82. package/dist/src/8407.js +0 -1
  83. package/dist/src/8699.js +0 -1
  84. package/dist/src/9238.js +0 -4
  85. package/dist/src/9673.js +0 -1
  86. package/dist/src/9974.js +0 -1
@@ -1,2 +1,2 @@
1
- import{__webpack_require__ as e}from"./rslib-runtime.js";import{promises as t}from"node:fs";import a from"node:os";import n from"node:path";import{centerOfRect as s}from"./1644.js";import{normalizeError as o,AppError as r}from"./9152.js";import{withDiagnosticTimer as i,emitDiagnostic as l}from"./7599.js";import{pressAndroid as d,backAndroid as p}from"./input-actions.js";import{createDaemonRuntimeSessionStore as u,captureSnapshot as c,dispatchCommand as f,resolveSnapshotScope as m,runMacOsAlertAction as h,errorResponse as w,runIosRunnerCommand as g,isCommandSupportedOnDevice as y,snapshotAndroid as _,annotateScreenshotWithRefs as v,context_contextFromFlags as S}from"./2415.js";import{successText as b}from"./1534.js";import{sleep as I}from"./4829.js";import{buildSnapshotSession as A,dispatchWaitViaRuntime as R,createDaemonRuntimePolicy as C,recordIfSession as N,resolveSessionDevice as M,withSessionlessRunnerCleanup as P}from"./selector-runtime.js";import{parseTimeout as k}from"./2284.js";import{SETTINGS_INVALID_ARGS_MESSAGE as D,isMacOsSettingSupported as E,getUnsupportedMacOsSettingMessage as K}from"./1352.js";import{createAgentDevice as O}from"./123.js";var U={};e.r(U),e.d(U,{handleSnapshotCommands:()=>em});let x=new Set(["com.android.permissioncontroller","com.google.android.permissioncontroller","com.google.android.packageinstaller","com.android.packageinstaller"]),L=new Set(["android","com.android.systemui"]),B=/^android:id\/(?:alertTitle|message|button[123]|parentPanel|buttonPanel|contentPanel)$/i,H=/^android:id\/button[123]$/i,$=/(?:^|:)id\/permission_/i,q=/\b(?:is(?:n't| not) responding|keeps stopping|has stopped|close app|app info)\b/i,T=/^(?:ok|allow|allow all|while using the app|only this time|yes|continue|save|confirm|turn on|open settings)$/i,F=/^(?:cancel|deny|don.t allow|don’t allow|not now|no|dismiss|close|close app|later|skip)$/i;function j(e,t){let a=t[0];if(!a)return;let n=z(e,a.index);for(let a of t.slice(1)){let t=new Set(z(e,a.index));for(let e=n.length-1;e>=0;e-=1)t.has(n[e])||n.splice(e,1)}return n[n.length-1]}function z(e,t){let a=new Map(e.map(e=>[e.index,e])),n=[],s=a.get(t);for(;s;)n.push(s.index),s=void 0===s.parentIndex?void 0:a.get(s.parentIndex);return n.reverse()}function G(e,t){let a=new Map;for(let t of e){if(void 0===t.parentIndex)continue;let e=a.get(t.parentIndex)??[];e.push(t),a.set(t.parentIndex,e)}let n=new Set([t]),s=[t];for(let e of s)for(let t of a.get(e)??[])n.has(t.index)||(n.add(t.index),s.push(t.index));return e.filter(e=>n.has(e.index))}function V(e){let t=e.type??"",a=e.identifier??"";return!!(e.hittable||/\bbutton\b/i.test(t)||H.test(a)||/(?:^|:)id\/permission_(?:allow|deny)/i.test(a))}function W(e){if(!e)return"";let t=[e.label,e.value].filter(e=>"string"==typeof e&&e.trim().length>0);return t[0]?.trim()??""}async function J(e,t,a={}){if("wait"===t)return await Q(e,a.timeoutMs??1e4);if("get"===t){var n;let t=await Z(e);return{kind:"alertStatus",platform:"android",action:"get",alert:n=t?.alert??null,...n?b("Alert visible"):b("No alert visible")}}return await X(e,t)}async function Q(e,t){let a=Date.now(),n=await Y(e,t);if(!n)throw new r("COMMAND_FAILED","alert wait timed out");return{kind:"alertWait",platform:"android",action:"wait",alert:n.alert,waitedMs:Date.now()-a,...b("Alert visible")}}async function X(e,t){var a;let n,s,o=await Y(e,2e3);if(!o)throw new r("COMMAND_FAILED","alert not found",{hint:"If a sheet is visible in snapshot but alert reports no alert, it is likely app-owned UI. Use snapshot -i and press the visible label/ref."});let i=(a=o.buttons,n="accept"===t?"accept":"dismiss",(s=a.find(e=>e.role===n))||("dismiss"===t?a.find(e=>"neutral"===e.role)??null:1===a.length?a[0]??null:null));if(i)return await d(e,i.x,i.y),ee(t,o.alert,i.label);if("dismiss"===t)return await p(e),ee(t,o.alert,"Back");throw new r("COMMAND_FAILED","alert accept found an alert but no accept button",{alert:o.alert,hint:"Inspect alert get --json for visible buttons, then use press by visible label/ref if needed."})}async function Y(e,t){let a=Date.now();for(;Date.now()-a<t;){let t=await Z(e);if(t)return t;await I(300)}return null}async function Z(e){return function(e){var t,a,n,o;let r,i=function(e){let t=e.filter(e=>{var t;let a;return a=(t=e).bundleId??"",x.has(a)||$.test(t.identifier??"")});if(t.length)return{nodes:t,source:"permission"};let a=function(e){let t=e.filter(e=>{var t;let a;return a=(t=e).bundleId??"",L.has(a)&&q.test(W(t))});if(0===t.length)return[];let a=j(e,t);return void 0===a?t:G(e,a).filter(e=>e.bundleId&&L.has(e.bundleId))}(e);return a.length?{nodes:a,source:"system-dialog"}:{nodes:function(e){var t;let a,n,s=e.filter(e=>{var t;return t=e.type??"",/(?:^|[.$])[^.]*Dialog$/i.test(t)}),o=e.filter(e=>B.test(e.identifier??"")),r=s.length?[...s,...o]:(a=(t=o).some(e=>H.test(e.identifier??"")),n=t.some(e=>!H.test(e.identifier??"")),a&&n?t:[]);if(0===r.length)return[];let i=j(e,r);return void 0===i?r:G(e,i)}(e),source:"native-dialog"}}(e),l=i.nodes;if(0===l.length)return null;let d=function(e){let t=new Set,a=[];for(let n of e){let e=W(n);if(!e||!n.rect||!V(n))continue;let o=e.trim().toLowerCase();if(!o||t.has(o))continue;t.add(o);let r=s(n.rect);a.push({label:e,x:r.x,y:r.y,role:function(e,t){var a;let n=(a=e.identifier??"",/(?:^|:)id\/button1$/i.test(a)?"accept":/(?:^|:)id\/button2$/i.test(a)?"dismiss":/(?:^|:)id\/button3$/i.test(a)?"neutral":/(?:^|:)id\/permission_allow/i.test(a)?"accept":/(?:^|:)id\/permission_deny/i.test(a)?"dismiss":null);return n||(T.test(t.trim())?"accept":F.test(t.trim())?"dismiss":"neutral")}(n,e)})}return a}(l),p=l.filter(e=>W(e)&&!V(e)),u=W((t=p).find(e=>/(?:^|:)id\/(?:alertTitle|permission_message)$/i.test(e.identifier??"")))||W(t[0]),c=(a=p,n=u,(r=a.map(e=>W(e)).filter(e=>e&&e!==n)).length?[...new Set(r)].join("\n"):void 0),f=(o=l,o.find(e=>e.bundleId)?.bundleId);return{alert:{...u?{title:u}:{},...c?{message:c}:{},buttons:d.map(e=>e.label),platform:"android",source:i.source,...f?{packageName:f}:{}},buttons:d}}((await i("snapshot_capture",async()=>await _(e,{helperWaitForIdleTimeoutMs:0,includeHiddenContentHints:!1}),{backend:"android",purpose:"alert"})).nodes)}function ee(e,t,a){return{kind:"alertHandled",platform:"android",action:e,handled:!0,alert:t,button:a,...b(`Alert ${e}ed`)}}async function et(e){var t;let{req:a,logPath:n,session:s,device:o}=e,r="accept"===(t=a.positionals?.[0])||"dismiss"===t||"wait"===t?t:"get",i=s?"frontmost-app"===s.surface?{surface:"frontmost-app"}:{bundleId:s.appBundleId,surface:s.surface}:{};if(!y("alert",o))return w("UNSUPPORTED_OPERATION","alert is not supported on this device");if("android"===o.platform){let t=k(a.positionals?.[1])??1e4;return eo(e,await J(o,r,{timeoutMs:t}))}if("macos"===o.platform){let t=async e=>await h(e,i);return await ea(e,r,t)}let l={verbose:a.flags?.verbose,logPath:n,traceLogPath:s?.trace?.outPath,requestId:a.meta?.requestId},d=async e=>await g(o,{command:"alert",action:e,appBundleId:s?.appBundleId},l);return await ea(e,r,d)}async function ea(e,t,a){if("wait"===t)return await en(e,a);let n="accept"===t||"dismiss"===t?t:"get";return"accept"===n||"dismiss"===n?await es(e,n,a):eo(e,await a("get"))}async function en(e,t){let a=k(e.req.positionals?.[1])??1e4,n=Date.now();for(;Date.now()-n<a;){try{return eo(e,await t("get"))}catch{}await I(300)}return w("COMMAND_FAILED","alert wait timed out")}async function es(e,t,a){var n,s;let o,i,l=Date.now();for(;Date.now()-l<2e3;){try{return eo(e,await a(t))}catch(t){i=t;let e=String(t?.message??"").toLowerCase();if(!e.includes("alert not found")&&!e.includes("no alert"))break}await I(300)}throw(n=i)instanceof r&&(s=n,(o=String(s?.message??"").toLowerCase()).includes("alert not found")||o.includes("no alert"))?new r(n.code,n.message,{...n.details??{},hint:"If the permission sheet is visible in snapshot or screenshot but alert reports no alert, take a scoped snapshot around the visible button label and use press @ref."}):n}function eo(e,t){return N(e.sessionStore,e.session,e.req,t),{ok:!0,data:t}}async function er(e){let{req:t,logPath:a,sessionStore:n,session:s,device:o,parsed:r}=e,{setting:i,state:l,appBundleId:d,permissionTarget:p,latitude:u,longitude:c}=r;if(!y("settings",o))return w("UNSUPPORTED_OPERATION","settings is not supported on this device");if("macos"===o.platform&&!E(i))return w("INVALID_ARGS",K(i));let m=d??s?.appBundleId;if("clear-app-state"===i&&!m)return w("INVALID_ARGS","settings clear-app-state requires an app id when no app is bound to the session");let h="clear-app-state"===i?[i,l,m??""]:"permission"===i?[i,l,p??"",t.positionals?.[3]??"",m??""]:"location"===i&&"set"===l?[i,l,u??"",c??"",m??""]:[i,l,m??""],g=await f(o,"settings",h,t.flags?.out,{...S(a,t.flags,m,s?.trace?.outPath)});return N(n,s,t,g??{setting:i,state:l}),{ok:!0,data:g??{setting:i,state:l}}}async function ei(e){var t,a;let n;if("snapshot"!==e.command||"android"!==e.device.platform)return;let s=o(e.error);if("COMMAND_FAILED"===(t=s).code&&(a=t,n=`${a.message}
2
- ${a.hint??""}`,/Android UI hierarchy dump timed out/i.test(n)||/Stock UIAutomator fallback was skipped/i.test(n)||/Android accessibility snapshots can be blocked/i.test(n)||function(e){if(!e||"object"!=typeof e)return!1;let t=String(e.errorType??""),a=String(e.message??"");return/TimeoutException/i.test(t)||/timed out/i.test(a)}(t.details?.helper)||function(e){var t;if(!e)return!1;let a=e?.timeoutMs,n=e?.cmd,s=Array.isArray(t=e?.args)?t.map(String):"string"==typeof t?t.split(/\s+/):[];return"number"==typeof a&&"adb"===n&&s.includes("uiautomator")&&s.includes("dump")}(t.details)))return{ok:!1,error:{...s,details:{...s.details??{},androidSnapshotTimeoutScreenshot:await el(e)}}}}async function el(e){try{var s,r,i;let o=await t.mkdtemp(n.join(a.tmpdir(),"agent-device-android-snapshot-timeout-")),d=n.join(o,"snapshot-timeout-overlay-refs.png"),p=(s=await f(e.device,"screenshot",[d],void 0,{...S(e.logPath,{screenshotNoStabilize:!0},e.session?.appBundleId,e.session?.trace?.outPath),surface:e.session?.surface}),r=d,(i=s,"object"==typeof i&&null!==i&&"path"in i&&"string"==typeof i.path)?s.path:r);await t.access(p);let u=await ed(p,e.session);return l({level:"warn",phase:"android_snapshot_timeout_screenshot_captured",data:{path:p,overlayRefCount:"overlayRefCount"in u?u.overlayRefCount:void 0,overlayRefsAnnotated:"overlayRefsAnnotated"in u?u.overlayRefsAnnotated:void 0}}),u}catch(t){let e=o(t);return l({level:"warn",phase:"android_snapshot_timeout_screenshot_failed",data:{error:e.message}}),{captureFailed:!0,error:e.message}}}async function ed(e,t){if(!t?.snapshot)return{path:e,overlayRefsRequested:!0,overlayRefsAnnotated:!1,overlayRefSource:"unavailable",overlayRefCount:0};try{let a=await v({screenshotPath:e,snapshot:t.snapshot});return{path:e,overlayRefsRequested:!0,overlayRefsAnnotated:a.length>0,overlayRefCount:a.length,overlayRefSource:"session-snapshot",overlayRefs:a}}catch(a){let t=o(a);return l({level:"warn",phase:"android_snapshot_timeout_screenshot_overlay_failed",data:{path:e,error:t.message}}),{path:e,overlayRefsRequested:!0,overlayRefsAnnotated:!1,overlayRefSource:"session-snapshot",overlayRefCount:0,overlayAnnotationError:t.message}}}async function ep(e){return await ec({...e,command:"snapshot",unsupportedMessage:"snapshot is not supported on this device",execute:async({runtime:e,sessionName:t,req:a,snapshotScope:n})=>{let s=await e.capture.snapshot({session:t,interactiveOnly:a.flags?.snapshotInteractiveOnly,compact:a.flags?.snapshotCompact,depth:a.flags?.snapshotDepth,scope:n,raw:a.flags?.snapshotRaw,forceFull:a.flags?.snapshotForceFull});return{data:s,record:{kind:"snapshot",nodes:s.nodes.length,truncated:s.truncated}}}})}async function eu(e){return await ec({...e,command:"diff",unsupportedMessage:"diff is not supported on this device",execute:async({runtime:e,sessionName:t,req:a,snapshotScope:n})=>{let s=await e.capture.diffSnapshot({session:t,interactiveOnly:a.flags?.snapshotInteractiveOnly,compact:a.flags?.snapshotCompact,depth:a.flags?.snapshotDepth,scope:n,raw:a.flags?.snapshotRaw});return{data:s,record:{kind:"diff",mode:"snapshot",baselineInitialized:s.baselineInitialized,summary:s.summary}}}})}async function ec(e){var t,a;let{req:n,sessionName:s,logPath:o,sessionStore:i}=e,{session:l,device:d}=await M(i,s,n.flags);if(!y(e.command,d))return w("UNSUPPORTED_OPERATION",e.unsupportedMessage);let p=m(n.flags?.snapshotScope,l);if(!p.ok)return p;let f=(t=e.command,a=l,"ios"!==d.platform||a?.appBundleId?null:w("SESSION_NOT_FOUND",`iOS ${t} requires an active app session on the target device. Run open first (for example: open --session ${a?.name??"sim"} --platform ios --device "<name>" <app>).`));return f||await P(l,d,async()=>{var t,a;let f,m,h=function(e){let{req:t,sessionName:a,logPath:n,sessionStore:s,session:o,device:i,snapshotScope:l}=e;return O({backend:function(e){let{req:t,logPath:a,session:n,device:s,snapshotScope:o}=e;return{platform:s.platform,captureSnapshot:async(e,r)=>{let i=await c({device:s,session:n,flags:t.flags,outPath:r?.outPath??t.flags?.out,logPath:a,snapshotScope:o});return{snapshot:i.snapshot,analysis:i.analysis,androidSnapshot:i.androidSnapshot,freshness:i.freshness,appName:n?.appBundleId?n.appName??n.appBundleId:void 0,appBundleId:n?.appBundleId}}}}({req:t,logPath:n,session:o,device:i,snapshotScope:l}),...C("snapshot"),sessions:u({sessionName:a,getSession:()=>s.get(a),recordOptions:{includeSnapshot:!0},setRecord:e=>{var n;let o=function(e){if(!e.snapshot)throw new r("UNKNOWN","snapshot runtime did not produce session state");return e}(e),l=s.get(a);s.set(a,function(e){var t,a;let{current:n,sessionName:s,device:o,record:r,refScopedSnapshot:i}=e,l=(t=n,a=r,i&&a.snapshot?.nodes.length===0&&t?.snapshot!==void 0),d=l?n.snapshot:r.snapshot,p=A({session:n,sessionName:s,device:o,snapshot:d,appBundleId:r.appBundleId});return p.snapshotScopeSource=function(e){let{current:t,keepCurrentSnapshot:a,refScopedSnapshot:n}=e;if(n)return a?t?.snapshotScopeSource:t?.snapshotScopeSource??t?.snapshot}({current:n,keepCurrentSnapshot:l,refScopedSnapshot:i}),r.appName&&(p.appName=r.appName),p}({current:l,sessionName:a,device:i,record:o,refScopedSnapshot:(n=t,n.flags?.snapshotScope?.trim().startsWith("@")===!0)}))}})})}({req:n,sessionName:s,logPath:o,sessionStore:i,session:l,device:d,snapshotScope:p.scope});try{m=await e.execute({runtime:h,sessionName:s,req:n,snapshotScope:p.scope})}catch(a){let t=await ei({error:a,command:e.command,logPath:o,session:l,device:d});if(!t)throw a;return t}return(f=(t={req:n,sessionName:s,sessionStore:i,result:m.record}).sessionStore.get(t.sessionName))&&t.sessionStore.recordAction(f,{command:t.req.command,positionals:t.req.positionals??[],flags:t.req.flags??{},result:"snapshot"===(a=t.result).kind?{nodes:a.nodes,truncated:a.truncated}:{mode:a.mode,baselineInitialized:a.baselineInitialized,summary:a.summary}}),{ok:!0,data:m.data}})}let ef={snapshot:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>await ep({req:e,sessionName:t,logPath:a,sessionStore:n}),diff:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>e.positionals?.[0]!=="snapshot"?w("INVALID_ARGS","diff currently supports only: diff snapshot"):await eu({req:e,sessionName:t,logPath:a,sessionStore:n}),wait:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>await R({req:e,sessionName:t,logPath:a,sessionStore:n}),alert:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>{let{session:s,device:o}=await M(n,t,e.flags);return await P(s,o,async()=>await et({req:e,logPath:a,sessionStore:n,session:s,device:o}))},settings:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>{let s,o,r,i=(s=e.positionals?.[0]?.toLowerCase(),o=e.positionals?.[1]?.toLowerCase(),r=e.positionals?.[2]?.toLowerCase(),"clear-app-state"===s?{ok:!0,parsed:{setting:s,state:"clear",appBundleId:"clear"===o?e.positionals?.[2]:e.positionals?.[1]}}:s&&o&&("permission"!==s||r)&&("location"!==s||"set"!==o||e.positionals?.[2]&&e.positionals?.[3])?{ok:!0,parsed:{setting:s,state:o,permissionTarget:r,latitude:e.positionals?.[2],longitude:e.positionals?.[3]}}:w("INVALID_ARGS",D));if(!i.ok)return i;let{session:l,device:d}=await M(n,t,e.flags);return await P(l,d,async()=>await er({req:e,logPath:a,sessionStore:n,session:l,device:d,parsed:i.parsed}))}};async function em(e){let t=ef[e.req.command];return t?await t(e):null}export{U as snapshot_namespaceObject};
1
+ import{__webpack_require__ as e}from"./rslib-runtime.js";import{promises as t}from"node:fs";import a from"node:os";import n from"node:path";import{centerOfRect as s}from"./9616.js";import{normalizeError as o,AppError as r}from"./9152.js";import{withDiagnosticTimer as i,emitDiagnostic as l}from"./7599.js";import{pressAndroid as d,backAndroid as p}from"./input-actions.js";import{createDaemonRuntimeSessionStore as u,captureSnapshot as c,dispatchCommand as f,resolveSnapshotScope as m,runMacOsAlertAction as h,errorResponse as w,runIosRunnerCommand as g,isCommandSupportedOnDevice as y,snapshotAndroid as _,annotateScreenshotWithRefs as v,context_contextFromFlags as S}from"./2415.js";import{SETTINGS_INVALID_ARGS_MESSAGE as b,parseTimeout as I,getUnsupportedMacOsSettingMessage as A,successText as R,isMacOsSettingSupported as C,snapshotCaptureAnnotationsFrom as N}from"./6133.js";import{sleep as k}from"./4829.js";import{buildSnapshotSession as M,dispatchWaitViaRuntime as P,createDaemonRuntimePolicy as D,recordIfSession as E,resolveSessionDevice as O,withSessionlessRunnerCleanup as K}from"./selector-runtime.js";import{createAgentDevice as U}from"./123.js";import{summarizeSnapshotDiagnostics as x}from"./4012.js";var L={};e.r(L),e.d(L,{handleSnapshotCommands:()=>ew});let B=new Set(["com.android.permissioncontroller","com.google.android.permissioncontroller","com.google.android.packageinstaller","com.android.packageinstaller"]),$=new Set(["android","com.android.systemui"]),H=/^android:id\/(?:alertTitle|message|button[123]|parentPanel|buttonPanel|contentPanel)$/i,q=/^android:id\/button[123]$/i,T=/(?:^|:)id\/permission_/i,F=/\b(?:is(?:n't| not) responding|keeps stopping|has stopped|close app|app info)\b/i,j=/^(?:ok|allow|allow all|while using the app|only this time|yes|continue|save|confirm|turn on|open settings)$/i,z=/^(?:cancel|deny|don.t allow|don’t allow|not now|no|dismiss|close|close app|later|skip)$/i;function G(e,t){let a=t[0];if(!a)return;let n=V(e,a.index);for(let a of t.slice(1)){let t=new Set(V(e,a.index));for(let e=n.length-1;e>=0;e-=1)t.has(n[e])||n.splice(e,1)}return n[n.length-1]}function V(e,t){let a=new Map(e.map(e=>[e.index,e])),n=[],s=a.get(t);for(;s;)n.push(s.index),s=void 0===s.parentIndex?void 0:a.get(s.parentIndex);return n.reverse()}function W(e,t){let a=new Map;for(let t of e){if(void 0===t.parentIndex)continue;let e=a.get(t.parentIndex)??[];e.push(t),a.set(t.parentIndex,e)}let n=new Set([t]),s=[t];for(let e of s)for(let t of a.get(e)??[])n.has(t.index)||(n.add(t.index),s.push(t.index));return e.filter(e=>n.has(e.index))}function J(e){let t=e.type??"",a=e.identifier??"";return!!(e.hittable||/\bbutton\b/i.test(t)||q.test(a)||/(?:^|:)id\/permission_(?:allow|deny)/i.test(a))}function Q(e){if(!e)return"";let t=[e.label,e.value].filter(e=>"string"==typeof e&&e.trim().length>0);return t[0]?.trim()??""}async function X(e,t,a={}){if("wait"===t)return await Y(e,a.timeoutMs??1e4);if("get"===t){var n;let t=await et(e);return{kind:"alertStatus",platform:"android",action:"get",alert:n=t?.alert??null,...n?R("Alert visible"):R("No alert visible")}}return await Z(e,t)}async function Y(e,t){let a=Date.now(),n=await ee(e,t);if(!n)throw new r("COMMAND_FAILED","alert wait timed out");return{kind:"alertWait",platform:"android",action:"wait",alert:n.alert,waitedMs:Date.now()-a,...R("Alert visible")}}async function Z(e,t){var a;let n,s,o=await ee(e,2e3);if(!o)throw new r("COMMAND_FAILED","alert not found",{hint:"If a sheet is visible in snapshot but alert reports no alert, it is likely app-owned UI. Use snapshot -i and press the visible label/ref."});let i=(a=o.buttons,n="accept"===t?"accept":"dismiss",(s=a.find(e=>e.role===n))||("dismiss"===t?a.find(e=>"neutral"===e.role)??null:1===a.length?a[0]??null:null));if(i)return await d(e,i.x,i.y),ea(t,o.alert,i.label);if("dismiss"===t)return await p(e),ea(t,o.alert,"Back");throw new r("COMMAND_FAILED","alert accept found an alert but no accept button",{alert:o.alert,hint:"Inspect alert get --json for visible buttons, then use press by visible label/ref if needed."})}async function ee(e,t){let a=Date.now();for(;Date.now()-a<t;){let t=await et(e);if(t)return t;await k(300)}return null}async function et(e){return function(e){var t,a,n,o;let r,i=function(e){let t=e.filter(e=>{var t;let a;return a=(t=e).bundleId??"",B.has(a)||T.test(t.identifier??"")});if(t.length)return{nodes:t,source:"permission"};let a=function(e){let t=e.filter(e=>{var t;let a;return a=(t=e).bundleId??"",$.has(a)&&F.test(Q(t))});if(0===t.length)return[];let a=G(e,t);return void 0===a?t:W(e,a).filter(e=>e.bundleId&&$.has(e.bundleId))}(e);return a.length?{nodes:a,source:"system-dialog"}:{nodes:function(e){var t;let a,n,s=e.filter(e=>{var t;return t=e.type??"",/(?:^|[.$])[^.]*Dialog$/i.test(t)}),o=e.filter(e=>H.test(e.identifier??"")),r=s.length?[...s,...o]:(a=(t=o).some(e=>q.test(e.identifier??"")),n=t.some(e=>!q.test(e.identifier??"")),a&&n?t:[]);if(0===r.length)return[];let i=G(e,r);return void 0===i?r:W(e,i)}(e),source:"native-dialog"}}(e),l=i.nodes;if(0===l.length)return null;let d=function(e){let t=new Set,a=[];for(let n of e){let e=Q(n);if(!e||!n.rect||!J(n))continue;let o=e.trim().toLowerCase();if(!o||t.has(o))continue;t.add(o);let r=s(n.rect);a.push({label:e,x:r.x,y:r.y,role:function(e,t){var a;let n=(a=e.identifier??"",/(?:^|:)id\/button1$/i.test(a)?"accept":/(?:^|:)id\/button2$/i.test(a)?"dismiss":/(?:^|:)id\/button3$/i.test(a)?"neutral":/(?:^|:)id\/permission_allow/i.test(a)?"accept":/(?:^|:)id\/permission_deny/i.test(a)?"dismiss":null);return n||(j.test(t.trim())?"accept":z.test(t.trim())?"dismiss":"neutral")}(n,e)})}return a}(l),p=l.filter(e=>Q(e)&&!J(e)),u=Q((t=p).find(e=>/(?:^|:)id\/(?:alertTitle|permission_message)$/i.test(e.identifier??"")))||Q(t[0]),c=(a=p,n=u,(r=a.map(e=>Q(e)).filter(e=>e&&e!==n)).length?[...new Set(r)].join("\n"):void 0),f=(o=l,o.find(e=>e.bundleId)?.bundleId);return{alert:{...u?{title:u}:{},...c?{message:c}:{},buttons:d.map(e=>e.label),platform:"android",source:i.source,...f?{packageName:f}:{}},buttons:d}}((await i("snapshot_capture",async()=>await _(e,{helperWaitForIdleTimeoutMs:0,includeHiddenContentHints:!1}),{backend:"android",purpose:"alert"})).nodes)}function ea(e,t,a){return{kind:"alertHandled",platform:"android",action:e,handled:!0,alert:t,button:a,...R(`Alert ${e}ed`)}}async function en(e){var t;let{req:a,logPath:n,session:s,device:o}=e,r="accept"===(t=a.positionals?.[0])||"dismiss"===t||"wait"===t?t:"get",i=s?"frontmost-app"===s.surface?{surface:"frontmost-app"}:{bundleId:s.appBundleId,surface:s.surface}:{};if(!y("alert",o))return w("UNSUPPORTED_OPERATION","alert is not supported on this device");if("android"===o.platform){let t=I(a.positionals?.[1])??1e4;return ei(e,await X(o,r,{timeoutMs:t}))}if("macos"===o.platform){let t=async e=>await h(e,i);return await es(e,r,t)}let l={verbose:a.flags?.verbose,logPath:n,traceLogPath:s?.trace?.outPath,requestId:a.meta?.requestId},d=async e=>await g(o,{command:"alert",action:e,appBundleId:s?.appBundleId},l);return await es(e,r,d)}async function es(e,t,a){if("wait"===t)return await eo(e,a);let n="accept"===t||"dismiss"===t?t:"get";return"accept"===n||"dismiss"===n?await er(e,n,a):ei(e,await a("get"))}async function eo(e,t){let a=I(e.req.positionals?.[1])??1e4,n=Date.now();for(;Date.now()-n<a;){try{return ei(e,await t("get"))}catch{}await k(300)}return w("COMMAND_FAILED","alert wait timed out")}async function er(e,t,a){var n,s;let o,i,l=Date.now();for(;Date.now()-l<2e3;){try{return ei(e,await a(t))}catch(t){i=t;let e=String(t?.message??"").toLowerCase();if(!e.includes("alert not found")&&!e.includes("no alert"))break}await k(300)}throw(n=i)instanceof r&&(s=n,(o=String(s?.message??"").toLowerCase()).includes("alert not found")||o.includes("no alert"))?new r(n.code,n.message,{...n.details??{},hint:"If the permission sheet is visible in snapshot or screenshot but alert reports no alert, take a scoped snapshot around the visible button label and use press @ref."}):n}function ei(e,t){return E(e.sessionStore,e.session,e.req,t),{ok:!0,data:t}}async function el(e){let{req:t,logPath:a,sessionStore:n,session:s,device:o,parsed:r}=e,{setting:i,state:l,appBundleId:d,permissionTarget:p,latitude:u,longitude:c}=r;if(!y("settings",o))return w("UNSUPPORTED_OPERATION","settings is not supported on this device");if("macos"===o.platform&&!C(i))return w("INVALID_ARGS",A(i));let m=d??s?.appBundleId;if("clear-app-state"===i&&!m)return w("INVALID_ARGS","settings clear-app-state requires an app id when no app is bound to the session");let h="clear-app-state"===i?[i,l,m??""]:"permission"===i?[i,l,p??"",t.positionals?.[3]??"",m??""]:"location"===i&&"set"===l?[i,l,u??"",c??"",m??""]:[i,l,m??""],g=await f(o,"settings",h,t.flags?.out,{...S(a,t.flags,m,s?.trace?.outPath)});return E(n,s,t,g??{setting:i,state:l}),{ok:!0,data:g??{setting:i,state:l}}}async function ed(e){var t,a;let n;if("snapshot"!==e.command||"android"!==e.device.platform)return;let s=o(e.error);if("COMMAND_FAILED"===(t=s).code&&(a=t,n=`${a.message}
2
+ ${a.hint??""}`,/Android UI hierarchy dump timed out/i.test(n)||/Stock UIAutomator fallback was skipped/i.test(n)||/Android accessibility snapshots can be blocked/i.test(n)||function(e){if(!e||"object"!=typeof e)return!1;let t=String(e.errorType??""),a=String(e.message??"");return/TimeoutException/i.test(t)||/timed out/i.test(a)}(t.details?.helper)||function(e){var t;if(!e)return!1;let a=e?.timeoutMs,n=e?.cmd,s=Array.isArray(t=e?.args)?t.map(String):"string"==typeof t?t.split(/\s+/):[];return"number"==typeof a&&"adb"===n&&s.includes("uiautomator")&&s.includes("dump")}(t.details)))return{ok:!1,error:{...s,details:{...s.details??{},androidSnapshotTimeoutScreenshot:await ep(e)}}}}async function ep(e){try{var s,r,i;let o=await t.mkdtemp(n.join(a.tmpdir(),"agent-device-android-snapshot-timeout-")),d=n.join(o,"snapshot-timeout-overlay-refs.png"),p=(s=await f(e.device,"screenshot",[d],void 0,{...S(e.logPath,{screenshotNoStabilize:!0},e.session?.appBundleId,e.session?.trace?.outPath),surface:e.session?.surface}),r=d,(i=s,"object"==typeof i&&null!==i&&"path"in i&&"string"==typeof i.path)?s.path:r);await t.access(p);let u=await eu(p,e.session);return l({level:"warn",phase:"android_snapshot_timeout_screenshot_captured",data:{path:p,overlayRefCount:"overlayRefCount"in u?u.overlayRefCount:void 0,overlayRefsAnnotated:"overlayRefsAnnotated"in u?u.overlayRefsAnnotated:void 0}}),u}catch(t){let e=o(t);return l({level:"warn",phase:"android_snapshot_timeout_screenshot_failed",data:{error:e.message}}),{captureFailed:!0,error:e.message}}}async function eu(e,t){if(!t?.snapshot)return{path:e,overlayRefsRequested:!0,overlayRefsAnnotated:!1,overlayRefSource:"unavailable",overlayRefCount:0};try{let a=await v({screenshotPath:e,snapshot:t.snapshot});return{path:e,overlayRefsRequested:!0,overlayRefsAnnotated:a.length>0,overlayRefCount:a.length,overlayRefSource:"session-snapshot",overlayRefs:a}}catch(a){let t=o(a);return l({level:"warn",phase:"android_snapshot_timeout_screenshot_overlay_failed",data:{path:e,error:t.message}}),{path:e,overlayRefsRequested:!0,overlayRefsAnnotated:!1,overlayRefSource:"session-snapshot",overlayRefCount:0,overlayAnnotationError:t.message}}}async function ec(e){return await em({...e,command:"snapshot",unsupportedMessage:"snapshot is not supported on this device",execute:async({runtime:e,sessionName:t,req:a,snapshotScope:n})=>{let s=await e.capture.snapshot({session:t,interactiveOnly:a.flags?.snapshotInteractiveOnly,depth:a.flags?.snapshotDepth,scope:n,raw:a.flags?.snapshotRaw,forceFull:a.flags?.snapshotForceFull});return{data:s,record:{kind:"snapshot",nodes:s.nodes.length,truncated:s.truncated}}}})}async function ef(e){return await em({...e,command:"diff",unsupportedMessage:"diff is not supported on this device",execute:async({runtime:e,sessionName:t,req:a,snapshotScope:n})=>{let s=await e.capture.diffSnapshot({session:t,interactiveOnly:a.flags?.snapshotInteractiveOnly,depth:a.flags?.snapshotDepth,scope:n,raw:a.flags?.snapshotRaw});return{data:s,record:{kind:"diff",mode:"snapshot",baselineInitialized:s.baselineInitialized,summary:s.summary}}}})}async function em(e){var t,a;let{req:n,sessionName:s,logPath:o,sessionStore:i}=e,{session:l,device:d}=await O(i,s,n.flags);if(!y(e.command,d))return w("UNSUPPORTED_OPERATION",e.unsupportedMessage);let p=m(n.flags?.snapshotScope,l);if(!p.ok)return p;let f=(t=e.command,a=l,"ios"!==d.platform||a?.appBundleId?null:w("SESSION_NOT_FOUND",`iOS ${t} requires an active app session on the target device. Run open first (for example: open --session ${a?.name??"sim"} --platform ios --device "<name>" <app>).`));return f||await K(l,d,async()=>{var t,a;let f,m,h=function(e){let{req:t,sessionName:a,logPath:n,sessionStore:s,session:o,device:i,snapshotScope:l}=e;return U({backend:function(e){let{req:t,logPath:a,session:n,device:s,snapshotScope:o}=e;return{platform:s.platform,captureSnapshot:async(e,r)=>{let i=await c({device:s,session:n,flags:t.flags,outPath:r?.outPath??t.flags?.out,logPath:a,snapshotScope:o}),l=x(n);return{snapshot:i.snapshot,...N(i),...l?{snapshotDiagnostics:l}:{},appName:n?.appBundleId?n.appName??n.appBundleId:void 0,appBundleId:n?.appBundleId}}}}({req:t,logPath:n,session:o,device:i,snapshotScope:l}),...D("snapshot"),sessions:u({sessionName:a,getSession:()=>s.get(a),recordOptions:{includeSnapshot:!0},setRecord:e=>{var n;let o=function(e){if(!e.snapshot)throw new r("UNKNOWN","snapshot runtime did not produce session state");return e}(e),l=s.get(a);s.set(a,function(e){var t,a;let{current:n,sessionName:s,device:o,record:r,refScopedSnapshot:i}=e,l=(t=n,a=r,i&&a.snapshot?.nodes.length===0&&t?.snapshot!==void 0),d=l?n.snapshot:r.snapshot,p=M({session:n,sessionName:s,device:o,snapshot:d,appBundleId:r.appBundleId});return p.snapshotScopeSource=function(e){let{current:t,keepCurrentSnapshot:a,refScopedSnapshot:n}=e;if(n)return a?t?.snapshotScopeSource:t?.snapshotScopeSource??t?.snapshot}({current:n,keepCurrentSnapshot:l,refScopedSnapshot:i}),r.appName&&(p.appName=r.appName),p}({current:l,sessionName:a,device:i,record:o,refScopedSnapshot:(n=t,n.flags?.snapshotScope?.trim().startsWith("@")===!0)}))}})})}({req:n,sessionName:s,logPath:o,sessionStore:i,session:l,device:d,snapshotScope:p.scope});try{m=await e.execute({runtime:h,sessionName:s,req:n,snapshotScope:p.scope})}catch(a){let t=await ed({error:a,command:e.command,logPath:o,session:l,device:d});if(!t)throw a;return t}return(f=(t={req:n,sessionName:s,sessionStore:i,result:m.record}).sessionStore.get(t.sessionName))&&t.sessionStore.recordAction(f,{command:t.req.command,positionals:t.req.positionals??[],flags:t.req.flags??{},result:"snapshot"===(a=t.result).kind?{nodes:a.nodes,truncated:a.truncated}:{mode:a.mode,baselineInitialized:a.baselineInitialized,summary:a.summary}}),{ok:!0,data:m.data}})}let eh={snapshot:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>await ec({req:e,sessionName:t,logPath:a,sessionStore:n}),diff:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>e.positionals?.[0]!=="snapshot"?w("INVALID_ARGS","diff currently supports only: diff snapshot"):await ef({req:e,sessionName:t,logPath:a,sessionStore:n}),wait:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>await P({req:e,sessionName:t,logPath:a,sessionStore:n}),alert:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>{let{session:s,device:o}=await O(n,t,e.flags);return await K(s,o,async()=>await en({req:e,logPath:a,sessionStore:n,session:s,device:o}))},settings:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>{let s,o,r,i=(s=e.positionals?.[0]?.toLowerCase(),o=e.positionals?.[1]?.toLowerCase(),r=e.positionals?.[2]?.toLowerCase(),"clear-app-state"===s?{ok:!0,parsed:{setting:s,state:"clear",appBundleId:"clear"===o?e.positionals?.[2]:e.positionals?.[1]}}:s&&o&&("permission"!==s||r)&&("location"!==s||"set"!==o||e.positionals?.[2]&&e.positionals?.[3])?{ok:!0,parsed:{setting:s,state:o,permissionTarget:r,latitude:e.positionals?.[2],longitude:e.positionals?.[3]}}:w("INVALID_ARGS",b));if(!i.ok)return i;let{session:l,device:d}=await O(n,t,e.flags);return await K(l,d,async()=>await el({req:e,logPath:a,sessionStore:n,session:l,device:d,parsed:i.parsed}))}};async function ew(e){let t=eh[e.req.command];return t?await t(e):null}export{L as snapshot_namespaceObject};
@@ -1,2 +1,3 @@
1
1
  #import "RunnerObjCExceptionCatcher.h"
2
+ #import "RunnerAXSnapshotBridge.h"
2
3
  #import "RunnerSynthesizedGesture.h"
@@ -0,0 +1,14 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <XCTest/XCTest.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ @interface RunnerAXSnapshotBridge : NSObject
7
+
8
+ + (NSDictionary<NSString *, id> *)snapshotTreeForApplication:(XCUIApplication *)application
9
+ maxDepth:(NSInteger)maxDepth
10
+ maxNodes:(NSInteger)maxNodes;
11
+
12
+ @end
13
+
14
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,297 @@
1
+ #import "RunnerAXSnapshotBridge.h"
2
+
3
+ #import <CoreGraphics/CoreGraphics.h>
4
+ #import <objc/message.h>
5
+
6
+ static NSString *const RunnerAXSnapshotOkKey = @"ok";
7
+ static NSString *const RunnerAXSnapshotErrorKey = @"error";
8
+ static NSString *const RunnerAXSnapshotRootKey = @"root";
9
+ static NSString *const RunnerAXSnapshotTruncatedKey = @"truncated";
10
+
11
+ typedef id (*RunnerAXObjectMsgSend)(id, SEL);
12
+ typedef NSInteger (*RunnerAXIntegerMsgSend)(id, SEL);
13
+ typedef id (*RunnerAXSnapshotMsgSend)(id, SEL, id, id, id, NSError **);
14
+
15
+ @implementation RunnerAXSnapshotBridge
16
+
17
+ + (NSDictionary<NSString *, id> *)snapshotTreeForApplication:(XCUIApplication *)application
18
+ maxDepth:(NSInteger)maxDepth
19
+ maxNodes:(NSInteger)maxNodes
20
+ {
21
+ @try {
22
+ id axClient = [self objectFrom:XCUIDevice.sharedDevice selectorName:@"accessibilityInterface"];
23
+ if (nil == axClient) {
24
+ return [self failure:@"XCUIDevice accessibilityInterface is unavailable"];
25
+ }
26
+
27
+ id target = [self accessibilityApplicationForApplication:application axClient:axClient];
28
+ if (nil == target) {
29
+ return [self failure:@"Could not match active AX application for XCTest application"];
30
+ }
31
+
32
+ NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
33
+ id defaults = [self objectFrom:axClient selectorName:@"defaultParameters"];
34
+ if ([defaults isKindOfClass:NSDictionary.class]) {
35
+ [parameters addEntriesFromDictionary:(NSDictionary *)defaults];
36
+ }
37
+ parameters[@"maxDepth"] = @(MAX(0, maxDepth));
38
+ parameters[@"maxChildren"] = @(MAX(1, maxNodes));
39
+ parameters[@"maxArrayCount"] = @(MAX(1, maxNodes));
40
+ parameters[@"traverseFromParentsToChildren"] = @YES;
41
+
42
+ SEL requestSelector = NSSelectorFromString(@"requestSnapshotForElement:attributes:parameters:error:");
43
+ if (![axClient respondsToSelector:requestSelector]) {
44
+ return [self failure:@"AX client does not support requestSnapshotForElement"];
45
+ }
46
+
47
+ NSError *error = nil;
48
+ NSArray<NSString *> *keyPaths = @[
49
+ @"elementType",
50
+ @"identifier",
51
+ @"label",
52
+ @"value",
53
+ @"frame",
54
+ @"enabled",
55
+ @"selected",
56
+ @"hasFocus",
57
+ @"children",
58
+ ];
59
+ // The AX server expects real accessibility attribute identifiers, not snapshot keypath
60
+ // strings; passing raw keypaths silently drops attributes it does not recognize (frame
61
+ // came back zeroed). XCElementSnapshot owns the keypath -> AX attribute mapping.
62
+ NSArray *attributes = keyPaths;
63
+ Class snapshotClass = NSClassFromString(@"XCElementSnapshot");
64
+ SEL mapSelector = NSSelectorFromString(@"axAttributesForElementSnapshotKeyPaths:isMacOS:");
65
+ if ([snapshotClass respondsToSelector:mapSelector]) {
66
+ typedef id (*RunnerAXMapMsgSend)(id, SEL, id, BOOL);
67
+ RunnerAXMapMsgSend mapSend = (RunnerAXMapMsgSend)objc_msgSend;
68
+ id mapped = mapSend(snapshotClass, mapSelector, keyPaths, NO);
69
+ if ([mapped isKindOfClass:NSSet.class]) {
70
+ mapped = [(NSSet *)mapped allObjects];
71
+ }
72
+ if ([mapped isKindOfClass:NSArray.class] && [(NSArray *)mapped count] > 0) {
73
+ // The mapper expands keypaths with extra attributes (automation type, window display
74
+ // id, base type) that are disproportionately expensive for the AX server to compute
75
+ // on large React Native trees. Keep only the attributes we actually consume.
76
+ NSArray *needed = @[ @"ElementType", @"Identifier", @"Label", @"Value", @"Frame",
77
+ @"Enabled", @"Selected", @"Focus" ];
78
+ NSMutableArray *filtered = [NSMutableArray array];
79
+ for (id attribute in (NSArray *)mapped) {
80
+ NSString *name = [attribute description];
81
+ for (NSString *suffix in needed) {
82
+ if ([name hasSuffix:suffix]) {
83
+ [filtered addObject:attribute];
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ attributes = filtered.count > 0 ? filtered : mapped;
89
+ }
90
+ }
91
+ RunnerAXSnapshotMsgSend send = (RunnerAXSnapshotMsgSend)objc_msgSend;
92
+ id result = send(axClient, requestSelector, target, attributes, parameters.copy, &error);
93
+ if (nil == result) {
94
+ return [self failure:error.localizedDescription ?: @"AX snapshot request returned nil"];
95
+ }
96
+
97
+ id root = nil;
98
+ @try {
99
+ root = [result valueForKey:@"_rootElementSnapshot"];
100
+ } @catch (NSException *exception) {
101
+ root = nil;
102
+ }
103
+ if (nil == root) {
104
+ root = result;
105
+ }
106
+
107
+ BOOL truncated = NO;
108
+ NSInteger nodeCount = 0;
109
+ NSDictionary *rootNode = [self dictionaryForSnapshot:root
110
+ depth:0
111
+ maxDepth:maxDepth
112
+ maxNodes:maxNodes
113
+ nodeCount:&nodeCount
114
+ truncated:&truncated];
115
+ if (nil == rootNode) {
116
+ return [self failure:@"AX snapshot root could not be serialized"];
117
+ }
118
+
119
+ return @{
120
+ RunnerAXSnapshotOkKey: @YES,
121
+ RunnerAXSnapshotRootKey: rootNode,
122
+ RunnerAXSnapshotTruncatedKey: @(truncated),
123
+ };
124
+ } @catch (NSException *exception) {
125
+ return [self failure:exception.reason ?: exception.name ?: @"AX snapshot bridge exception"];
126
+ }
127
+ }
128
+
129
+ + (NSDictionary<NSString *, id> *)failure:(NSString *)message
130
+ {
131
+ return @{
132
+ RunnerAXSnapshotOkKey: @NO,
133
+ RunnerAXSnapshotErrorKey: message,
134
+ };
135
+ }
136
+
137
+ + (id)objectFrom:(id)target selectorName:(NSString *)selectorName
138
+ {
139
+ SEL selector = NSSelectorFromString(selectorName);
140
+ if (![target respondsToSelector:selector]) {
141
+ return nil;
142
+ }
143
+ RunnerAXObjectMsgSend send = (RunnerAXObjectMsgSend)objc_msgSend;
144
+ return send(target, selector);
145
+ }
146
+
147
+ + (NSInteger)integerFrom:(id)target selectorName:(NSString *)selectorName
148
+ {
149
+ SEL selector = NSSelectorFromString(selectorName);
150
+ if (![target respondsToSelector:selector]) {
151
+ return 0;
152
+ }
153
+ // processID/processIdentifier return pid_t (int32); reading them through an
154
+ // NSInteger-returning cast is not upper-32-bit safe on arm64. Use the method
155
+ // signature to pick the correctly sized call.
156
+ NSMethodSignature *signature = [target methodSignatureForSelector:selector];
157
+ const char *returnType = signature.methodReturnType;
158
+ if (returnType != NULL && strcmp(returnType, @encode(int)) == 0) {
159
+ typedef int (*RunnerAXIntMsgSend)(id, SEL);
160
+ RunnerAXIntMsgSend send = (RunnerAXIntMsgSend)objc_msgSend;
161
+ return (NSInteger)send(target, selector);
162
+ }
163
+ RunnerAXIntegerMsgSend send = (RunnerAXIntegerMsgSend)objc_msgSend;
164
+ return send(target, selector);
165
+ }
166
+
167
+ + (id)accessibilityApplicationForApplication:(XCUIApplication *)application axClient:(id)axClient
168
+ {
169
+ NSInteger targetProcessID = [self integerFrom:application selectorName:@"processID"];
170
+ id activeApplications = [self objectFrom:axClient selectorName:@"activeApplications"];
171
+ if (![activeApplications isKindOfClass:NSArray.class]) {
172
+ return nil;
173
+ }
174
+
175
+ for (id candidate in (NSArray *)activeApplications) {
176
+ NSInteger candidateProcessID = [self integerFrom:candidate selectorName:@"processIdentifier"];
177
+ if (targetProcessID > 0 && candidateProcessID == targetProcessID) {
178
+ return candidate;
179
+ }
180
+ }
181
+ return nil;
182
+ }
183
+
184
+ + (nullable NSDictionary *)dictionaryForSnapshot:(id)snapshot
185
+ depth:(NSInteger)depth
186
+ maxDepth:(NSInteger)maxDepth
187
+ maxNodes:(NSInteger)maxNodes
188
+ nodeCount:(NSInteger *)nodeCount
189
+ truncated:(BOOL *)truncated
190
+ {
191
+ if (nil == snapshot || *nodeCount >= maxNodes) {
192
+ *truncated = YES;
193
+ return nil;
194
+ }
195
+
196
+ *nodeCount += 1;
197
+ NSMutableDictionary *result = [NSMutableDictionary dictionary];
198
+ result[@"type"] = [self numberValueForKey:@"elementType" snapshot:snapshot] ?: @0;
199
+ result[@"identifier"] = [self stringValueForKey:@"identifier" snapshot:snapshot] ?: @"";
200
+ result[@"label"] = [self stringValueForKey:@"label" snapshot:snapshot] ?: @"";
201
+ result[@"value"] = [self stringValueForKey:@"value" snapshot:snapshot] ?: @"";
202
+ result[@"frame"] = [self frameValueForSnapshot:snapshot];
203
+ result[@"enabled"] = [self boolNumberForKey:@"enabled" snapshot:snapshot defaultValue:YES];
204
+ result[@"selected"] = [self boolNumberForKey:@"selected" snapshot:snapshot defaultValue:NO];
205
+ result[@"focused"] = [self boolNumberForKey:@"hasFocus" snapshot:snapshot defaultValue:NO];
206
+
207
+ NSMutableArray *children = [NSMutableArray array];
208
+ if (depth < maxDepth) {
209
+ for (id child in [self childrenForSnapshot:snapshot]) {
210
+ NSDictionary *childNode = [self dictionaryForSnapshot:child
211
+ depth:depth + 1
212
+ maxDepth:maxDepth
213
+ maxNodes:maxNodes
214
+ nodeCount:nodeCount
215
+ truncated:truncated];
216
+ if (nil != childNode) {
217
+ [children addObject:childNode];
218
+ }
219
+ if (*nodeCount >= maxNodes) {
220
+ *truncated = YES;
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ result[@"children"] = children;
226
+ return result.copy;
227
+ }
228
+
229
+ + (NSArray *)childrenForSnapshot:(id)snapshot
230
+ {
231
+ id children = nil;
232
+ @try {
233
+ children = [snapshot valueForKey:@"children"];
234
+ } @catch (NSException *exception) {
235
+ children = nil;
236
+ }
237
+ return [children isKindOfClass:NSArray.class] ? children : @[];
238
+ }
239
+
240
+ + (nullable NSNumber *)numberValueForKey:(NSString *)key snapshot:(id)snapshot
241
+ {
242
+ id value = nil;
243
+ @try {
244
+ value = [snapshot valueForKey:key];
245
+ } @catch (NSException *exception) {
246
+ return nil;
247
+ }
248
+ return [value isKindOfClass:NSNumber.class] ? value : nil;
249
+ }
250
+
251
+ + (nullable NSString *)stringValueForKey:(NSString *)key snapshot:(id)snapshot
252
+ {
253
+ id value = nil;
254
+ @try {
255
+ value = [snapshot valueForKey:key];
256
+ } @catch (NSException *exception) {
257
+ return nil;
258
+ }
259
+ if (nil == value || value == NSNull.null) {
260
+ return nil;
261
+ }
262
+ if ([value isKindOfClass:NSString.class]) {
263
+ return [(NSString *)value stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
264
+ }
265
+ return [[value description] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
266
+ }
267
+
268
+ + (NSNumber *)boolNumberForKey:(NSString *)key snapshot:(id)snapshot defaultValue:(BOOL)defaultValue
269
+ {
270
+ NSNumber *value = [self numberValueForKey:key snapshot:snapshot];
271
+ return nil == value ? @(defaultValue) : @([value boolValue]);
272
+ }
273
+
274
+ + (NSDictionary *)frameValueForSnapshot:(id)snapshot
275
+ {
276
+ CGRect frame = CGRectZero;
277
+ @try {
278
+ id value = [snapshot valueForKey:@"frame"];
279
+ if ([value isKindOfClass:NSValue.class]
280
+ && strcmp([(NSValue *)value objCType], @encode(CGRect)) == 0) {
281
+ [(NSValue *)value getValue:&frame];
282
+ }
283
+ } @catch (NSException *exception) {
284
+ frame = CGRectZero;
285
+ }
286
+ if (CGRectIsNull(frame) || CGRectIsInfinite(frame)) {
287
+ frame = CGRectZero;
288
+ }
289
+ return @{
290
+ @"x": @(CGRectGetMinX(frame)),
291
+ @"y": @(CGRectGetMinY(frame)),
292
+ @"width": @(CGRectGetWidth(frame)),
293
+ @"height": @(CGRectGetHeight(frame)),
294
+ };
295
+ }
296
+
297
+ @end