<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: krish pavuluri</title>
    <description>The latest articles on DEV Community by krish pavuluri (@krish_pavuluri).</description>
    <link>https://dev.to/krish_pavuluri</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3817490%2F05f4f8f8-0d38-4318-983f-f58bc03ff8ad.png</url>
      <title>DEV Community: krish pavuluri</title>
      <link>https://dev.to/krish_pavuluri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krish_pavuluri"/>
    <language>en</language>
    <item>
      <title>Your Xbox Pad Is Not an Xbox Pad — Testing Controller Support on Real Android Devices</title>
      <dc:creator>krish pavuluri</dc:creator>
      <pubDate>Fri, 04 Sep 2026 22:57:54 +0000</pubDate>
      <link>https://dev.to/krish_pavuluri/your-xbox-pad-is-not-an-xbox-pad-testing-controller-support-on-real-android-devices-2g9d</link>
      <guid>https://dev.to/krish_pavuluri/your-xbox-pad-is-not-an-xbox-pad-testing-controller-support-on-real-android-devices-2g9d</guid>
      <description>&lt;p&gt;Press A on an Xbox pad. The game receives B. Nothing logs an error, nothing crashes, and every automated check still passes.&lt;/p&gt;

&lt;p&gt;That was the bug, and it cost more time than the rest of the feature combined. We spent a chunk of this cycle forwarding a controller from your own machine into a real Android device in a browser tab. Most of it was straightforward. One part was not, and it is the part worth writing down, because it fails silently and it will bite anyone who tries this.&lt;/p&gt;

&lt;p&gt;If you ship a mobile game with controller support you already know the testing story it replaces: a drawer of phones, a drawer of pads, and a Bluetooth pairing dance every time you want to check whether B still does what it did last sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easy half: input that the device believes
&lt;/h2&gt;

&lt;p&gt;The naive version of remote input is a synthetic event: you take a tap in the browser, ship the coordinates, and inject them at the app layer. That works for a lot of testing and it falls apart for exactly the cases games care about. A synthetic event aimed at a focused view is not what a controller produces, and anything reading the actual input stack — which is most game engines — will not see it.&lt;/p&gt;

&lt;p&gt;So the input has to arrive lower down. On Android, a forwarded controller can be presented to the system as a genuine input device: the kernel enumerates it, the input stack routes it, and a native game receives it the same way it would from a pad plugged into the phone. Nothing in the app has to be instrumented, and nothing has to know it is remote.&lt;/p&gt;

&lt;p&gt;That distinction matters more than it sounds. It is the difference between "we can test the menus" and "we can test the game."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The iOS caveat, stated plainly.&lt;/strong&gt; This is an Android claim. On iOS a forwarded pad can be presented to web content, but native apps do not see it. If you build an iOS game, remote controller testing is not solved by this and I am not going to pretend otherwise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The hard half: an Xbox pad is not an Xbox pad
&lt;/h2&gt;

&lt;p&gt;Android decides how to interpret a controller's report by looking at its vendor and product ids, then loading the matching key layout file. That layout is not cosmetic. It decides which bit in the report becomes which button.&lt;/p&gt;

&lt;p&gt;The first version presented one fixed identity for every pad. Plug in an Xbox controller, and it arrived at the device claiming to be a PlayStation one. Everything &lt;em&gt;looked&lt;/em&gt; fine — the device enumerated a controller, the buttons produced events, the sticks moved. But the two key layouts are near mirror images of each other. Read off a real device:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DualShock&lt;/th&gt;
&lt;th&gt;Xbox&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Right stick&lt;/td&gt;
&lt;td&gt;Z / Rz&lt;/td&gt;
&lt;td&gt;Rx / Ry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triggers&lt;/td&gt;
&lt;td&gt;Rx / Ry&lt;/td&gt;
&lt;td&gt;Z / Rz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Face buttons&lt;/td&gt;
&lt;td&gt;rotated&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital L2 / R2&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the right stick arrived as a trigger, and the face buttons were rotated by one. A mismapped button still produces a perfectly valid event, so nothing anywhere reported an error — which is how you get back to pressing A and receiving B, with nothing to flag it but a human watching the screen going "that's not right."&lt;/p&gt;

&lt;p&gt;The fix is that an identity is &lt;strong&gt;a descriptor and a button map together, never one alone&lt;/strong&gt;. Present the ids, and you have committed to the layout that goes with them. Detection reads the vendor id the browser reports for the pad and falls back to the product name — and that fallback is not a nicety. A real DualShock 4 over Bluetooth on macOS reports itself to Chrome as &lt;code&gt;Wireless Controller (STANDARD GAMEPAD)&lt;/code&gt;, with no vendor or product id in the string at all. Match on vendor id alone and that pad never resolves.&lt;/p&gt;

&lt;p&gt;Two things follow that are worth stealing if you are building anything similar:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An unrecognised pad should fall back to a &lt;em&gt;real&lt;/em&gt; identity, not a neutral one.&lt;/strong&gt; A neutral vendor id is what makes Chrome refuse to classify the pad, and then web content mislabels every button it guesses at. Better to be confidently one specific controller than vaguely none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swapping controllers mid-session has to tear down and re-create the device&lt;/strong&gt;, because the identity is fixed at creation time. You cannot mutate a pad into a different pad.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That table came off a real device, confirmed bit by bit, because this is not an area where reading the documentation is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  And then an agent picked up the controller
&lt;/h2&gt;

&lt;p&gt;The part we did not plan for.&lt;/p&gt;

&lt;p&gt;The same controller surface is exposed as tool calls, so an AI agent can hold the pad. It sees the screen and sends button and axis state to the device.&lt;/p&gt;

&lt;p&gt;The first attempt at this was useless, in an instructive way. An agent calling one tool per button press produces input separated by a full network round trip — seconds, not milliseconds. Every combo renders as a held button. The device is not wrong; the timing is simply gone.&lt;/p&gt;

&lt;p&gt;What works is sending the &lt;em&gt;sequence&lt;/em&gt; rather than the presses: a timed series of controller frames, played back on the device at thirty a second. The agent decides what the combo is, and the timing is preserved on the far end where it can actually be honoured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this is not:&lt;/strong&gt; an agent with human reflexes. We measured it. A round trip through the tool interface is around six seconds. A purpose-built local loop — frame in, controller out, no agent — came in at 134 ms median, against a pre-registered bar of about 50 ms for anything that could claim reflex play. It missed. The verdict was to not build the reflex surface, and that verdict stands.&lt;/p&gt;

&lt;p&gt;So: the agent can play. It cannot out-react you. Anyone claiming otherwise about their own setup should be asked for the number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;The honest pitch is narrow. If your game has controller support and you currently test it by keeping physical pads next to physical phones, this removes the physical part: any device, any pad, from a browser, with the input arriving as real hardware.&lt;/p&gt;

&lt;p&gt;That is it. It does not test your game for you, and on iOS it does not reach native apps at all.&lt;/p&gt;

&lt;p&gt;If you want to try it, the devices are at &lt;a href="https://robotactions.com" rel="noopener noreferrer"&gt;robotactions.com&lt;/a&gt; — there is a free tier, and the tool interface installs with &lt;code&gt;npx @robotactions/mcp@latest init&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And if you have hit the key-layout problem from the other side — writing the game rather than the harness — I would genuinely like to hear how you found it. My guess is "a tester said the buttons were wrong and nobody believed them for a week."&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>android</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your Mobile Suite Isn't Slow, It's Serial</title>
      <dc:creator>krish pavuluri</dc:creator>
      <pubDate>Sun, 23 Aug 2026 18:25:18 +0000</pubDate>
      <link>https://dev.to/krish_pavuluri/your-mobile-suite-isnt-slow-its-serial-5aom</link>
      <guid>https://dev.to/krish_pavuluri/your-mobile-suite-isnt-slow-its-serial-5aom</guid>
      <description>&lt;p&gt;A 400-test Appium suite that takes 90 minutes is not usually 90 minutes of slow tests. It's 90 minutes of one device doing 400 things in a row.&lt;/p&gt;

&lt;p&gt;You can shave seconds off individual steps forever and never fix that. The only lever with real leverage is how many devices are running at once — and the reasons teams don't pull it are almost never technical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The arithmetic nobody does
&lt;/h2&gt;

&lt;p&gt;Take the suite you have. Total wall-clock is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wall_clock ≈ (sum of all test durations / devices) + slowest_single_test + setup_per_device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things fall out of that immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One:&lt;/strong&gt; past a certain point, adding devices stops helping. If your longest single test is 6 minutes, no amount of parallelism gets the suite under 6 minutes. Splitting that one test is worth more than the next four devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two:&lt;/strong&gt; &lt;code&gt;setup_per_device&lt;/code&gt; is paid once per shard, not once per suite. If every shard installs the app, waits for a device to be handed to it, signs in and seeds fixtures, then a 12-way split pays that twelve times. Teams routinely go from 4 shards to 12, watch wall-clock barely move, and conclude parallelism doesn't work for mobile. What actually happened is that setup went from 20% of the run to 60% of it.&lt;/p&gt;

&lt;p&gt;Measure both numbers before you buy anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to cut the suite
&lt;/h2&gt;

&lt;p&gt;Sharding by "test file, alphabetically, into N buckets" is the default in most CI templates and it's the worst one available. Test durations on mobile are wildly uneven — a login test is 20 seconds, a checkout-through-payment test is four minutes — so alphabetical buckets give you one shard that finishes in three minutes and one that runs for twenty. Your suite is as slow as that last shard.&lt;/p&gt;

&lt;p&gt;Two better options, in order of effort:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Balance by recorded duration.&lt;/strong&gt; Keep last run's per-test timings in a JSON file in the repo, sort tests descending, and greedily assign each to whichever shard currently has the least work. Fifteen lines of code, and it typically beats alphabetical splitting by 30–40% on wall-clock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shard.mjs — longest-first bin packing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-timings.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allTests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lightest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;shards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;lightest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;lightest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unknown tests default to a middle-of-the-road guess so a new test never lands in a shard that's already full.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split by device requirement instead.&lt;/strong&gt; Some tests only make sense on specific hardware — the biometric flow, the small-screen layout regression, the tablet split view. Those aren't interchangeable work units, so pin them and balance whatever's left.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that shows up only in parallel
&lt;/h2&gt;

&lt;p&gt;Serial suites hide shared state. Run them twelve at a time and it surfaces within a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One login account, twelve sessions.&lt;/strong&gt; The classic. Test 7 signs in, test 31 signs the same account in elsewhere, the backend invalidates the first session, test 7 fails on a screen that has nothing to do with auth. Every re-run picks a different victim, so it reads as flakiness. Fix it with a pool of accounts leased per shard, not per suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared fixtures with mutable state.&lt;/strong&gt; A test that edits the seeded user's profile and a test that asserts on that profile are fine in sequence and a coin flip in parallel. Either seed per shard or make tests create what they assert on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits and quotas.&lt;/strong&gt; Twelve shards hitting the same staging API multiply your request rate by twelve. Sandboxes for payments and SMS are especially quick to start throttling — and a 429 surfaces in your app as a generic error screen, not as "you are being rate limited."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Device state left behind.&lt;/strong&gt; Cached logins, granted permissions, notification badges, a half-finished onboarding. Serially, test 40 inherits whatever test 39 left; in parallel it inherits whatever some &lt;em&gt;other&lt;/em&gt; shard left. Decide explicitly whether each shard starts from a clean install or a warm known state, and enforce it in setup rather than hoping.&lt;/p&gt;

&lt;p&gt;None of these are parallelism bugs. Parallelism just stops your suite from accidentally serialising around them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just add more parallel runs" is usually the expensive answer
&lt;/h2&gt;

&lt;p&gt;On most device clouds, concurrency is the billed unit: you buy N parallel slots and the suite is shaped around N. That has two consequences that get felt long before the invoice does.&lt;/p&gt;

&lt;p&gt;The first is queuing. When all slots are busy, jobs wait — so the number that matters isn't your test duration, it's your time-to-first-device during the 4pm rush when everyone merges.&lt;/p&gt;

&lt;p&gt;The second is that you start optimising the wrong thing. Once concurrency is scarce, teams cut coverage to fit the slots they bought, and "which tests run on hardware" becomes a budgeting decision rather than a risk decision.&lt;/p&gt;

&lt;p&gt;Our answer is a different unit. Devices here are &lt;strong&gt;dedicated to you&lt;/strong&gt; and priced flat per device per month — not metered per parallel run, per minute, or per slot. A device you're paying for is yours whether it's mid-suite or idle at 3am, which means the shard count is something you tune for wall-clock, not something you ration.&lt;/p&gt;

&lt;p&gt;It also means state persists between runs. A shard doesn't have to reinstall and re-sign-in against a freshly wiped machine every time, which cuts straight into that &lt;code&gt;setup_per_device&lt;/code&gt; term that was eating your gains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into CI
&lt;/h2&gt;

&lt;p&gt;The matrix is the easy part. This is GitHub Actions, but the shape is identical in GitLab CI or Jenkins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;e2e&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;shard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;6&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node shard.mjs --index ${{ matrix.shard }} --total 6 &amp;gt; shard-tests.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx wdio run wdio.conf.js --spec-file shard-tests.txt&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GRID_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ROBOTACTIONS_GRID_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;GRID_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ROBOTACTIONS_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;results-${{ matrix.shard }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details that matter more than they look:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fail-fast: false&lt;/code&gt; — otherwise the first red shard cancels the other five and you get one failure per run instead of the full picture. On a suite you're trying to stabilise, that turns a one-hour debugging loop into a one-day one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;if: always()&lt;/code&gt; on the artifact upload — results from failed shards are the ones you actually need.&lt;/p&gt;

&lt;p&gt;And merge the reports. Six separate JUnit XMLs in six artifacts is not a test report; run them through a merge step so the PR gets a single pass/fail with a single list of failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Record per-test durations for one run. You need the data before any of this is a decision.&lt;/li&gt;
&lt;li&gt;Find your longest single test. That's your floor — split it or accept it.&lt;/li&gt;
&lt;li&gt;Measure &lt;code&gt;setup_per_device&lt;/code&gt;. If it's over ~90 seconds, fixing it beats adding shards.&lt;/li&gt;
&lt;li&gt;Move from alphabetical to duration-balanced sharding. It's an afternoon and it's free.&lt;/li&gt;
&lt;li&gt;Then, and only then, raise the shard count.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most suites have a comfortable 3–4× sitting in steps 2 through 4 before they need a single extra device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://robotactions.com" rel="noopener noreferrer"&gt;Run your suite across dedicated devices →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://robotactions.com/blog/parallel-mobile-tests-sharding" rel="noopener noreferrer"&gt;robotactions.com/blog/parallel-mobile-tests-sharding&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>cicd</category>
      <category>devops</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Flaky Mobile Tests: A Triage Order That Actually Converges</title>
      <dc:creator>krish pavuluri</dc:creator>
      <pubDate>Sun, 23 Aug 2026 18:22:48 +0000</pubDate>
      <link>https://dev.to/krish_pavuluri/flaky-mobile-tests-a-triage-order-that-actually-converges-29ho</link>
      <guid>https://dev.to/krish_pavuluri/flaky-mobile-tests-a-triage-order-that-actually-converges-29ho</guid>
      <description>&lt;p&gt;Most teams don't fix flakiness. They add a retry, watch the suite go green, and move on — and six months later nobody trusts a red build, because half of them are noise and telling which half is a coin flip.&lt;/p&gt;

&lt;p&gt;The reason it never converges isn't a lack of effort. It's that "flaky" names five completely different problems with completely different fixes, and teams debug whichever one they thought of first.&lt;/p&gt;

&lt;p&gt;Here's an order that terminates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: measure, don't remember
&lt;/h2&gt;

&lt;p&gt;You cannot triage from impressions. Before anything else, get one number per test: &lt;strong&gt;failure rate over the last 50 runs on unchanged code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don't have that, the cheapest way to get it is a nightly job that runs the suite against &lt;code&gt;main&lt;/code&gt; five times and appends results to a CSV. A week gets you 35 data points per test, which is enough to rank.&lt;/p&gt;

&lt;p&gt;Two things fall out immediately and both are worth the week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usually &lt;strong&gt;fewer than 10% of tests produce more than half the flaky failures.&lt;/strong&gt; Fix those five tests and the suite feels transformed. Without the data you'd have spent that time on whichever test annoyed someone loudly.&lt;/li&gt;
&lt;li&gt;Some "flaky" tests aren't flaky at all — they fail 100% of the time on one device or one OS version and pass everywhere else. That's not flakiness, it's an unfiled bug wearing a disguise, and it's the highest-value thing in the list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rank by &lt;code&gt;failure_rate × runs_per_day&lt;/code&gt;. Work top-down. Stop reading Slack for candidates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: is it the test, or is it the app?
&lt;/h2&gt;

&lt;p&gt;Before assuming a test bug, take the top offender and run it 20 times in isolation on one device.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fails at a similar rate in isolation&lt;/strong&gt; → it's the test, or a genuine app bug. Continue to step 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passes 20/20 in isolation, fails in the suite&lt;/strong&gt; → it's interference from the tests around it. Skip to step 3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one question splits the work in half, takes twenty minutes, and is skipped almost universally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: the sync problem (most flakiness lives here)
&lt;/h2&gt;

&lt;p&gt;Nearly all single-test flakiness is a race between your test and the app's rendering, and nearly all of it comes from the same three patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sleeping instead of waiting.&lt;/strong&gt; &lt;code&gt;sleep(2)&lt;/code&gt; is a bet that the app is ready in under two seconds on every device, every network, every CI load. It passes on your M-series laptop and fails on a mid-range Android under thermal load. Every fixed sleep is a flaky test with a delay fuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting for presence, acting on interactivity.&lt;/strong&gt; The element exists in the tree well before it's ready. A button that's rendered but still animating in eats the tap and reports success — the tap landed, it just didn't do anything, and you fail three assertions later on a screen that never changed. Wait for &lt;em&gt;enabled and stable&lt;/em&gt;, not merely &lt;em&gt;present&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting for the wrong thing.&lt;/strong&gt; The classic: waiting for a spinner to appear, when on a fast response it appears and vanishes before the poll interval catches it, and your wait times out on a screen that already loaded successfully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Flaky: passes when the spinner is slow, times out when the app is fast&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visibilityOfElementLocated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loading_spinner"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invisibilityOfElementLocated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loading_spinner"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

&lt;span class="c1"&gt;// Stable: wait for the post-condition, not the transition&lt;/span&gt;
&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elementToBeClickable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"results_list"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The general rule: &lt;strong&gt;assert on the state you want, never on the transition into it.&lt;/strong&gt; Transitions are racy by definition; end states are not.&lt;/p&gt;

&lt;p&gt;One more that only shows up on real hardware — animations. A screen that has finished loading but is still sliding in will hand your tap to whatever pixel is under the finger mid-flight. Disabling system animations on the device (&lt;code&gt;appium:disableWindowAnimation&lt;/code&gt;, or the developer options scale settings) removes an entire class of failure and speeds the suite up as a bonus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: state leakage (the one that scales with your suite)
&lt;/h2&gt;

&lt;p&gt;If the test passes alone and fails in the pack, something outside it is changing what it sees.&lt;/p&gt;

&lt;p&gt;Work through the shared surfaces in this order — roughly most to least common:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Device state.&lt;/strong&gt; Cached logins, granted permissions, dismissed onboarding, a notification still on screen, keyboard left open, app left backgrounded. Test 40 inherits whatever test 39 left behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend state.&lt;/strong&gt; The account whose cart another test just emptied; the record another test renamed; the feature flag someone flipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared credentials.&lt;/strong&gt; One login used by parallel shards, where a new session invalidates the old one and some unrelated test gets bounced to the login screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time and clock.&lt;/strong&gt; A test that passes except around midnight, or on the last day of a month, or in a CI runner on UTC when the assertion assumed local time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data accumulation.&lt;/strong&gt; Test 1 creates an item, so test 12's "list should show 3 items" assertion has been quietly counting up all week.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is always the same shape and it's rarely "clean up afterwards": &lt;strong&gt;make each test create what it asserts on.&lt;/strong&gt; Teardown-based cleanup fails exactly when the test fails — which is when the leakage matters most — so the next test inherits the mess from the run that already went wrong, and one real failure cascades into six fake ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: environment (rule out before rewriting anything)
&lt;/h2&gt;

&lt;p&gt;If steps 2 and 3 found nothing, stop editing the test and look at where it ran.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Device health.&lt;/strong&gt; Low battery throttles CPU. Full storage makes installs and screenshots fail in creative ways. Thermal throttling after an hour of continuous runs slows everything enough to blow timeouts that were fine at run 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network.&lt;/strong&gt; A staging API with a p99 of eight seconds will produce flakiness no test-side fix can remove. Log the request timings; if the p99 moves with the failures, the fix is in the backend, not the suite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits.&lt;/strong&gt; Parallel shards multiply your request rate. A 429 usually surfaces in the app as a generic error screen, so it reads as an app bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS-level interruptions.&lt;/strong&gt; An OS update banner, a "storage almost full" alert, a carrier message landing on top of your flow. Rarer than people assume, but genuinely random when it happens — which makes it the most confusing category to debug from a stack trace alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also where inconsistent devices show up as inconsistent tests. If two runs of the same test get two differently-configured handsets — different locale, different OS patch level, different leftover state — the test is being blamed for the environment's variance. Running against a device that's &lt;strong&gt;dedicated and stable between runs&lt;/strong&gt; doesn't fix a badly written test, but it does remove the variable, which is what lets you conclude anything at all from step 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: it's a real bug
&lt;/h2&gt;

&lt;p&gt;Some flaky tests are correct. They're catching a genuine race in the app — a request that sometimes resolves after the screen is gone, a cache that's occasionally stale, a listener registered twice.&lt;/p&gt;

&lt;p&gt;The tell: the failure is &lt;strong&gt;not&lt;/strong&gt; at a &lt;code&gt;findElement&lt;/code&gt;, the screenshot shows a plausible screen, and the app behaves wrongly rather than the test looking in the wrong place. Deleting or retrying this test is deleting a bug report that reproduces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to capture so triage is possible at all
&lt;/h2&gt;

&lt;p&gt;Most of the above is impossible from a stack trace. On every failure, capture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A screenshot at the moment of failure&lt;/strong&gt; — separates "wrong screen" from "right screen, bad selector" instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The element tree at that moment&lt;/strong&gt; — tells you what locators actually existed, which is what you need to write the fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video of the run&lt;/strong&gt; — the only way to catch a dialog that appeared and disappeared, or a tap that landed mid-animation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device logs and network requests&lt;/strong&gt; — turns "the app showed an error" into "the API returned 429."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which device, OS version, and build&lt;/strong&gt; — so the "fails only on one device" pattern can surface at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without these you re-run locally to reproduce, which for genuinely flaky tests is the single biggest time sink in the process — you're trying to reproduce something that happens 1 in 8 times, and every attempt costs a full suite setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  On retries
&lt;/h2&gt;

&lt;p&gt;Retries are a &lt;strong&gt;measurement tool&lt;/strong&gt;, not a fix. Configure them so that a test that passes on retry is still reported distinctly from one that passed first time — most runners support this, and it's the difference between a retry policy and a cover-up.&lt;/p&gt;

&lt;p&gt;Then treat the retry-pass count as your flakiness metric, and hold it to a budget. A test that needs retries is on a clock, not on a list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quarantine rule
&lt;/h2&gt;

&lt;p&gt;Quarantine works only with an expiry date. Without one it's a graveyard: tests nobody runs, nobody deletes, and nobody trusts.&lt;/p&gt;

&lt;p&gt;A policy that survives contact with a real team:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A test over your flakiness budget gets quarantined — still runs, doesn't block the build.&lt;/li&gt;
&lt;li&gt;It gets an owner and a two-week deadline, both written down.&lt;/li&gt;
&lt;li&gt;At two weeks it is either fixed or &lt;strong&gt;deleted&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Deleting is a legitimate outcome. A test nobody trusts and nobody fixes has negative value: it costs runtime, it costs triage attention, and it trains everyone to ignore red.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://robotactions.com" rel="noopener noreferrer"&gt;Run your suite on dedicated devices with video, logs and the element tree on every failure →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://robotactions.com/blog/flaky-mobile-tests-triage" rel="noopener noreferrer"&gt;robotactions.com/blog/flaky-mobile-tests-triage&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>mobile</category>
      <category>qa</category>
      <category>devops</category>
    </item>
    <item>
      <title>Auto-Accept Permission Dialogs at Session Start — Then Turn It Off Mid-Test</title>
      <dc:creator>krish pavuluri</dc:creator>
      <pubDate>Sun, 23 Aug 2026 18:19:44 +0000</pubDate>
      <link>https://dev.to/krish_pavuluri/auto-accept-permission-dialogs-at-session-start-then-turn-it-off-mid-test-2nm0</link>
      <guid>https://dev.to/krish_pavuluri/auto-accept-permission-dialogs-at-session-start-then-turn-it-off-mid-test-2nm0</guid>
      <description>&lt;p&gt;Every mobile suite hits this on day one. The app launches, iOS asks for notifications, then location, then tracking, and every test that isn't about permissions dies on a dialog it never asked for.&lt;/p&gt;

&lt;p&gt;So you set &lt;code&gt;appium:autoAcceptAlerts&lt;/code&gt; and the suite goes green.&lt;/p&gt;

&lt;p&gt;Then someone writes the one test that &lt;em&gt;is&lt;/em&gt; about permissions — the one that asserts the location prompt appears, taps &lt;strong&gt;Don't Allow&lt;/strong&gt;, and checks the empty state — and discovers that the capability that saved the suite is now the thing blocking it. The alert is gone before the test can see it.&lt;/p&gt;

&lt;p&gt;Here's the accurate picture of what's session-scoped, what's runtime-toggleable, and what to reach for on each platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: the session capabilities
&lt;/h2&gt;

&lt;p&gt;These are set at session creation. They're the ones everyone reaches for first — and, as the next section shows, they are not the layer you actually want to be controlling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS (XCUITest).&lt;/strong&gt; &lt;a href="https://appium.github.io/appium-xcuitest-driver/latest/reference/capabilities/" rel="noopener noreferrer"&gt;&lt;code&gt;appium:autoAcceptAlerts&lt;/code&gt;&lt;/a&gt; — "Accept all iOS alerts automatically if they pop up. This includes privacy access permission alerts (location, contacts, photos). Default is &lt;code&gt;false&lt;/code&gt;." &lt;code&gt;appium:autoDismissAlerts&lt;/code&gt; is the mirror image. Note what the wording covers: &lt;strong&gt;system&lt;/strong&gt; alerts, including privacy prompts, not just your app's own &lt;code&gt;UIAlertController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android (UiAutomator2).&lt;/strong&gt; There's no &lt;code&gt;autoAcceptAlerts&lt;/code&gt; equivalent, because Android runtime permissions aren't really alerts you accept — they're grants you can hand over before the dialog ever appears. That's &lt;a href="https://github.com/appium/appium-uiautomator2-driver/blob/master/README.md" rel="noopener noreferrer"&gt;&lt;code&gt;appium:autoGrantPermissions&lt;/code&gt;&lt;/a&gt;: "Whether to grant all the requested application permissions automatically when a test starts."&lt;/p&gt;

&lt;p&gt;Three constraints on it that bite in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;targetSdkVersion&lt;/code&gt; in the manifest must be ≥ 23 and the device must be Android 6+.&lt;/li&gt;
&lt;li&gt;Apps with &lt;code&gt;targetSdkVersion&lt;/code&gt; ≤ 22 "must be reinstalled to grant permissions" — e.g. with &lt;code&gt;appium:fullReset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Grants happen &lt;strong&gt;at install time&lt;/strong&gt;. If your session reuses an already-installed app, the capability has nothing to act on. This is the single most common reason people report that "autoGrantPermissions doesn't work."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And for special permissions — notifications access, media recording — the plain grant path doesn't cover them; the docs point you at &lt;code&gt;mobile: changePermissions&lt;/code&gt; with the &lt;code&gt;appops&lt;/code&gt; target instead. More on that below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One iOS caveat worth internalising:&lt;/strong&gt; the &lt;code&gt;appium:permissions&lt;/code&gt; capability, the one that lets you preset per-service permission states, "Allows to set permissions for the specified application bundle &lt;strong&gt;on Simulator only&lt;/strong&gt;." On a real iPhone it isn't available. Real-device iOS permission state is something you handle through dialogs or through how the device was left.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two settings that actually control it
&lt;/h2&gt;

&lt;p&gt;The capabilities are not the mechanism. They're a convenience that seeds one, and the mechanism underneath is runtime-mutable — which means the mid-session toggle everyone assumes is impossible is in fact two lines of code.&lt;/p&gt;

&lt;p&gt;When the driver builds its underlying iOS automation session, it does exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoAcceptAlerts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;wdaCaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultAlertAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoDismissAlerts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;wdaCaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultAlertAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dismiss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;appium:autoAcceptAlerts&lt;/code&gt; is a &lt;em&gt;front end&lt;/em&gt; for a setting called &lt;code&gt;defaultAlertAction&lt;/code&gt;. And &lt;code&gt;defaultAlertAction&lt;/code&gt; is registered in the settings map, so it can be written any time during the session. The driver forwards any setting key it doesn't handle specially straight to the agent's settings endpoint, so a plain &lt;code&gt;setSettings&lt;/code&gt; call reaches it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Turn auto-accept off, mid-session&lt;/span&gt;
&lt;span class="n"&gt;getAppiumDriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setSettings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImmutableMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"defaultAlertAction"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// ... assert on the dialog, tap what you want ...&lt;/span&gt;

&lt;span class="c1"&gt;// Turn it back on&lt;/span&gt;
&lt;span class="n"&gt;getAppiumDriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setSettings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImmutableMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"defaultAlertAction"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"accept"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see &lt;code&gt;"off"&lt;/code&gt; used for the disable value, and it works — but by accident rather than by design. The handling is a straight comparison against &lt;code&gt;accept&lt;/code&gt; and &lt;code&gt;dismiss&lt;/code&gt;; anything else falls through to a branch that logs &lt;code&gt;'off' default alert action is unsupported&lt;/code&gt; and does nothing. The behaviour you want, with a warning line per alert. An &lt;strong&gt;empty string&lt;/strong&gt; is the value that's explicitly checked for and returns early with no log noise. (&lt;code&gt;null&lt;/code&gt; works too, but &lt;code&gt;ImmutableMap&lt;/code&gt; rejects null values, which is exactly why &lt;code&gt;"off"&lt;/code&gt; tends to be what people reach for in Java.)&lt;/p&gt;

&lt;p&gt;This is worth knowing precisely because none of it is in the driver's settings documentation — &lt;code&gt;defaultAlertAction&lt;/code&gt; isn't listed there, and the &lt;a href="https://discuss.appium.io/t/temporarily-disable-autoacceptalerts-for-appium-and-then-re-enable/12523" rel="noopener noreferrer"&gt;Appium forum answer on this question&lt;/a&gt; is "remove autoAccept and manage all by yourself." That answer is out of date. The setting is real, it's readable back, and it's the supported shape of the thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;autoClickAlertSelector&lt;/code&gt;, and why it's checked first
&lt;/h3&gt;

&lt;p&gt;The second setting is &lt;a href="https://appium.github.io/appium-xcuitest-driver/latest/reference/settings/" rel="noopener noreferrer"&gt;&lt;code&gt;autoClickAlertSelector&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Custom selector for an alert button. This can be used to automatically locate an element inside&lt;br&gt;
an alert hierarchy, and tap it as soon as a new alert is detected.&lt;/p&gt;

&lt;p&gt;This setting takes priority over the &lt;code&gt;appium:autoAcceptAlerts&lt;/code&gt; and &lt;code&gt;appium:autoDismissAlerts&lt;/code&gt;&lt;br&gt;
capabilities.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Takes priority" is literal: when an alert appears, the selector is consulted first, and if it's set the handler taps and returns without ever consulting &lt;code&gt;defaultAlertAction&lt;/code&gt; — including when the tap fails. It's strictly more expressive than the boolean, because it names &lt;em&gt;which&lt;/em&gt; button gets tapped rather than accepting whatever the OS considers the default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateSettings&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;autoClickAlertSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/XCUIElementTypeButton[`label CONTAINS[c] 'allow'`]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To disable it, set it to an empty string. This isn't a trick — an empty value is special-cased ahead of any parsing, and it additionally tears down the alert monitor rather than merely failing to match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;getAppiumDriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setSettings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImmutableMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"autoClickAlertSelector"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't disable it by supplying a valid selector that matches nothing. That leaves the monitor running and pays for a class-chain query against every alert for no benefit. And note that the documented "an error is thrown if the provided selector is invalid" applies only to non-empty values — the parse step is never reached for an empty one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting the two together
&lt;/h3&gt;

&lt;p&gt;Because the selector short-circuits the alert action, &lt;strong&gt;fully disabling auto-handling means clearing both&lt;/strong&gt;, in that order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;pauseAlertHandling&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;getAppiumDriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setSettings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImmutableMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"autoClickAlertSelector"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
  &lt;span class="n"&gt;getAppiumDriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setSettings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImmutableMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"defaultAlertAction"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;resumeAlertHandling&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;getAppiumDriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setSettings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImmutableMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"defaultAlertAction"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"accept"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clearing only &lt;code&gt;defaultAlertAction&lt;/code&gt; while a selector is still set changes nothing at all, because the selector is what's handling your alerts. That's the failure mode to watch for if you've configured both — the disable appears to do nothing, and it looks like the setting was ignored.&lt;/p&gt;

&lt;p&gt;Wire that pair into &lt;code&gt;@BeforeEach&lt;/code&gt; / &lt;code&gt;@AfterEach&lt;/code&gt; and the permission-specific tests live in the same session and the same suite as everything else, with no second capability set.&lt;/p&gt;

&lt;p&gt;You can also start the session with the setting instead of the boolean capability, using Appium's &lt;code&gt;appium:settings[...]&lt;/code&gt; prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platformName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appium:automationName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XCUITest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appium:settings[autoClickAlertSelector]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/XCUIElementTypeButton[`label CONTAINS[c] 'allow'`]"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same effect as &lt;code&gt;autoAcceptAlerts&lt;/code&gt;, but now the behaviour lives somewhere you can change without tearing down the session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2, Android: revoke and re-grant at runtime
&lt;/h2&gt;

&lt;p&gt;Android's runtime lever isn't about alerts at all — it's about the grant itself. The UiAutomator2 driver's &lt;code&gt;mobile: changePermissions&lt;/code&gt; "Changes package permissions in runtime":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Argument&lt;/th&gt;
&lt;th&gt;Values&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;permissions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;permission name, array, or &lt;code&gt;all&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;all&lt;/code&gt; works only with &lt;code&gt;target: pm&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appPackage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;package name&lt;/td&gt;
&lt;td&gt;defaults to the app under test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;grant&lt;/code&gt; (default) / &lt;code&gt;revoke&lt;/code&gt; for &lt;code&gt;pm&lt;/code&gt;; &lt;code&gt;allow&lt;/code&gt; / &lt;code&gt;deny&lt;/code&gt; / &lt;code&gt;ignore&lt;/code&gt; / &lt;code&gt;default&lt;/code&gt; for &lt;code&gt;appops&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pm&lt;/code&gt; (default) or &lt;code&gt;appops&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;appops&lt;/code&gt; requires the driver's &lt;code&gt;adb_shell&lt;/code&gt; server security option&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the whole test starts with permissions granted, and the one test that needs the dialog revokes it first and lets the app ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Start clean: revoke, so the next launch actually prompts&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeScript&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mobile: changePermissions"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"permissions"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"android.permission.ACCESS_FINE_LOCATION"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"revoke"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeScript&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mobile: activateApp"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"appId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"com.mycompany.myapp"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// ... assert the system dialog, tap Deny, assert your empty state ...&lt;/span&gt;

&lt;span class="c1"&gt;// Hand it back for the rest of the suite&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeScript&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mobile: changePermissions"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"permissions"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"all"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"grant"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to assert on state rather than guess at it, &lt;code&gt;mobile: getPermissions&lt;/code&gt; takes a &lt;code&gt;type&lt;/code&gt; of &lt;code&gt;denied&lt;/code&gt;, &lt;code&gt;granted&lt;/code&gt; or &lt;code&gt;requested&lt;/code&gt; (the default) and returns a list of permission names. That turns "did the grant actually apply?" from a debugging session into an assertion.&lt;/p&gt;

&lt;p&gt;Note the &lt;code&gt;revoke&lt;/code&gt;-then-relaunch pattern. Revoking a permission that a running process already holds is not something the app observes gracefully — Android may kill the process. Do it before activating the app, not in the middle of a flow, unless killing the app is the thing you're testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: handling one alert by hand
&lt;/h2&gt;

&lt;p&gt;When you just need to deal with the dialog in front of you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt; — &lt;code&gt;mobile: alert&lt;/code&gt; takes an &lt;code&gt;action&lt;/code&gt; of &lt;code&gt;accept&lt;/code&gt;, &lt;code&gt;dismiss&lt;/code&gt; or &lt;code&gt;getButtons&lt;/code&gt;, plus an optional &lt;code&gt;buttonLabel&lt;/code&gt;. &lt;code&gt;getButtons&lt;/code&gt; is underused and genuinely handy: assert on the exact button set the OS is offering before you commit to tapping one, which is how you catch iOS-version differences in prompt wording instead of failing on a hardcoded label.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buttons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mobile: alert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;getButtons&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// e.g. ["Don't Allow", "Allow Once", "Allow While Using App"]&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mobile: alert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accept&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buttonLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allow Once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Android&lt;/strong&gt; — &lt;code&gt;mobile: acceptAlert&lt;/code&gt; and &lt;code&gt;mobile: dismissAlert&lt;/code&gt;, both with an optional &lt;code&gt;buttonLabel&lt;/code&gt;. The docs are refreshingly honest about the reliability: "This method might not always be reliable as there is no single standard for how Android alerts should look like within the Accessibility representation." Pass an explicit &lt;code&gt;buttonLabel&lt;/code&gt; when you know it, and prefer &lt;code&gt;changePermissions&lt;/code&gt; over dialog-tapping for anything permission-shaped.&lt;/p&gt;

&lt;p&gt;Two more iOS settings worth knowing while you're in here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;acceptAlertButtonSelector&lt;/code&gt; / &lt;code&gt;dismissAlertButtonSelector&lt;/code&gt; — class chains that change which button the standard W3C Accept Alert / Dismiss Alert commands press. For "handle accept buttons with arbitrary text," i.e. custom in-app dialogs whose buttons say &lt;em&gt;Got it&lt;/em&gt; and &lt;em&gt;Maybe later&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;respectSystemAlerts&lt;/code&gt; — "Whether to automatically switch the active application to the system springboard if a native alert element is detected." Default &lt;code&gt;false&lt;/code&gt;. If your alert handling works everywhere except when a system prompt overlays your app, turn this on before you start suspecting the driver.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A policy that holds up
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Permission prompts are noise in 95% of tests&lt;/td&gt;
&lt;td&gt;iOS: &lt;code&gt;appium:settings[autoClickAlertSelector]&lt;/code&gt;. Android: &lt;code&gt;appium:autoGrantPermissions&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One test must assert on the prompt&lt;/td&gt;
&lt;td&gt;iOS: clear &lt;code&gt;autoClickAlertSelector&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;defaultAlertAction&lt;/code&gt; to &lt;code&gt;""&lt;/code&gt;, restore after. Android: &lt;code&gt;mobile: changePermissions&lt;/code&gt; → &lt;code&gt;revoke&lt;/code&gt;, then relaunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Special permissions (notifications, recording)&lt;/td&gt;
&lt;td&gt;Android: &lt;code&gt;mobile: changePermissions&lt;/code&gt; with &lt;code&gt;target: appops&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt wording varies by OS version&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mobile: alert&lt;/code&gt; → &lt;code&gt;getButtons&lt;/code&gt;, assert, then act&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom in-app dialog, non-standard buttons&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;acceptAlertButtonSelector&lt;/code&gt; / &lt;code&gt;dismissAlertButtonSelector&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS Simulator, preset state before launch&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;appium:permissions&lt;/code&gt; (Simulator only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The through-line: prefer the setting over the capability even when both would work today. The capability is a decision you make once per session and then live with; the setting is a decision you can revisit per test. Mobile suites always eventually contain the test that needs the opposite of the default, and discovering that after you've built 400 tests around a capability is an expensive time to find out the layer beneath it was writable all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes on a real device
&lt;/h2&gt;

&lt;p&gt;Two things, and both cut in your favour once you know about them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;appium:permissions&lt;/code&gt; being Simulator-only means real-device iOS permission state comes from the device itself — whatever the last session left behind. On a shared pool that's a source of mystery failures: you don't know which state you inherited. On a &lt;strong&gt;dedicated&lt;/strong&gt; device, that state is &lt;em&gt;yours&lt;/em&gt; and it's stable between runs, so "granted at the start of the suite" is something you can establish once and rely on rather than re-derive every session.&lt;/p&gt;

&lt;p&gt;The flip side is that stable state is still state. Make the starting condition explicit at suite setup — grant or revoke deliberately — rather than letting run N inherit run N−1 by accident.&lt;/p&gt;

&lt;p&gt;And when a prompt does slip through in CI, the thing that ends the argument fastest is seeing it: a video of the run and the element tree at the moment of failure tell you in seconds whether the dialog appeared and went unhandled, or never appeared at all. Those two failures produce identical stack traces and completely different fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://robotactions.com" rel="noopener noreferrer"&gt;Run your Appium suite on a dedicated real device →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://robotactions.com/blog/auto-accept-dismiss-alerts-appium" rel="noopener noreferrer"&gt;robotactions.com/blog/auto-accept-dismiss-alerts-appium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>appium</category>
      <category>testing</category>
      <category>mobile</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Give an AI Agent Full Control of a Real iOS or Android Device (over MCP)</title>
      <dc:creator>krish pavuluri</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:44:07 +0000</pubDate>
      <link>https://dev.to/krish_pavuluri/how-to-give-an-ai-agent-full-control-of-a-real-ios-or-android-device-over-mcp-42gc</link>
      <guid>https://dev.to/krish_pavuluri/how-to-give-an-ai-agent-full-control-of-a-real-ios-or-android-device-over-mcp-42gc</guid>
      <description>&lt;p&gt;AI agents are getting good at writing code, browsing the web, and calling APIs. But there's one surface most of them still can't touch: a &lt;strong&gt;real phone&lt;/strong&gt;. Not a simulator, not a screenshot fed back to the model — an actual iOS or Android device, driven with the fidelity of a human's fingers.&lt;/p&gt;

&lt;p&gt;This guide shows you how to give an agent exactly that, over the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;. By the end, your MCP client — Claude Desktop, Cursor, Cline, or your own — will be able to open a real device, tap and type on it, see what the app is doing, capture and mock network traffic, spoof GPS, and even explore an app and generate test scripts. All of it as MCP tools the agent can call directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll be able to do
&lt;/h2&gt;

&lt;p&gt;Once connected, the agent has the same reach a human tester has on a real device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt; — real taps, press-and-hold, swipes, drag, and typing (native, hardware-level input, not a laggy screen-share overlay)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;See&lt;/strong&gt; — live screenshots, on-device web inspection, the UI element tree / DOM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect the network&lt;/strong&gt; — capture every request the app makes, and &lt;strong&gt;mock any request or response&lt;/strong&gt; to force error states and edge cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulate&lt;/strong&gt; — set GPS location anywhere on earth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate&lt;/strong&gt; — explore an app, find issues, and generate runnable test scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An MCP-capable client (Claude Desktop, Cursor, Cline, or any client that speaks MCP)&lt;/li&gt;
&lt;li&gt;A free RobotActions account (that's what exposes the real devices as MCP tools)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — Create a free account
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;strong&gt;&lt;a href="https://robotactions.com" rel="noopener noreferrer"&gt;robotactions.com&lt;/a&gt;&lt;/strong&gt; — one click with Google or GitHub.&lt;/p&gt;

&lt;p&gt;That's all the setup you need: you'll authorize your MCP client in the next step and it fetches your access automatically, so there's no token to create, copy, or paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Connect your MCP client
&lt;/h2&gt;

&lt;p&gt;You have two paths. Pick the one that matches your client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A — Claude.ai / Claude Desktop (remote connector)
&lt;/h3&gt;

&lt;p&gt;Add RobotActions as a remote MCP connector pointing at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mcp.robotactions.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00uzij4kr7306cg5rzjt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00uzij4kr7306cg5rzjt.png" alt="Add RobotActions as a custom MCP connector in Claude — paste the endpoint URL and click Add." width="543" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click &lt;strong&gt;Connect&lt;/strong&gt; and authorize. Signing in fetches your access automatically — no manual token entry.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcodxdks3izrs51sptdzz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcodxdks3izrs51sptdzz.png" alt="Approve the connection when prompted, and Claude finishes linking to RobotActions." width="753" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B — Cursor, Cline, or a local MCP config
&lt;/h3&gt;

&lt;p&gt;Use the connector package to wire it into your MCP config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @robotactions/mcp@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or point your client's MCP config at the same endpoint &lt;code&gt;https://mcp.robotactions.com/mcp&lt;/code&gt;, then authorize the same way when prompted. (RobotActions is also listed on the official MCP registry, Smithery, and glama if your client installs from there.)&lt;/p&gt;

&lt;p&gt;Reload your client and you should see the RobotActions tools appear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnml02j3vgloq4kevyil.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnml02j3vgloq4kevyil.png" alt="RobotActions connected in Claude — the full set of device tools is now available to the agent." width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Your first commands on a real device
&lt;/h2&gt;

&lt;p&gt;Now just talk to your agent. Try:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"List the available devices, open an Android one, and take a screenshot."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent will call the device-list and screenshot tools and hand you back a real frame from a real phone. From there:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Open the Settings app, go to Wi-Fi, and turn it off."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent taps and swipes on the actual device — real touches, not a simulated overlay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — The part agents couldn't do before
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. Because the agent has &lt;em&gt;full&lt;/em&gt; access — visual and programmatic — you can ask it to do things a screen-share bot never could:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inspect and mock the network:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Capture the network calls this app makes on launch, then mock the /profile response to return an empty state, and show me how the app renders it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Force a location:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Set the device's GPS to Tokyo and check that the store locator updates."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Explore and test:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Walk through the checkout flow, flag anything that looks broken, and generate a test script that reproduces it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent sees the screen, reads the network and the UI elements, and drives the input — so "test this app" becomes a single instruction instead of a week of setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Mobile test infrastructure was built for humans staring at a remote screen. As agents start doing real work, they need to &lt;em&gt;operate&lt;/em&gt; real devices with human-level fidelity — and reach everything programmatically, not just the pixels. Exposing real-device control as first-class MCP tools is what turns "an agent that can see a phone" into "an agent that can actually use one."&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;You can do everything above right now on a live demo device — no cables, no local setup, no simulators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://robotactions.com" rel="noopener noreferrer"&gt;Sign in with Google or GitHub and try it free →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>testing</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How We Built a Chat AI Agent Into Live Device Testing Sessions</title>
      <dc:creator>krish pavuluri</dc:creator>
      <pubDate>Tue, 10 Mar 2026 20:40:36 +0000</pubDate>
      <link>https://dev.to/krish_pavuluri/how-we-built-a-chat-ai-agent-into-live-device-testing-sessions-eg2</link>
      <guid>https://dev.to/krish_pavuluri/how-we-built-a-chat-ai-agent-into-live-device-testing-sessions-eg2</guid>
      <description>&lt;p&gt;We ship a cloud device farm — real Android and iOS devices you can control from a browser. Our users are mostly SDETs and QA engineers running Appium tests.&lt;/p&gt;

&lt;p&gt;The problem we kept hearing: &lt;strong&gt;finding the right locator wastes too much time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The typical workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a device session&lt;/li&gt;
&lt;li&gt;Notice an element on screen&lt;/li&gt;
&lt;li&gt;Switch to Appium Inspector&lt;/li&gt;
&lt;li&gt;Inspect the element tree&lt;/li&gt;
&lt;li&gt;Copy the locator&lt;/li&gt;
&lt;li&gt;Paste it into your test&lt;/li&gt;
&lt;li&gt;Run the test, fail, go back to step 3&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We wanted to collapse that loop. So we built a Chat AI Agent that lives inside the device session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;The agent can see the live device screen. You can ask it in plain English:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"What's the XPath for the equals button?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Give me a UIAutomator2 selector for the digit 7"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"What's the Accessibility ID of the login button?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it responds instantly with working locators — in whatever language you're using (Java, Python, Swift, Kotlin, WebDriverIO).&lt;/p&gt;

&lt;p&gt;No switching tools. No Appium Inspector. Just ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Screen visibility
&lt;/h3&gt;

&lt;p&gt;Our sessions already stream device screens via WebRTC. We grab frames from the stream at the point of the user's question — a single screenshot at query time. This keeps latency low and avoids sending a continuous video feed to the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  The model
&lt;/h3&gt;

&lt;p&gt;We send the screenshot + user message to a vision-capable LLM. The prompt is structured to return locators in a specific format — we parse the response and render it with syntax highlighting in the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locator formats
&lt;/h3&gt;

&lt;p&gt;We support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XPath&lt;/li&gt;
&lt;li&gt;CSS Selector&lt;/li&gt;
&lt;li&gt;UIAutomator2 (Android)&lt;/li&gt;
&lt;li&gt;XCUITest (iOS)&lt;/li&gt;
&lt;li&gt;Accessibility ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is instructed to return all applicable formats for the visible element, not just one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code output
&lt;/h3&gt;

&lt;p&gt;Users pick their language from a dropdown (Java, Python, Swift, Kotlin, WebDriverIO). We wrap the locator in idiomatic framework code for each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python / Appium
&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppiumBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;//android.widget.Button[@content-desc=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;equals&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Java / Appium&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;xpath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"//android.widget.Button[@content-desc='equals']"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  UI integration
&lt;/h3&gt;

&lt;p&gt;The panel sits alongside the device stream — it doesn't overlay the screen. Users can keep testing while asking questions. The conversation history stays within the session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;The hardest part wasn't the AI integration — it was the prompt engineering. Getting the model to return clean, parseable locator output (not prose with embedded code) required iteration.&lt;/p&gt;

&lt;p&gt;We also found that grounding the model on the &lt;em&gt;visible&lt;/em&gt; screen state (not a DOM or accessibility tree) made responses feel more natural. Users think in terms of what they &lt;em&gt;see&lt;/em&gt;, not what's in the XML hierarchy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The Chat AI Agent is live now in the &lt;a href="https://robotactions.com" rel="noopener noreferrer"&gt;RobotActions portal&lt;/a&gt;. Free trial available.&lt;/p&gt;

&lt;p&gt;We'd love feedback from anyone doing Appium or mobile automation — especially if you've built similar tooling. Drop a comment or reach out directly.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>automation</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
