agent-device 0.17.3 → 0.17.5

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 (31) hide show
  1. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.3.apk → agent-device-android-multitouch-helper-0.17.5.apk} +0 -0
  2. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.5.apk.sha256 +1 -0
  3. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.3.manifest.json → agent-device-android-multitouch-helper-0.17.5.manifest.json} +4 -4
  4. package/android-snapshot-helper/README.md +3 -0
  5. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.3.apk → agent-device-android-snapshot-helper-0.17.5.apk} +0 -0
  6. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.5.apk.sha256 +1 -0
  7. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.3.manifest.json → agent-device-android-snapshot-helper-0.17.5.manifest.json} +6 -6
  8. package/dist/src/123.js +1 -1
  9. package/dist/src/2415.js +33 -32
  10. package/dist/src/4012.js +4 -0
  11. package/dist/src/6133.js +1 -1
  12. package/dist/src/9542.js +5 -5
  13. package/dist/src/apple.js +1 -1
  14. package/dist/src/cli-output.js +2 -1
  15. package/dist/src/cli.js +4 -4
  16. package/dist/src/index.d.ts +16 -0
  17. package/dist/src/session.js +8 -8
  18. package/dist/src/snapshot.js +2 -2
  19. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +4 -0
  20. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +8 -0
  21. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +3 -4
  22. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +1 -1
  23. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +118 -10
  24. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +2 -2
  25. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +47 -3
  26. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +0 -9
  27. package/package.json +1 -1
  28. package/server.json +2 -2
  29. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.3.apk.sha256 +0 -1
  30. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.3.apk.sha256 +0 -1
  31. package/dist/src/9238.js +0 -4
@@ -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"./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};
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};
@@ -25,6 +25,10 @@ NS_ASSUME_NONNULL_BEGIN
25
25
  x:(double)x
26
26
  y:(double)y;
27
27
 
28
+ // UIInterfaceOrientation of the app (1 portrait, 2 upsideDown, 3 landscapeRight,
29
+ // 4 landscapeLeft), or 0 if unreadable.
30
+ + (NSInteger)interfaceOrientationForApplication:(id)application;
31
+
28
32
  @end
29
33
 
30
34
  NS_ASSUME_NONNULL_END
@@ -131,6 +131,14 @@ static double RunnerSmoothStep(double t);
131
131
  }
132
132
  }
133
133
 
134
+ + (NSInteger)interfaceOrientationForApplication:(id)application {
135
+ SEL selector = NSSelectorFromString(@"interfaceOrientation");
136
+ if (![application respondsToSelector:selector]) {
137
+ return 0; // UIInterfaceOrientationUnknown
138
+ }
139
+ return ((RunnerMsgSendInteger)objc_msgSend)(application, selector);
140
+ }
141
+
134
142
  + (NSString * _Nullable)trySynthesizeTransformWithApplication:(id)application
135
143
  x:(double)x
136
144
  y:(double)y
@@ -49,10 +49,9 @@ extension RunnerTests {
49
49
  return nil
50
50
  }
51
51
 
52
- // The public windows query backing safeSnapshotViewport can fail on the same apps that
53
- // need this fallback, degrading to an infinite viewport that marks off-screen content
54
- // (e.g. closed drawer menus at negative x) as visible and tappable. The private root's
55
- // own frame is the reliable screen rect here.
52
+ // If the app frame is unavailable, the private root's own frame is the reliable screen
53
+ // rect here. Avoid public window queries: stale transient windows can record XCTest
54
+ // failures after the runner already returned a successful command response.
56
55
  var viewport = safeSnapshotViewport(app: app)
57
56
  let rootFrame = privateAXRect(root["frame"])
58
57
  if viewport.isInfinite || viewport.isNull || viewport.isEmpty, !rootFrame.isEmpty {
@@ -13,7 +13,7 @@ extension RunnerTests {
13
13
  return (gestureStartUptimeMs, currentUptimeMs())
14
14
  }
15
15
 
16
- private func synthesizedSwipeFallbackHoldDuration(durationMs: Double) -> TimeInterval {
16
+ func synthesizedSwipeFallbackHoldDuration(durationMs: Double) -> TimeInterval {
17
17
  min(max((durationMs / 5.0) / 1000.0, 0.016), 0.120)
18
18
  }
19
19
 
@@ -1,5 +1,13 @@
1
1
  import XCTest
2
2
 
3
+ private enum RunnerInterfaceOrientation {
4
+ static let unknown = 0
5
+ static let portrait = 1
6
+ static let portraitUpsideDown = 2
7
+ static let landscapeRight = 3
8
+ static let landscapeLeft = 4
9
+ }
10
+
3
11
  extension RunnerTests {
4
12
  struct TouchVisualizationFrame {
5
13
  let x: Double
@@ -634,6 +642,50 @@ extension RunnerTests {
634
642
  return performCoordinateDrag(app: app, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
635
643
  }
636
644
 
645
+ /// Rotates an interface-oriented point into the device-native (portrait) space the
646
+ /// synthesized event path consumes — synthesized events skip XCTest's orientation
647
+ /// handling, so without this a landscape tap lands in the wrong place.
648
+ func nativeSynthesizedPoint(
649
+ orientedX x: Double,
650
+ orientedY y: Double,
651
+ in frame: CGRect,
652
+ interfaceOrientation: Int
653
+ ) -> CGPoint {
654
+ let localX = x - Double(frame.minX)
655
+ let localY = y - Double(frame.minY)
656
+ let width = Double(frame.width)
657
+ let height = Double(frame.height)
658
+ switch interfaceOrientation {
659
+ case RunnerInterfaceOrientation.landscapeRight:
660
+ return CGPoint(x: height - localY, y: localX)
661
+ case RunnerInterfaceOrientation.landscapeLeft:
662
+ return CGPoint(x: localY, y: width - localX)
663
+ case RunnerInterfaceOrientation.portraitUpsideDown:
664
+ return CGPoint(x: width - localX, y: height - localY)
665
+ default: // portrait or unknown
666
+ return CGPoint(x: localX, y: localY)
667
+ }
668
+ }
669
+
670
+ /// Rotates an interface-oriented translation vector into the same native
671
+ /// coordinate space as `nativeSynthesizedPoint`.
672
+ func nativeSynthesizedVector(
673
+ orientedDx dx: Double,
674
+ orientedDy dy: Double,
675
+ interfaceOrientation: Int
676
+ ) -> CGVector {
677
+ switch interfaceOrientation {
678
+ case RunnerInterfaceOrientation.landscapeRight:
679
+ return CGVector(dx: -dy, dy: dx)
680
+ case RunnerInterfaceOrientation.landscapeLeft:
681
+ return CGVector(dx: dy, dy: -dx)
682
+ case RunnerInterfaceOrientation.portraitUpsideDown:
683
+ return CGVector(dx: -dx, dy: -dy)
684
+ default: // portrait or unknown
685
+ return CGVector(dx: dx, dy: dy)
686
+ }
687
+ }
688
+
637
689
  func synthesizedDragAt(
638
690
  app: XCUIApplication,
639
691
  x: Double,
@@ -643,12 +695,16 @@ extension RunnerTests {
643
695
  durationMs: Double
644
696
  ) -> RunnerInteractionOutcome {
645
697
  #if os(iOS)
698
+ let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
699
+ let frame = app.frame
700
+ let start = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: frame, interfaceOrientation: orientation)
701
+ let end = nativeSynthesizedPoint(orientedX: x2, orientedY: y2, in: frame, interfaceOrientation: orientation)
646
702
  if let message = RunnerSynthesizedGesture.synthesizeSwipe(
647
703
  withApplication: app,
648
- x: x,
649
- y: y,
650
- x2: x2,
651
- y2: y2,
704
+ x: Double(start.x),
705
+ y: Double(start.y),
706
+ x2: Double(end.x),
707
+ y2: Double(end.y),
652
708
  durationMs: durationMs
653
709
  ) {
654
710
  return .unsupported(
@@ -672,10 +728,12 @@ extension RunnerTests {
672
728
 
673
729
  func synthesizedTapAt(app: XCUIApplication, x: Double, y: Double) -> RunnerInteractionOutcome {
674
730
  #if os(iOS)
731
+ let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
732
+ let point = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: app.frame, interfaceOrientation: orientation)
675
733
  if let message = RunnerSynthesizedGesture.synthesizeTap(
676
734
  withApplication: app,
677
- x: x,
678
- y: y
735
+ x: Double(point.x),
736
+ y: Double(point.y)
679
737
  ) {
680
738
  return .unsupported(
681
739
  message: message,
@@ -952,12 +1010,15 @@ extension RunnerTests {
952
1010
  ) -> RunnerInteractionOutcome {
953
1011
  #if os(iOS)
954
1012
  let target = interactionRoot(app: app)
1013
+ let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
1014
+ let point = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: app.frame, interfaceOrientation: orientation)
1015
+ let vector = nativeSynthesizedVector(orientedDx: dx, orientedDy: dy, interfaceOrientation: orientation)
955
1016
  if let message = RunnerSynthesizedGesture.synthesizeTransform(
956
1017
  withApplication: app,
957
- x: x,
958
- y: y,
959
- dx: dx,
960
- dy: dy,
1018
+ x: Double(point.x),
1019
+ y: Double(point.y),
1020
+ dx: Double(vector.dx),
1021
+ dy: Double(vector.dy),
961
1022
  scale: scale,
962
1023
  degrees: degrees,
963
1024
  radius: transformGestureRadius(frame: target.frame, scale: scale),
@@ -1091,4 +1152,51 @@ extension RunnerTests {
1091
1152
  let element = app.descendants(matching: .any).matching(predicate).firstMatch
1092
1153
  return element.exists ? element : nil
1093
1154
  }
1155
+
1156
+ // Identity in portrait/unknown, 90° per landscape, 180° upside-down.
1157
+ func testNativeSynthesizedPointRotatesByInterfaceOrientation() {
1158
+ let portrait = CGRect(x: 0, y: 0, width: 834, height: 1210)
1159
+ let landscape = CGRect(x: 0, y: 0, width: 1210, height: 834)
1160
+ let offsetLandscape = CGRect(x: 10, y: 20, width: 1210, height: 834)
1161
+ // (frame, UIInterfaceOrientation, expected native point) for a tap at (170, 268).
1162
+ let cases: [(CGRect, Int, CGPoint)] = [
1163
+ (portrait, RunnerInterfaceOrientation.portrait, CGPoint(x: 170, y: 268)),
1164
+ (landscape, RunnerInterfaceOrientation.landscapeRight, CGPoint(x: 566, y: 170)),
1165
+ (landscape, RunnerInterfaceOrientation.landscapeLeft, CGPoint(x: 268, y: 1040)),
1166
+ (portrait, RunnerInterfaceOrientation.portraitUpsideDown, CGPoint(x: 664, y: 942)),
1167
+ (portrait, RunnerInterfaceOrientation.unknown, CGPoint(x: 170, y: 268)),
1168
+ ]
1169
+ for (frame, orientation, expected) in cases {
1170
+ XCTAssertEqual(
1171
+ nativeSynthesizedPoint(orientedX: 170, orientedY: 268, in: frame, interfaceOrientation: orientation),
1172
+ expected,
1173
+ "interfaceOrientation \(orientation)"
1174
+ )
1175
+ }
1176
+ XCTAssertEqual(
1177
+ nativeSynthesizedPoint(
1178
+ orientedX: 180,
1179
+ orientedY: 288,
1180
+ in: offsetLandscape,
1181
+ interfaceOrientation: RunnerInterfaceOrientation.landscapeLeft
1182
+ ),
1183
+ CGPoint(x: 268, y: 1040),
1184
+ "non-zero frame origin is localized before rotation"
1185
+ )
1186
+ }
1187
+
1188
+ func testNativeSynthesizedVectorRotatesByInterfaceOrientation() {
1189
+ let cases: [(Int, CGVector)] = [
1190
+ (RunnerInterfaceOrientation.portrait, CGVector(dx: 40, dy: -20)),
1191
+ (RunnerInterfaceOrientation.landscapeRight, CGVector(dx: 20, dy: 40)),
1192
+ (RunnerInterfaceOrientation.landscapeLeft, CGVector(dx: -20, dy: -40)),
1193
+ (RunnerInterfaceOrientation.portraitUpsideDown, CGVector(dx: -40, dy: 20)),
1194
+ (RunnerInterfaceOrientation.unknown, CGVector(dx: 40, dy: -20)),
1195
+ ]
1196
+ for (orientation, expected) in cases {
1197
+ let vector = nativeSynthesizedVector(orientedDx: 40, orientedDy: -20, interfaceOrientation: orientation)
1198
+ XCTAssertEqual(vector.dx, expected.dx, "dx interfaceOrientation \(orientation)")
1199
+ XCTAssertEqual(vector.dy, expected.dy, "dy interfaceOrientation \(orientation)")
1200
+ }
1201
+ }
1094
1202
  }
@@ -157,8 +157,8 @@ struct SequenceStep: Codable {
157
157
  let y2: Double?
158
158
  let durationMs: Double?
159
159
  let pauseMs: Double?
160
- /// For `tap` steps on iOS non-tv: use the synthesized HID tap fast path (synthesizedTapAt)
161
- /// instead of the drag-based XCUICoordinate tapAt, matching the individual `tap` command.
160
+ /// For `tap`/`drag` steps on iOS non-tv: use the synthesized HID fast path instead of the
161
+ /// drag-based XCUICoordinate path, matching the individual command behavior.
162
162
  let synthesized: Bool?
163
163
  }
164
164
 
@@ -164,6 +164,50 @@ extension RunnerTests {
164
164
  }
165
165
  // Synthesis unsupported (e.g. macOS) — fall through to the drag-based tapAt below.
166
166
  }
167
+ if step.kind == "drag", step.synthesized == true {
168
+ let dragPoints = keyboardAvoidingDragPoints(
169
+ app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
170
+ let durationMs = min(max(step.durationMs ?? 250, 16), 10000)
171
+ let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
172
+ synthesizedDragAt(
173
+ app: activeApp,
174
+ x: dragPoints.x,
175
+ y: dragPoints.y,
176
+ x2: dragPoints.x2,
177
+ y2: dragPoints.y2,
178
+ durationMs: durationMs
179
+ )
180
+ }
181
+ if case .performed = outcome {
182
+ if let pauseMs = step.pauseMs, pauseMs > 0 {
183
+ sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
184
+ }
185
+ return SequenceStepOutcome(
186
+ outcome: outcome,
187
+ gestureStartUptimeMs: timing.gestureStartUptimeMs,
188
+ gestureEndUptimeMs: timing.gestureEndUptimeMs
189
+ )
190
+ }
191
+ let fallbackHoldDuration = synthesizedSwipeFallbackHoldDuration(durationMs: step.durationMs ?? 250)
192
+ let (fallbackTiming, fallbackOutcome) = performGesture(activeApp) {
193
+ dragAt(
194
+ app: activeApp,
195
+ x: dragPoints.x,
196
+ y: dragPoints.y,
197
+ x2: dragPoints.x2,
198
+ y2: dragPoints.y2,
199
+ holdDuration: fallbackHoldDuration
200
+ )
201
+ }
202
+ if case .performed = fallbackOutcome, let pauseMs = step.pauseMs, pauseMs > 0 {
203
+ sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
204
+ }
205
+ return SequenceStepOutcome(
206
+ outcome: fallbackOutcome,
207
+ gestureStartUptimeMs: fallbackTiming.gestureStartUptimeMs,
208
+ gestureEndUptimeMs: fallbackTiming.gestureEndUptimeMs
209
+ )
210
+ }
167
211
  let (timing, outcome) = performGesture(activeApp) {
168
212
  switch step.kind {
169
213
  case "doubleTap":
@@ -173,9 +217,9 @@ extension RunnerTests {
173
217
  let duration = min(max(step.durationMs ?? 800, 16), 10000) / 1000.0
174
218
  return longPressAt(app: activeApp, x: x, y: y, duration: duration)
175
219
  case "drag":
176
- // Route through keyboardAvoidingDragPoints for parity with the individual `.drag` command
177
- // (RunnerTests+CommandExecution.swift). durationMs is intentionally ignored on this
178
- // coordinate-drag path, matching that command's non-synthesized branch.
220
+ // Route through keyboardAvoidingDragPoints for parity with the individual `.drag` command.
221
+ // The non-synthesized coordinate-drag path ignores durationMs, matching that command's
222
+ // non-synthesized branch.
179
223
  let dragPoints = keyboardAvoidingDragPoints(
180
224
  app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
181
225
  return dragAt(
@@ -739,15 +739,6 @@ extension RunnerTests {
739
739
  }
740
740
 
741
741
  private func snapshotViewport(app: XCUIApplication) -> CGRect {
742
- let windows = app.windows.allElementsBoundByIndex
743
- let windowFrames = windows
744
- .filter { $0.exists && !$0.frame.isNull && !$0.frame.isEmpty }
745
- .map(\.frame)
746
- if let largestWindowFrame = windowFrames.max(by: { left, right in
747
- left.width * left.height < right.width * right.height
748
- }) {
749
- return largestWindowFrame
750
- }
751
742
  let appFrame = app.frame
752
743
  if !appFrame.isNull && !appFrame.isEmpty {
753
744
  return appFrame
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.17.3",
3
+ "version": "0.17.5",
4
4
  "description": "Agent-native CLI for AI mobile testing and app automation across iOS, Android, tvOS, Android TV, macOS, and Linux.",
5
5
  "mcpName": "io.github.callstackincubator/agent-device",
6
6
  "license": "MIT",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/callstackincubator/agent-device",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.17.3",
10
+ "version": "0.17.5",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "agent-device",
15
- "version": "0.17.3",
15
+ "version": "0.17.5",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -1 +0,0 @@
1
- 6989d98eafa3826bd53b5d36268b5c96f2c9404f87478d0b541139c25783c26a agent-device-android-multitouch-helper-0.17.3.apk
@@ -1 +0,0 @@
1
- 522875a8a672915e05710a4b15fb51162de206824f4a19a43028d184b337a76d agent-device-android-snapshot-helper-0.17.3.apk
package/dist/src/9238.js DELETED
@@ -1,4 +0,0 @@
1
- import e from"node:crypto";import t from"node:fs";import n from"node:path";import{resolveUserPath as r,expandUserHomePath as o}from"./3267.js";import{findProjectRoot as s}from"./9671.js";function i(l,a={}){var u,c;let f,p=(u=l,c=a,(f=(u??"").trim())?r(f,{env:c.env}):function(r={}){var i;let l,a,u,c=n.join(o("~",{env:r.env}),".agent-device"),f=r.projectRoot??s();return(i=f,t.existsSync(n.join(i,"package.json"))&&t.existsSync(n.join(i,"src","daemon.ts")))?n.join(c,"dev",(l=function(e){try{return t.realpathSync.native(e)}catch{return n.resolve(e)}}(f),a=n.basename(l).replaceAll(/[^a-zA-Z0-9._-]+/g,"-"),u=e.createHash("sha1").update(l).digest("hex").slice(0,12),`${a||"agent-device"}-${u}`)):c}(c));return{baseDir:p,infoPath:n.join(p,"daemon.json"),lockPath:n.join(p,"daemon.lock"),logPath:n.join(p,"daemon.log"),sessionsDir:n.join(p,"sessions")}}function l(e){let t=(e??"").trim().toLowerCase();return"http"===t?"http":"dual"===t?"dual":"socket"}function a(e){let t=(e??"").trim().toLowerCase();return"auto"===t?"auto":"socket"===t?"socket":"http"===t?"http":"auto"}function u(e){return"tenant"===(e??"").trim().toLowerCase()?"tenant":"none"}function c(e){if(!e)return;let t=e.trim();if(t&&/^[a-zA-Z0-9._-]{1,128}$/.test(t))return t}let f=/(?:^|[^\w$.])(?:import|export)\s+(?:type\s+)?(?:[^'"`]*?\s+from\s+)?['"]([^'"]+)['"]/gm,p=/import\(\s*['"]([^'"]+)['"]\s*\)/gm,m=[".ts",".tsx",".js",".jsx",".mjs",".cjs"];function d(){let e=process.argv[1];return e?h(e):"unknown"}function h(r,o=s()){try{let s=n.resolve(o),i=[n.resolve(r)],l=new Set,a=[];for(;i.length>0;){let e=i.pop();if(!e||l.has(e))continue;l.add(e);let r=t.statSync(e);if(!r.isFile())continue;let o=n.relative(s,e)||e;a.push(`${o}:${r.size}:${Math.trunc(r.mtimeMs)}`);let u=t.readFileSync(e,"utf8");for(let t of function(e){let t=new Set;return v(e,f,t),v(e,p,t),[...t]}(u)){let r=function(e,t){let r=n.resolve(n.dirname(e),t),o=g(r);if(o)return o;for(let e of m){let t=g(`${r}${e}`);if(t)return t}for(let e of m){let t=g(n.join(r,`index${e}`));if(t)return t}return null}(e,t);r&&i.push(r)}}let u=a.sort().join("|"),c=e.createHash("sha1").update(u).digest("hex");return`graph:${a.length}:${c}`}catch{return"unknown"}}function v(e,t,n){t.lastIndex=0;let r=null;for(;null!==(r=t.exec(e));){let e=r[1]?.trim();e?.startsWith(".")&&n.add(e)}}function g(e){try{return t.statSync(e).isFile()?e:null}catch{return null}}let y=/^[A-Za-z0-9_@%+=:,./-]+$/;function S(e){return`'${e.replaceAll("'","'\\''")}'`}function j(e){return y.test(e)?e:S(e)}function $(e,t){let n=[],r=e+t.toString(),o=r.indexOf("\n");for(;-1!==o;){let e=r.slice(0,o).trim();r=r.slice(o+1),e&&n.push(e),o=r.indexOf("\n")}return{lines:n,buffer:r}}function x(e){return e.meta?.requestProgress==="replay-test"}function _(e){return!!e&&"object"==typeof e&&"progress"===e.type&&!!e.event}function D(e){return!!e&&"object"==typeof e&&"response"===e.type&&!!e.response}function P(e){return`${JSON.stringify({type:"progress",event:e})}
2
- `}function C(e){return`${JSON.stringify({type:"response",response:e})}
3
- `}function k(e){return`${JSON.stringify({type:"response",response:e})}
4
- `}export{h as computeDaemonCodeSignature,$ as consumeTextLines,_ as isDaemonProgressEnvelope,D as isDaemonResponseEnvelope,c as normalizeTenantId,d as resolveDaemonCodeSignature,i as resolveDaemonPaths,l as resolveDaemonServerMode,a as resolveDaemonTransportPreference,u as resolveSessionIsolationMode,P as serializeDaemonProgressEnvelope,C as serializeDaemonResponseEnvelope,k as serializeDaemonRpcResponseEnvelope,S as shellQuote,j as shellQuoteIfNeeded,x as shouldStreamRequestProgress};