agent-device 0.17.2 → 0.17.3
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.
- package/README.md +2 -2
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.apk → agent-device-android-multitouch-helper-0.17.3.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.3.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.manifest.json → agent-device-android-multitouch-helper-0.17.3.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.apk → agent-device-android-snapshot-helper-0.17.3.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.3.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.manifest.json → agent-device-android-snapshot-helper-0.17.3.manifest.json} +6 -6
- package/dist/src/123.js +1 -1
- package/dist/src/1231.js +1 -1
- package/dist/src/1352.js +1 -1
- package/dist/src/221.js +2 -2
- package/dist/src/2403.js +3 -3
- package/dist/src/2415.js +36 -29
- package/dist/src/5611.js +1 -0
- package/dist/src/5827.js +1 -0
- package/dist/src/6088.js +1 -0
- package/dist/src/6133.js +1 -0
- package/dist/src/7847.js +1 -1
- package/dist/src/9471.js +1 -1
- package/dist/src/9542.js +6 -3
- package/dist/src/9616.js +2 -1
- package/dist/src/9818.js +1 -1
- package/dist/src/android-snapshot-helper.d.ts +0 -1
- package/dist/src/android.js +5 -5
- package/dist/src/apple.js +1 -1
- package/dist/src/apps.js +10 -10
- package/dist/src/args.js +53 -14
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-output.js +29 -0
- package/dist/src/cli.js +15 -16
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +52 -0
- package/dist/src/contracts.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/generic.js +5 -5
- package/dist/src/index.d.ts +111 -8
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +3 -3
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +14 -0
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +2 -2
- package/dist/src/session.js +8 -10
- package/dist/src/snapshot.js +2 -2
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +14 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +297 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +328 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +121 -172
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +153 -5
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +171 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +0 -10
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +64 -18
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +200 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +406 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +82 -169
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +425 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +77 -6
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +10 -1
- package/ios-runner/README.md +5 -5
- package/ios-runner/RUNNER_PROTOCOL.md +0 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.2.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.2.apk.sha256 +0 -1
- package/dist/src/1534.js +0 -1
- package/dist/src/1620.js +0 -1
- package/dist/src/1644.js +0 -2
- package/dist/src/2284.js +0 -1
- package/dist/src/2474.js +0 -1
- package/dist/src/2672.js +0 -1
- package/dist/src/3393.js +0 -1
- package/dist/src/5898.js +0 -1
- package/dist/src/695.js +0 -1
- package/dist/src/7455.js +0 -1
- package/dist/src/8173.js +0 -27
- package/dist/src/8407.js +0 -1
- package/dist/src/8699.js +0 -1
- package/dist/src/9673.js +0 -1
- package/dist/src/9974.js +0 -1
package/dist/src/snapshot.js
CHANGED
|
@@ -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"./
|
|
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
|
|
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 D,dispatchWaitViaRuntime as M,createDaemonRuntimePolicy as P,recordIfSession as O,resolveSessionDevice as E,withSessionlessRunnerCleanup as x}from"./selector-runtime.js";import{createAgentDevice as U}from"./123.js";var K={};e.r(K),e.d(K,{handleSnapshotCommands:()=>eh});let L=new Set(["com.android.permissioncontroller","com.google.android.permissioncontroller","com.google.android.packageinstaller","com.android.packageinstaller"]),B=new Set(["android","com.android.systemui"]),$=/^android:id\/(?:alertTitle|message|button[123]|parentPanel|buttonPanel|contentPanel)$/i,q=/^android:id\/button[123]$/i,H=/(?:^|:)id\/permission_/i,T=/\b(?:is(?:n't| not) responding|keeps stopping|has stopped|close app|app info)\b/i,F=/^(?:ok|allow|allow all|while using the app|only this time|yes|continue|save|confirm|turn on|open settings)$/i,j=/^(?:cancel|deny|don.t allow|don’t allow|not now|no|dismiss|close|close app|later|skip)$/i;function z(e,t){let a=t[0];if(!a)return;let n=G(e,a.index);for(let a of t.slice(1)){let t=new Set(G(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 G(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 V(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 W(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 J(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 Q(e,t,a={}){if("wait"===t)return await X(e,a.timeoutMs??1e4);if("get"===t){var n;let t=await ee(e);return{kind:"alertStatus",platform:"android",action:"get",alert:n=t?.alert??null,...n?R("Alert visible"):R("No alert visible")}}return await Y(e,t)}async function X(e,t){let a=Date.now(),n=await Z(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 Y(e,t){var a;let n,s,o=await Z(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),et(t,o.alert,i.label);if("dismiss"===t)return await p(e),et(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 Z(e,t){let a=Date.now();for(;Date.now()-a<t;){let t=await ee(e);if(t)return t;await k(300)}return null}async function ee(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??"",L.has(a)||H.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??"",B.has(a)&&T.test(J(t))});if(0===t.length)return[];let a=z(e,t);return void 0===a?t:V(e,a).filter(e=>e.bundleId&&B.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=>$.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=z(e,r);return void 0===i?r:V(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=J(n);if(!e||!n.rect||!W(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||(F.test(t.trim())?"accept":j.test(t.trim())?"dismiss":"neutral")}(n,e)})}return a}(l),p=l.filter(e=>J(e)&&!W(e)),u=J((t=p).find(e=>/(?:^|:)id\/(?:alertTitle|permission_message)$/i.test(e.identifier??"")))||J(t[0]),c=(a=p,n=u,(r=a.map(e=>J(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 et(e,t,a){return{kind:"alertHandled",platform:"android",action:e,handled:!0,alert:t,button:a,...R(`Alert ${e}ed`)}}async function ea(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 er(e,await Q(o,r,{timeoutMs:t}))}if("macos"===o.platform){let t=async e=>await h(e,i);return await en(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 en(e,r,d)}async function en(e,t,a){if("wait"===t)return await es(e,a);let n="accept"===t||"dismiss"===t?t:"get";return"accept"===n||"dismiss"===n?await eo(e,n,a):er(e,await a("get"))}async function es(e,t){let a=I(e.req.positionals?.[1])??1e4,n=Date.now();for(;Date.now()-n<a;){try{return er(e,await t("get"))}catch{}await k(300)}return w("COMMAND_FAILED","alert wait timed out")}async function eo(e,t,a){var n,s;let o,i,l=Date.now();for(;Date.now()-l<2e3;){try{return er(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 er(e,t){return O(e.sessionStore,e.session,e.req,t),{ok:!0,data:t}}async function ei(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 O(n,s,t,g??{setting:i,state:l}),{ok:!0,data:g??{setting:i,state:l}}}async function el(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 ed(e)}}}}async function ed(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 ep(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 ep(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 eu(e){return await ef({...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 ec(e){return await ef({...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 ef(e){var t,a;let{req:n,sessionName:s,logPath:o,sessionStore:i}=e,{session:l,device:d}=await E(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 x(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});return{snapshot:i.snapshot,...N(i),appName:n?.appBundleId?n.appName??n.appBundleId:void 0,appBundleId:n?.appBundleId}}}}({req:t,logPath:n,session:o,device:i,snapshotScope:l}),...P("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=D({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 el({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 em={snapshot:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>await eu({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 ec({req:e,sessionName:t,logPath:a,sessionStore:n}),wait:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>await M({req:e,sessionName:t,logPath:a,sessionStore:n}),alert:async({req:e,sessionName:t,logPath:a,sessionStore:n})=>{let{session:s,device:o}=await E(n,t,e.flags);return await x(s,o,async()=>await ea({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 E(n,t,e.flags);return await x(l,d,async()=>await ei({req:e,logPath:a,sessionStore:n,session:l,device:d,parsed:i.parsed}))}};async function eh(e){let t=em[e.req.command];return t?await t(e):null}export{K as snapshot_namespaceObject};
|
|
@@ -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
|