agent-device 0.17.7 → 0.17.9

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 (56) hide show
  1. package/README.md +14 -0
  2. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.7.apk → agent-device-android-multitouch-helper-0.17.9.apk} +0 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.9.apk.sha256 +1 -0
  4. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.7.manifest.json → agent-device-android-multitouch-helper-0.17.9.manifest.json} +4 -4
  5. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.7.apk → agent-device-android-snapshot-helper-0.17.9.apk} +0 -0
  6. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.9.apk.sha256 +1 -0
  7. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.7.manifest.json → agent-device-android-snapshot-helper-0.17.9.manifest.json} +6 -6
  8. package/dist/src/1404.js +1 -0
  9. package/dist/src/2133.js +1 -1
  10. package/dist/src/2577.js +1 -0
  11. package/dist/src/2948.js +30 -1
  12. package/dist/src/4428.js +1 -1
  13. package/dist/src/587.js +1 -1
  14. package/dist/src/7394.js +5 -5
  15. package/dist/src/8184.js +1 -1
  16. package/dist/src/8358.js +4 -0
  17. package/dist/src/9581.js +13 -2
  18. package/dist/src/9722.js +36 -36
  19. package/dist/src/9836.js +1 -1
  20. package/dist/src/9919.js +5 -5
  21. package/dist/src/android.js +3 -3
  22. package/dist/src/apple.js +1 -1
  23. package/dist/src/apps.js +1 -1
  24. package/dist/src/cli-help.js +25 -7
  25. package/dist/src/cli-output.js +1 -4
  26. package/dist/src/cli.js +28 -18
  27. package/dist/src/command-surface.js +1 -1
  28. package/dist/src/find.js +1 -1
  29. package/dist/src/generic.js +5 -5
  30. package/dist/src/index.d.ts +12 -0
  31. package/dist/src/input-actions.js +1 -1
  32. package/dist/src/input-actions~1.js +1 -1
  33. package/dist/src/interaction.js +1 -1
  34. package/dist/src/internal/bin.js +1 -1
  35. package/dist/src/react-native.js +1 -1
  36. package/dist/src/record-trace-recording.js +1 -1
  37. package/dist/src/selector-runtime.js +1 -1
  38. package/dist/src/selectors.js +1 -1
  39. package/dist/src/server.js +1 -1
  40. package/dist/src/session.js +7 -7
  41. package/dist/src/snapshot.js +1 -1
  42. package/dist/src/web.js +1 -1
  43. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +92 -4
  44. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +1 -1
  45. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +130 -0
  46. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +4 -2
  47. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +18 -0
  48. package/package.json +3 -3
  49. package/server.json +2 -2
  50. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.7.apk.sha256 +0 -1
  51. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.7.apk.sha256 +0 -1
  52. package/dist/src/2892.js +0 -4
  53. package/dist/src/368.js +0 -1
  54. package/dist/src/6311.js +0 -27
  55. package/dist/src/7244.js +0 -1
  56. package/dist/src/9873.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"./4291.js";import{normalizeError as o,AppError as r}from"./733.js";import{withDiagnosticTimer as i,emitDiagnostic as l}from"./432.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"./9722.js";import{SETTINGS_INVALID_ARGS_MESSAGE as b,parseTimeout as I,getUnsupportedMacOsSettingMessage as A,successText as R,isMacOsSettingSupported as C,snapshotCaptureAnnotationsFrom as N}from"./9873.js";import{sleep as D}from"./5560.js";import{buildSnapshotSession as P,dispatchWaitViaRuntime as k,createDaemonRuntimePolicy as M,recordIfSession as E,resolveSessionDevice as O,withSessionlessRunnerCleanup as K}from"./selector-runtime.js";import{createAgentDevice as U}from"./368.js";import{summarizeSnapshotDiagnostics as x}from"./2892.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,F=/(?:^|:)id\/permission_/i,T=/\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,X=/^(?: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 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 D(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)||F.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)&&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&&$.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=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||(j.test(t.trim())?"accept":X.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 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 Q(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,iosXctestrunFile:a.flags?.iosXctestrunFile,iosXctestDerivedDataPath:a.flags?.iosXctestDerivedDataPath,iosXctestEnvDir:a.flags?.iosXctestEnvDir},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 D(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 D(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}
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"./4291.js";import{normalizeError as o,AppError as r}from"./733.js";import{withDiagnosticTimer as i,emitDiagnostic as l}from"./432.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"./9722.js";import{SETTINGS_INVALID_ARGS_MESSAGE as b,parseTimeout as I,getUnsupportedMacOsSettingMessage as A,successText as R,isMacOsSettingSupported as C,snapshotCaptureAnnotationsFrom as N}from"./2948.js";import{sleep as D}from"./5560.js";import{buildSnapshotSession as P,dispatchWaitViaRuntime as k,createDaemonRuntimePolicy as M,recordIfSession as E,resolveSessionDevice as O,withSessionlessRunnerCleanup as K}from"./selector-runtime.js";import{createAgentDevice as U}from"./2577.js";import{summarizeSnapshotDiagnostics as x}from"./8358.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,F=/(?:^|:)id\/permission_/i,T=/\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,X=/^(?: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 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 D(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)||F.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)&&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&&$.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=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||(j.test(t.trim())?"accept":X.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 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 Q(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,iosXctestrunFile:a.flags?.iosXctestrunFile,iosXctestDerivedDataPath:a.flags?.iosXctestDerivedDataPath,iosXctestEnvDir:a.flags?.iosXctestEnvDir},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 D(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 D(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
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}),...M("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=P({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 k({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};
package/dist/src/web.js CHANGED
@@ -1 +1 @@
1
- import{AppError as e}from"./733.js";import{withDiagnosticTimer as t}from"./432.js";import{resolveWebProvider as r}from"./provider.js";function o(){return{open:(e,t)=>r().open(t?.url??e,{url:t?.url}),openDevice:()=>r().open("about:blank"),close:e=>r().close(e),tap:(e,t)=>r().click(e,t),doubleTap:()=>n("doubleTap"),swipe:()=>n("swipe"),pan:()=>n("pan"),fling:()=>n("fling"),longPress:()=>n("longPress"),focus:(e,t)=>r().click(e,t),type:(e,t)=>r().typeText(e,{delayMs:t}),fill:(e,t,o,n)=>r().fill(e,t,o,{delayMs:n}),scroll:(e,t)=>r().scroll(e,t),pinch:()=>n("pinch"),screenshot:(e,t)=>r().screenshot(e,t),snapshot:async e=>{let o=await t("snapshot_capture",async()=>await r().snapshot(e),{backend:"web"});return{nodes:o.nodes,truncated:o.truncated??!1,backend:"web"}},back:()=>n("back"),home:()=>n("home"),rotate:()=>n("rotate"),rotateGesture:()=>n("rotateGesture"),transformGesture:()=>n("transformGesture"),appSwitcher:()=>n("appSwitcher"),readClipboard:()=>n("readClipboard"),writeClipboard:()=>n("writeClipboard"),setSetting:()=>n("setSetting")}}async function n(t){throw new e("UNSUPPORTED_OPERATION",`${t} is not supported on web`)}export{o as createWebInteractor};
1
+ import{AppError as e}from"./733.js";import{withDiagnosticTimer as t}from"./432.js";import{resolveWebProvider as r}from"./provider.js";function o(){return{open:(e,t)=>r().open(t?.url??e,{url:t?.url}),openDevice:()=>r().open("about:blank"),close:e=>r().close(e),tap:(e,t)=>r().click(e,t),doubleTap:()=>n("doubleTap"),swipe:()=>n("swipe"),pan:()=>n("pan"),fling:()=>n("fling"),longPress:()=>n("longPress"),focus:(e,t)=>r().click(e,t),type:(e,t)=>r().typeText(e,{delayMs:t}),fill:(e,t,o,n)=>r().fill(e,t,o,{delayMs:n}),scroll:(e,t)=>r().scroll(e,t),pinch:()=>n("pinch"),screenshot:(e,t)=>r().screenshot(e,t),setViewport:(e,t)=>r().setViewport(e,t),snapshot:async e=>{let o=await t("snapshot_capture",async()=>await r().snapshot(e),{backend:"web"});return{nodes:o.nodes,truncated:o.truncated??!1,backend:"web"}},back:()=>n("back"),home:()=>n("home"),rotate:()=>n("rotate"),rotateGesture:()=>n("rotateGesture"),transformGesture:()=>n("transformGesture"),appSwitcher:()=>n("appSwitcher"),readClipboard:()=>n("readClipboard"),writeClipboard:()=>n("writeClipboard"),setSetting:()=>n("setSetting")}}async function n(t){throw new e("UNSUPPORTED_OPERATION",`${t} is not supported on web`)}export{o as createWebInteractor};
@@ -625,16 +625,105 @@ extension RunnerTests {
625
625
  )
626
626
  )
627
627
  }
628
+ if let durationMs = command.durationMs,
629
+ durationMs.isFinite == false || durationMs < 0 || durationMs > 10000
630
+ {
631
+ return Response(
632
+ ok: false,
633
+ error: ErrorPayload(
634
+ code: "INVALID_ARGS",
635
+ message: "scroll durationMs must be between 0 and 10000"
636
+ )
637
+ )
638
+ }
628
639
  return executeDragGesture(
629
640
  activeApp: activeApp,
630
641
  x: frame.minX + plan.x1,
631
642
  y: frame.minY + plan.y1,
632
643
  x2: frame.minX + plan.x2,
633
644
  y2: frame.minY + plan.y2,
634
- durationMs: nil,
635
- synthesized: false,
645
+ durationMs: command.durationMs,
646
+ synthesized: command.durationMs != nil,
636
647
  message: "scrolled"
637
648
  )
649
+ case .desktopScroll:
650
+ guard let direction = command.direction,
651
+ direction == "up" || direction == "down" || direction == "left" || direction == "right"
652
+ else {
653
+ return Response(
654
+ ok: false,
655
+ error: ErrorPayload(
656
+ code: "INVALID_ARGS",
657
+ message: "desktopScroll requires direction up|down|left|right"
658
+ )
659
+ )
660
+ }
661
+ let appFrame = activeApp.frame
662
+ let frame = resolvedTouchReferenceFrame(app: activeApp, appFrame: appFrame)
663
+ guard frame.width > 0, frame.height > 0 else {
664
+ return Response(
665
+ ok: false,
666
+ error: ErrorPayload(message: "desktopScroll could not resolve a usable interaction frame")
667
+ )
668
+ }
669
+ guard let plan = runnerScrollGesturePlan(
670
+ direction: direction,
671
+ amount: command.amount,
672
+ pixels: command.pixels,
673
+ referenceWidth: frame.width,
674
+ referenceHeight: frame.height
675
+ ) else {
676
+ return Response(
677
+ ok: false,
678
+ error: ErrorPayload(
679
+ code: "INVALID_ARGS",
680
+ message: "desktopScroll could not compute a wheel plan"
681
+ )
682
+ )
683
+ }
684
+ let x = frame.midX
685
+ let y = frame.midY
686
+ let localX = x - (appFrame.isEmpty ? frame.minX : appFrame.minX)
687
+ let localY = y - (appFrame.isEmpty ? frame.minY : appFrame.minY)
688
+ if let durationMs = command.durationMs,
689
+ durationMs.isFinite == false || durationMs < 0 || durationMs > 10000
690
+ {
691
+ return Response(
692
+ ok: false,
693
+ error: ErrorPayload(
694
+ code: "INVALID_ARGS",
695
+ message: "desktopScroll durationMs must be between 0 and 10000"
696
+ )
697
+ )
698
+ }
699
+ let touchFrame = resolvedTouchVisualizationFrame(
700
+ app: activeApp,
701
+ x: localX,
702
+ y: localY
703
+ )
704
+ do {
705
+ var scrollError: Error?
706
+ let timing = measureGesture {
707
+ do {
708
+ try desktopScrollAt(
709
+ app: activeApp,
710
+ x: x,
711
+ y: y,
712
+ direction: direction,
713
+ pixels: plan.travelPixels,
714
+ durationMs: command.durationMs
715
+ )
716
+ } catch {
717
+ scrollError = error
718
+ }
719
+ }
720
+ if let scrollError {
721
+ throw scrollError
722
+ }
723
+ return gestureResponse(message: "scrolled", timing: timing, frame: .touch(touchFrame))
724
+ } catch {
725
+ return Response(ok: false, error: ErrorPayload(message: error.localizedDescription))
726
+ }
638
727
  case .remotePress:
639
728
  guard let button = tvRemoteButton(from: command.remoteButton) else {
640
729
  return Response(ok: false, error: ErrorPayload(message: "remotePress requires remoteButton"))
@@ -910,8 +999,7 @@ extension RunnerTests {
910
999
  /// Shared drag execution for `.drag` and the fused `.scroll`. Mirrors the original `.drag` body
911
1000
  /// exactly: keyboardAvoidingDragPoints -> resolvedDragVisualizationFrame -> synthesized branch
912
1001
  /// (16-10000ms clamp) or non-synthesized dragAt with coordinateDragHoldDuration ->
913
- /// gestureResponse(.drag). `.scroll` always passes synthesized: false, pinning the same
914
- /// non-synthesized drag path scroll's drag used today.
1002
+ /// gestureResponse(.drag). `.scroll` uses the synthesized path only when a duration is requested.
915
1003
  private func executeDragGesture(
916
1004
  activeApp: XCUIApplication,
917
1005
  x: Double,
@@ -144,7 +144,7 @@ final class RunnerCommandJournal {
144
144
  case .snapshot, .screenshot:
145
145
  return false
146
146
  case .tap, .mouseClick, .longPress, .drag,
147
- .remotePress, .type, .swipe, .scroll, .findText, .querySelector, .readText, .back,
147
+ .remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .back,
148
148
  .backInApp, .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn,
149
149
  .alert, .pinch, .sequence, .rotateGesture, .transformGesture, .recordStart, .recordStop,
150
150
  .status, .uptime, .shutdown:
@@ -1,5 +1,9 @@
1
1
  import XCTest
2
2
 
3
+ #if os(macOS)
4
+ import CoreGraphics
5
+ #endif
6
+
3
7
  private enum RunnerInterfaceOrientation {
4
8
  static let unknown = 0
5
9
  static let portrait = 1
@@ -605,6 +609,110 @@ extension RunnerTests {
605
609
  #endif
606
610
  }
607
611
 
612
+ func desktopScrollAt(
613
+ app: XCUIApplication,
614
+ x: Double,
615
+ y: Double,
616
+ direction: String,
617
+ pixels: Double,
618
+ durationMs: Double?
619
+ ) throws {
620
+ #if os(macOS)
621
+ guard let events = desktopScrollWheelDeltaEvents(
622
+ direction: direction,
623
+ pixels: pixels,
624
+ durationMs: durationMs
625
+ ) else {
626
+ throw NSError(
627
+ domain: "AgentDeviceRunner",
628
+ code: 1,
629
+ userInfo: [NSLocalizedDescriptionKey: "unsupported desktop scroll direction: \(direction)"]
630
+ )
631
+ }
632
+
633
+ let coordinate = interactionCoordinate(app: app, x: x, y: y)
634
+ let interval = desktopScrollEventIntervalSeconds(durationMs: durationMs, eventCount: events.count)
635
+ for (index, deltas) in events.enumerated() {
636
+ // Keep desktop scrolling on XCTest's coordinate API so macOS owns wheel synthesis, natural
637
+ // scrolling preference handling, and cursor placement instead of posting raw CGEvents.
638
+ coordinate.scroll(
639
+ byDeltaX: CGFloat(deltas.horizontal),
640
+ deltaY: CGFloat(deltas.vertical)
641
+ )
642
+ if interval > 0 && index < events.count - 1 {
643
+ Thread.sleep(forTimeInterval: interval)
644
+ }
645
+ }
646
+ #elseif os(tvOS)
647
+ throw NSError(
648
+ domain: "AgentDeviceRunner",
649
+ code: 1,
650
+ userInfo: [NSLocalizedDescriptionKey: "desktopScroll is not supported on tvOS"]
651
+ )
652
+ #else
653
+ throw NSError(
654
+ domain: "AgentDeviceRunner",
655
+ code: 1,
656
+ userInfo: [NSLocalizedDescriptionKey: "desktopScroll is only supported on macOS"]
657
+ )
658
+ #endif
659
+ }
660
+
661
+ func desktopScrollWheelDeltas(direction: String, pixels: Double) -> (vertical: Int32, horizontal: Int32)? {
662
+ let magnitude = Int32(max(1, min(Double(Int32.max), pixels.rounded())))
663
+ switch direction {
664
+ case "up":
665
+ return (vertical: magnitude, horizontal: 0)
666
+ case "down":
667
+ return (vertical: -magnitude, horizontal: 0)
668
+ case "left":
669
+ return (vertical: 0, horizontal: magnitude)
670
+ case "right":
671
+ return (vertical: 0, horizontal: -magnitude)
672
+ default:
673
+ return nil
674
+ }
675
+ }
676
+
677
+ func desktopScrollWheelDeltaEvents(
678
+ direction: String,
679
+ pixels: Double,
680
+ durationMs: Double?
681
+ ) -> [(vertical: Int32, horizontal: Int32)]? {
682
+ guard let totalDeltas = desktopScrollWheelDeltas(direction: direction, pixels: pixels) else {
683
+ return nil
684
+ }
685
+ let magnitude = max(abs(Int(totalDeltas.vertical)), abs(Int(totalDeltas.horizontal)))
686
+ let duration = max(0, durationMs ?? 0)
687
+ let requestedEventCount = duration > 0 ? Int(ceil(duration / 16.0)) : 1
688
+ let eventCount = max(1, min(magnitude, requestedEventCount))
689
+ guard eventCount > 1 else {
690
+ return [totalDeltas]
691
+ }
692
+
693
+ if totalDeltas.vertical != 0 {
694
+ return distributeDesktopScrollDelta(totalDeltas.vertical, eventCount: eventCount)
695
+ .map { (vertical: $0, horizontal: 0) }
696
+ }
697
+ return distributeDesktopScrollDelta(totalDeltas.horizontal, eventCount: eventCount)
698
+ .map { (vertical: 0, horizontal: $0) }
699
+ }
700
+
701
+ func desktopScrollEventIntervalSeconds(durationMs: Double?, eventCount: Int) -> TimeInterval {
702
+ guard let durationMs, durationMs > 0, eventCount > 1 else { return 0 }
703
+ return (durationMs / 1000.0) / Double(eventCount - 1)
704
+ }
705
+
706
+ private func distributeDesktopScrollDelta(_ delta: Int32, eventCount: Int) -> [Int32] {
707
+ let sign: Int32 = delta < 0 ? -1 : 1
708
+ let magnitude = abs(Int(delta))
709
+ let base = magnitude / eventCount
710
+ let remainder = magnitude % eventCount
711
+ return (0..<eventCount).map { index in
712
+ sign * Int32(base + (index < remainder ? 1 : 0))
713
+ }
714
+ }
715
+
608
716
  func doubleTapAt(app: XCUIApplication, x: Double, y: Double) -> RunnerInteractionOutcome {
609
717
  if let outcome = selectFocusedTvElement(app: app, point: CGPoint(x: x, y: y), action: "double tap") {
610
718
  guard case .performed = outcome else { return outcome }
@@ -1199,4 +1307,26 @@ extension RunnerTests {
1199
1307
  XCTAssertEqual(vector.dy, expected.dy, "dy interfaceOrientation \(orientation)")
1200
1308
  }
1201
1309
  }
1310
+
1311
+ func testDesktopScrollWheelDeltasMapDirections() throws {
1312
+ XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "up", pixels: 120)).vertical, 120)
1313
+ XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "down", pixels: 120)).vertical, -120)
1314
+ XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "left", pixels: 120)).horizontal, 120)
1315
+ XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "right", pixels: 120)).horizontal, -120)
1316
+ XCTAssertNil(desktopScrollWheelDeltas(direction: "diagonal", pixels: 120))
1317
+ }
1318
+
1319
+ func testDesktopScrollWheelDeltaEventsHonorDurationAndPreservePixels() throws {
1320
+ let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 50))
1321
+ XCTAssertEqual(events.count, 4)
1322
+ XCTAssertEqual(events.map(\.vertical).reduce(0, +), -200)
1323
+ XCTAssertEqual(events.map(\.horizontal).reduce(0, +), 0)
1324
+ XCTAssertEqual(desktopScrollEventIntervalSeconds(durationMs: 50, eventCount: events.count), 0.05 / 3.0)
1325
+ }
1326
+
1327
+ func testDesktopScrollWheelDeltaEventsKeepInstantScrollSingleEvent() throws {
1328
+ let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 0))
1329
+ XCTAssertEqual(events.count, 1)
1330
+ XCTAssertEqual(events.first?.vertical, -200)
1331
+ }
1202
1332
  }
@@ -9,6 +9,7 @@ enum CommandType: String, Codable {
9
9
  case type
10
10
  case swipe
11
11
  case scroll
12
+ case desktopScroll
12
13
  case findText
13
14
  case querySelector
14
15
  case readText
@@ -69,8 +70,9 @@ extension CommandType {
69
70
  // Interaction commands: require the foreground-guard + stabilization preflight.
70
71
  // keyboardReturn is the sibling of keyboardDismiss (missing from the historical switch —
71
72
  // drift the table now prevents). .scroll is the fused frame-resolve + drag scroll; same
72
- // classification as .drag. .sequence is the fused multi-step gesture batch.
73
- case .tap, .longPress, .drag, .remotePress, .type, .swipe, .scroll,
73
+ // classification as .drag. .desktopScroll is the macOS frame-resolve + wheel event sibling.
74
+ // .sequence is the fused multi-step gesture batch.
75
+ case .tap, .longPress, .drag, .remotePress, .type, .swipe, .scroll, .desktopScroll,
74
76
  .back, .backInApp, .backSystem, .rotate, .appSwitcher,
75
77
  .keyboardDismiss, .keyboardReturn, .pinch, .sequence, .rotateGesture, .transformGesture:
76
78
  return CommandTraits(isInteraction: true, readOnly: .never, isLifecycle: false)
@@ -119,6 +119,24 @@ extension RunnerTests {
119
119
  XCTAssertEqual(plan.travelPixels, 720)
120
120
  }
121
121
 
122
+ func testRunnerScrollGesturePlanClampsExplicitPixelsVertically() throws {
123
+ // 400x800, down, pixels 1000 clamps travel to the safe band (720): (200,760)->(200,40).
124
+ let plan = try XCTUnwrap(
125
+ runnerScrollGesturePlan(
126
+ direction: "down",
127
+ amount: nil,
128
+ pixels: 1000,
129
+ referenceWidth: 400,
130
+ referenceHeight: 800
131
+ )
132
+ )
133
+ XCTAssertEqual(plan.x1, 200)
134
+ XCTAssertEqual(plan.y1, 760)
135
+ XCTAssertEqual(plan.x2, 200)
136
+ XCTAssertEqual(plan.y2, 40)
137
+ XCTAssertEqual(plan.travelPixels, 720)
138
+ }
139
+
122
140
  func testRunnerScrollGesturePlanFloorsTinyFrames() throws {
123
141
  // 2x2, down, pixels 10 engages every max(1, ...) floor and the .5 rounding cases the two
124
142
  // ports must agree on (halfTravel 0.5 -> 1, center 1 from 2/2): (1,2)->(1,0), travel 1.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.17.7",
3
+ "version": "0.17.9",
4
4
  "description": "Agent-native CLI for AI app automation across iOS, Android, tvOS, Android TV, macOS, Linux, and web.",
5
5
  "mcpName": "io.github.callstackincubator/agent-device",
6
6
  "license": "MIT",
@@ -103,8 +103,8 @@
103
103
  "perf:ios": "node --experimental-strip-types scripts/perf/run.ts --platform ios",
104
104
  "perf:android": "node --experimental-strip-types scripts/perf/run.ts --platform android",
105
105
  "lint": "oxlint . --deny-warnings",
106
- "format": "oxfmt --write src test skills package.json tsconfig.json tsconfig.lib.json rslib.config.ts vitest.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
107
- "format:check": "oxfmt --check src test skills package.json tsconfig.json tsconfig.lib.json rslib.config.ts vitest.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
106
+ "format": "node ./node_modules/oxfmt/bin/oxfmt --write src test skills package.json tsconfig.json tsconfig.lib.json rslib.config.ts vitest.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
107
+ "format:check": "node ./node_modules/oxfmt/bin/oxfmt --check src test skills package.json tsconfig.json tsconfig.lib.json rslib.config.ts vitest.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
108
108
  "fallow": "fallow audit --base origin/main",
109
109
  "fallow:all": "fallow --summary",
110
110
  "fallow:baseline": "(fallow dead-code --save-baseline fallow-baselines/dead-code.json --summary || true) && (fallow health --save-baseline fallow-baselines/health.json --summary || true)",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/callstack/agent-device",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.17.7",
10
+ "version": "0.17.9",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "agent-device",
15
- "version": "0.17.7",
15
+ "version": "0.17.9",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -1 +0,0 @@
1
- cc41db9a5e5f7df8bdf302c3d4e6130ed7b35f1012bd37d2db583b70a0104ee8 agent-device-android-multitouch-helper-0.17.7.apk
@@ -1 +0,0 @@
1
- a8d1788b652e75d6542891763afee0213d38d02825f453a1b5d0edfca6ad8cd4 agent-device-android-snapshot-helper-0.17.7.apk
package/dist/src/2892.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"./9612.js";import{findProjectRoot as s}from"./7244.js";function i(a,u={}){var l,c;let p,f=(l=a,c=u,(p=(l??"").trim())?r(p,{env:c.env}):function(r={}){var i;let a,u,l,c=n.join(o("~",{env:r.env}),".agent-device"),p=r.projectRoot??s();return(i=p,t.existsSync(n.join(i,"package.json"))&&t.existsSync(n.join(i,"src","daemon.ts")))?n.join(c,"dev",(a=function(e){try{return t.realpathSync.native(e)}catch{return n.resolve(e)}}(p),u=n.basename(a).replaceAll(/[^a-zA-Z0-9._-]+/g,"-"),l=e.createHash("sha1").update(a).digest("hex").slice(0,12),`${u||"agent-device"}-${l}`)):c}(c));return{baseDir:f,infoPath:n.join(f,"daemon.json"),lockPath:n.join(f,"daemon.lock"),logPath:n.join(f,"daemon.log"),sessionsDir:n.join(f,"sessions")}}function a(e){let t=(e??"").trim().toLowerCase();return"http"===t?"http":"dual"===t?"dual":"socket"}function u(e){let t=(e??"").trim().toLowerCase();return"auto"===t?"auto":"socket"===t?"socket":"http"===t?"http":"auto"}function l(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 p=/(?:^|[^\w$.])(?:import|export)\s+(?:type\s+)?(?:[^'"`]*?\s+from\s+)?['"]([^'"]+)['"]/gm,f=/import\(\s*['"]([^'"]+)['"]\s*\)/gm,m=[".ts",".tsx",".js",".jsx",".mjs",".cjs"];function h(){let e=process.argv[1];return e?d(e):"unknown"}function d(r,o=s()){try{let s=n.resolve(o),i=[n.resolve(r)],a=new Set,u=[];for(;i.length>0;){let e=i.pop();if(!e||a.has(e))continue;a.add(e);let r=t.statSync(e);if(!r.isFile())continue;let o=n.relative(s,e)||e;u.push(`${o}:${r.size}:${Math.trunc(r.mtimeMs)}`);let l=t.readFileSync(e,"utf8");for(let t of function(e){let t=new Set;return g(e,p,t),g(e,f,t),[...t]}(l)){let r=function(e,t){let r=n.resolve(n.dirname(e),t),o=M(r);if(o)return o;for(let e of m){let t=M(`${r}${e}`);if(t)return t}for(let e of m){let t=M(n.join(r,`index${e}`));if(t)return t}return null}(e,t);r&&i.push(r)}}let l=u.sort().join("|"),c=e.createHash("sha1").update(l).digest("hex");return`graph:${u.length}:${c}`}catch{return"unknown"}}function g(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 M(e){try{return t.statSync(e).isFile()?e:null}catch{return null}}let v=/^[A-Za-z0-9_@%+=:,./-]+$/;function y(e){return`'${e.replaceAll("'","'\\''")}'`}function S(e){return v.test(e)?e:y(e)}function j(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 b(e){return!!e&&"object"==typeof e&&"response"===e.type&&!!e.response}function w(e){return`${JSON.stringify({type:"progress",event:e})}
2
- `}function D(e){return`${JSON.stringify({type:"response",response:e})}
3
- `}function k(e){return`${JSON.stringify({type:"response",response:e})}
4
- `}function _(e,t){e&&(e.snapshotDiagnostics??={samples:[]}).samples.push({...t,durationMs:Math.max(0,Math.round(t.durationMs))})}function A(e){let t=e?.snapshotDiagnostics?.samples;if(t&&0!==t.length)return P(t)}function P(e){if(0===e.length)return;let t=O(e);return{stats:t,...t.p95Ms>=1500?{warning:R(t)}:{}}}function z(e){let t=e.flatMap(e=>(function(e){if(!e||e.count<=0)return[];let t=e.platform;return 1===e.count?[{durationMs:e.maxMs,platform:t}]:2===e.count?[{durationMs:e.p50Ms,platform:t},{durationMs:e.maxMs,platform:t}]:[...Array.from({length:e.count-3},()=>({durationMs:e.p50Ms,platform:t})),{durationMs:e.p50Ms,platform:t},{durationMs:e.p95Ms,platform:t},{durationMs:e.maxMs,platform:t}]})(e?.stats));if(0===t.length)return;let n=O(t);return{stats:n,...n.p95Ms>=1500?{warning:R(n)}:{}}}function C(e){if(!e||"object"!=typeof e||Array.isArray(e))return;let t=function(e){let t=T(e);if(!t)return;let n=function(e){let t={count:e.count,p50Ms:e.p50Ms,p95Ms:e.p95Ms,maxMs:e.maxMs,slowThresholdMs:e.slowThresholdMs};if(!Object.values(t).some(e=>"number"!=typeof e))return t}(t);if(n){var r;let e,o,s,i;return{...n,...(o="string"==typeof(r=t).platform?r.platform:void 0,i=(s=T(r.backends))&&(e=Object.entries(s).filter(e=>"number"==typeof e[1])).length>0?Object.fromEntries(e):void 0,{...o?{platform:o}:{},...i?{backends:i}:{}})}}}(e.stats);if(!t)return;let n="string"==typeof e.warning?e.warning:void 0;return{stats:t,...n?{warning:n}:{}}}function O(e){let t,n=e.map(e=>e.durationMs).sort((e,t)=>e-t);return{count:n.length,p50Ms:E(n,50),p95Ms:E(n,95),maxMs:n[n.length-1]??0,slowThresholdMs:1500,...1===new Set(t=e.map(e=>e.platform).filter(e=>!!e)).size?{platform:t[0]}:{},...function(e){let t={};for(let n of e)n.backend&&(t[n.backend]=(t[n.backend]??0)+1);return Object.keys(t).length>0?{backends:t}:{}}(e)}}function E(e,t){if(0===e.length)return 0;let n=Math.max(0,Math.ceil(t/100*e.length)-1);return e[Math.min(n,e.length-1)]??0}function R(e){let t=e.platform?`${e.platform} `:"";return`Warning: ${t}snapshots are slow in this run: p95 ${e.p95Ms}ms over ${e.count} captures. Possible causes: device load, app or dev server stuck, helper fallback, or stale daemon.`}function T(e){return e&&"object"==typeof e&&!Array.isArray(e)?e:void 0}export{d as computeDaemonCodeSignature,j as consumeTextLines,$ as isDaemonProgressEnvelope,b as isDaemonResponseEnvelope,z as mergeSnapshotDiagnostics,c as normalizeTenantId,C as readSnapshotDiagnosticsSummary,_ as recordSnapshotTiming,h as resolveDaemonCodeSignature,i as resolveDaemonPaths,a as resolveDaemonServerMode,u as resolveDaemonTransportPreference,l as resolveSessionIsolationMode,w as serializeDaemonProgressEnvelope,D as serializeDaemonResponseEnvelope,k as serializeDaemonRpcResponseEnvelope,y as shellQuote,S as shellQuoteIfNeeded,x as shouldStreamRequestProgress,A as summarizeSnapshotDiagnostics,P as summarizeSnapshotTimingSamples};