appium

Activity

Stars and mentions · Last 30 days, Oct 6 – Nov 4

GitHub stars26
Mentions4
01234Oct 6Oct 20Nov 4
UTC days · repositories appium owns10 repositories

Top repositories

appium’s repositories ranked by pull request authors, as on trending.

Loading

Contributes to

Repositories outside appium’s account where appium opened or reviewed pull requests in the past 365 days.

Who noticed appium

Everyone who starred or forked appium’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

4posts
Nov 50 posts
  1. YouTube · · YouTube description · githubrepo_cloud · appium/appium

    🚀 [Fastest Growing GitHub Repos] appium: The Free Tool Taking Over GitHub! #shorts Discover appium/appium: Cross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol 💬 Comment "APPIUM" below for the direct link, or grab it from the links below: 👉 Interactive Star History & Live Analysis: githubrepo.cloud ⭐ Embed Live Verified Badges in your README: githubrepo.cloud 🔗 Direct GitHub Repository: appium/appium 📊 Repository Specs: ⭐ Stars: 22,141 🍴 Forks: 6,300 💻 Language: TypeScript 📜 License: Apache-2.0 #shorts #appium #android #automation #ios #webdev #javascript #typescript #reactjs

    95 views
  2. XPath to Android UiSelector: can Claude 5.5 replace a converter? TL;DR - UiSelector is faster than XPath on Android: it won all 30 benchmark cases. It saved 49 % per lookup at 100 elements and 12 % at 2000 (emulator). Below level 70 of the tree, XPath finds nothing at all. - 73 % of 1645 real XPath locators convert to a UiSelector. Every converted one ran on an emulator with 0 wrong results; the rest are skipped with a reason. Strict by default; --loose gives simpler, case-insensitive forms. - Claude 5.5 without rules: 47–63 wrong UiSelectors out of 680 (7–9 %). They look right and often pass a quick check. 65 of the 170 are textContains, which ignores case: wrong by a strict definition, and [open to debate](#is-textcontains-really-wrong). With a rules prompt, Opus and Sonnet made 1 and Haiku 4, and still no model beat the converter. npx xpath-to-uiselector ./tests # preview the diff npx xpath-to-uiselector ./tests --write # apply it Or paste your XPaths into the online converter. --- Contents 1. [Why leave XPath on Android](#why-leave-xpath-on-android) 2. [What a UiSelector is](#what-a-uiselector-is) 3. [Why converting by hand goes wrong](#why-converting-by-hand-goes-wrong) 4. [What the converter does](#what-the-converter-does) 5. [How I checked it](#how-i-checked-it) 6. [Claude 5.5 against the converter](#claude-55-against-the-converter) - [Is textContains really wrong?](#is-textcontains-really-wrong) 7. [Android is harder than iOS](#android-is-harder-than-ios) 8. [The future of UiSelector](#the-future-of-uiselector) 9. [What I would use](#what-i-would-use) 10. [Limits](#limits) --- This is the Android follow-up to my iOS posts. In part 1 I built a converter from XPath to iOS class chain; in part 2 I asked Claude to do the same job. On iOS the best model came within a few points of the converter. On Android I did both in one go: I built xpath-to-uiselector, then asked Claude Opus, Sonnet and Haiku 5.5 to convert the same locators. Android turned out to be much harder for the models, and the reason is in the API, not in the models. Why leave XPath on Android With the Appium UiAutomator2 driver, an XPath lookup first serializes the whole UI tree to XML and then runs the query on it. A UiSelector (the -android uiautomator strategy) walks the live tree and, for findElement, stops at the first match. I measured both in a separate benchmark: 30 cases (10 XPath shapes × 3 targets), each found by both strategies, on screens of 100 to 2000 elements. !Time per lookup against elements in the tree, and the deepest element each strategy finds - UiSelector won all 30 cases at every size. The saving in milliseconds grows with the tree (22 ms → 133 ms), the saving in percent shrinks (49 % → 12 %). - The depth limit matters more than the speed. The driver's snapshotMaxDepth is 70: below that, XPath and the page source see nothing. On a chain 100 levels deep, XPath found its last marker at level 66, and UiSelector found every marker down to level 100. - It is not 10–50× faster. Blog posts often say that, without showing a measurement. I measured 12–49 %: worth having on big screens and in long suites, but not a miracle. What a UiSelector is UiSelector is the androidx UiAutomator query builder. Appium takes it as a Java-like string and evaluates it on the device: | XPath | UiSelector | | ----------------------------------- | ------------------------------------------------------------------- | | //android.widget.Button | new UiSelector().className("android.widget.Button") | | //*[@text="Pay"] | new UiSelector().text("Pay") | | //*[@content-desc="Cart"] | new UiSelector().description("Cart") | | //*[@resource-id="com.app:id/ok"] | new UiSelector().resourceId("com.app:id/ok") | | //*[@checked="true"] | new UiSelector().checked(true) | | (//android.widget.EditText)[2] | new UiSelector().className("android.widget.EditText").instance(1) | It has no direct-child or parent step and no position among siblings: childSelector means any descendant, and fromParent searches among the siblings. Why converting by hand goes wrong The simple rows above are what everyone converts correctly. The trouble is the methods whose names promise the same thing as XPath but behave differently. All of these were checked on the emulator: - contains(@text,"Total") → textContains("Total")<br> textContains and textStartsWith ignore case: this also finds "TOTAL". Exact: textMatches("(?s).*Total.*"). - matches(@resource-id,".*\.ok") → resourceIdMatches(".*\\.ok") (in the locator text)<br> The server's string literal unescapes only \", so the regex gets \\., a literal backslash. It matches nothing. - (//ScrollView//ImageView)[3] → …childSelector(…ImageView.instance(2))<br> Inside childSelector, instance() counts per container, not across the screen. - //* → classNameMatches(".*")<br> With classNameMatches, findElements iterates index(i) and loses elements. Exact: new UiSelector(). - //ListView/TextView → …childSelector(…)<br> That is any descendant. UiSelector has no direct child, so there is no exact form. - ends-with(@text,"USD") → "skip: XPath 2.0"<br> The driver runs XPath 2.0 unless enforceXPath1 is set: matches(), ends-with(), lower-case() all work. The server accepts all of these: no syntax error, just other elements (or none). The first one is debatable (more on that [below](#is-textcontains-really-wrong)), but it changes what the locator finds without anyone noticing: the test passes today and taps another element when someone adds a "TOTAL" label above it. What the converter does xpath-to-uiselector scans your test files (Java, Kotlin, Python, JS/TS, C#, Ruby, JSON, YAML), finds the XPath strings and rewrites each one together with its strategy call: - AppiumBy.xpath("(//android.widget.TextView[@content-desc='price'])[2]") + AppiumBy.androidUIAutomator("new UiSelector().className(\"android.widget.TextView\").description(\"price\").instance(1)") - (AppiumBy.XPATH, '//*[@content-desc="Cart" and contains(@text, "USD")]') + (AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().description("Cart").textMatches("(?s).*USD.*")') [skip] UiSelector has no direct-child criterion: //android.widget.ListView/android.widget.TextView[1] Every XPath ends up in one of three groups: - Exact: the UiSelector finds the same elements in the same order. - Close: the same first element, but not always the same list, for example //A//B → childSelector. It comes with a warning, and --exact leaves these alone. - Skip: there is no exact UiSelector. Most skips come with a hint on how to rewrite the locator by hand. The default is strict: a converted locator finds the same elements as your XPath, so contains(@text,"Total") becomes the case-sensitive textMatches("(?s).*Total.*"). If you prefer simpler, more tolerant locators, --loose writes textContains / textStartsWith (they ignore case) and turns a direct child //A/B into childSelector. Each loose form is marked close with a warning. On the real locators, --loose converts 77 % instead of 73 %. The bare command only previews. --write saves, --json writes a report with every skip and warning. Node.js 20+, no dependencies. To try a single XPath without installing anything, use the online converter: it runs the same code in the browser. How I checked it The order was facts first, the converter last: 1. The source code: the UiAutomator2 server, androidx UiSelector and the XPath engine the driver uses, at pinned versions. That is where the case-insensitive Contains and the \"-only unescaping came from. 2. 170 edge cases (6 of them for --loose), each with a hand-written expected answer, ran on an Android emulator before the converter existed: XPath and UiSelector side by side, comparing the elements they find. Where the device disagreed with me, the case was corrected. 3. 1645 real XPath locators from 20 open-source Appium projects. Every converted one (1169 distinct XPaths) ran on random screens built from its own values and on screens built to break each condition. 0 wrong. 4. Checking the check: 1718 deliberately wrong UiSelectors; the screens caught 99.5 % of them. Android 16 emulator (and Android 17 for the edge cases), Appium 3.8.0, UiAutomator2 driver 8.7.0, default driver settings. The result: 73 % convert (64 % exact, 9 % close). Most of the rest have no exact UiSelector at all: direct children, absolute /hierarchy/… paths, sibling positions, web and iOS attributes. Claude 5.5 against the converter I used the same setup as in the iOS comparison: - 680 XPaths: 149 edge cases and 531 real locators from 19 projects, shuffled, with neutral ids. - One request per run: "convert each one to a UiSelector, or skip it with a reason". Claude Code CLI in an empty folder, Read and Write tools only, effort medium, one run per model. - The judge: every answer that differs from the converter's ran on the emulator next to the XPath. Scoring: a correct UiSelector earns 1 point, a close one 0.5, a skip 0, and a wrong one −1 (on iOS a wrong chain scored 0): it is worse than a skip, because it looks like a valid locator and finds another element. S is the score on the edge cases, R on the real-world locators, each out of 100 over the locators that have a UiSelector at all: 104 of 149 edge cases and 387 of 531 real ones. The converter scores S 94.2 / R 96.5, not 100, because for some locators it has only a close form, worth half a point. No model beat it on any locator. !S score against cost for three Claude 5.5 models, with and without the rules prompt, and a table with S, R, wrong answers, tokens, time and cost | Run | Correct | Close | Wrong | Skip | Missed | S | R | Min | Cost | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Converter | 452 | 39 | 0 | 189 | – | 94.2 | 96.5 | <&nbsp;0.1 | free | | Opus 5.5 | 400 | 32 | 47 | 179 | 22 | 56.7 | 80.1 | 12.2 | $3.00 | | Sonnet 5.5 | 401 | 32 | 60 | 176 | 11 | 43.8 | 80.5 | 4.9 | $0.95 | | Haiku 5.5 | 390 | 32 | 63 | 169 | 26 | 37.0 | 78.7 | 5.2 | $0.12 | "Missed" means the model skipped a locator that the converter converts correctly. Cost is the API list price the CLI reports. - On real locators the three models are close to each other (R 79–80), but 16 points below the converter, and each still wrote 31–32 wrong answers there. - Edge cases tell the models apart: S 37 to 57 against 94.2. - 7–9 % of all answers were wrong, and they look exactly like the correct ones. - Opus thought longest and was still wrong 47 times. Most of its 22 missed locators came from assuming that the driver runs XPath 1.0. What the models get wrong !170 wrong UiSelectors of the three runs without rules, by kind of mistake More than a third of all wrong answers are one pattern: textContains for contains(). Every model wrote it 20–23 times. The next group is a regex without (?s), where . stops at a line break; real resource ids have no line breaks, so those are mostly harmless. Then come the Java-style \\ (Sonnet alone: 15), values that exist only in the page source (an empty text, a toast, a slider's value), and positions inside childSelector. {% details Mistakes per model %} | Mistake | Opus | Sonnet | Haiku | | -------------------------------------------------------------------------------------------------------- | ------ | ------ | ------ | | Case-insensitive textContains / descriptionContains / textStartsWith | 20 | 23 | 22 | | Missing (?s) in a regex | 14 | 8 | 16 | | Java-style \\ in a regex or value | 1 | 15 | 2 | | Values only the page source has (@text="", a toast, a range widget's value) | 0 | 9 | 9 | | Positions: instance() in childSelector, //X[1] | 8 | 0 | 3 | | classNameMatches, (?i) for translate(), dropped existence tests, criteria UiSelector does not have | 4 | 5 | 11 | | Wrong | 47 | 60 | 63 | Each wrong answer is counted once, under the first mistake it shows. {% enddetails %} Is textContains really wrong? By the definition of this test, yes: contains(@text,"Total") does not find "TOTAL", and textContains("Total") does. The judge screens include such traps on purpose, so every one of the 65 answers found other elements than the XPath there. In a real app it is less clear. Many people who write contains(@text,"Total") probably never thought about case, and a case-insensitive locator survives a designer switching a label to upper case. The cost is the other side of the same thing: a migration of hundreds of locators quietly makes each one wider. Where a "TOTAL" header sits above the "Total: $5" line, findElement now returns the header. And a check that the label reads "Total" no longer catches a change to "TOTAL". That is why the converter is strict by default and --loose is a flag you turn on knowingly. In fact, 55 of the 65 answers are character for character what the converter writes with --loose; the other 10 are edge cases where --loose keeps the strict form (substring(), matches(), text(), a value with \). So without rules, Claude converts the way --loose does, only without saying that the locator became wider. If you count textContains as acceptable, the models made at least 27, 37 and 41 wrong answers without rules instead of 47, 60 and 63. Which side are you on? Tell me in the comments. A prompt with rules As on iOS, most of these mistakes are about knowing the driver, not about intelligence. So I wrote them down as general rules (about 8,000 characters, rules.md, no answers for particular locators) and ran the three models again: | Run | Correct | Close | Wrong | Skip | Missed | S | R | Min | Cost | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Converter | 452 | 39 | 0 | 189 | – | 94.2 | 96.5 | <&nbsp;0.1 | free | | Opus 5.5 + rules | 448 | 37 | 1 | 189 | 5 | 89.4 | 96.3 | 5.9 | $1.84 | | Sonnet 5.5 + rules | 448 | 37 | 1 | 189 | 5 | 89.4 | 96.3 | 4.1 | $0.89 | | Haiku 5.5 + rules | 443 | 37 | 4 | 189 | 7 | 87.5 | 94.7 | 6.3 | $0.33 | - Wrong answers dropped from 47–63 to 1–4, S rose from 37–57 to 88–89 and R from 79–80 to 95–96. The one left for Opus and Sonnet is a line-break case: string-length(@text)=5 → textMatches("(?s).{5}"), where ICU's . takes CR LF as one character. Haiku also dropped the className three times, against an explicit rule. - Opus got cheaper and faster with the rules: half the time, 60 % of the cost, a quarter of the thinking. - This is an upper bound. The rules were written from the mistakes on the same 680 locators, and every run was done once. What the models gave back The comparison also tested the converter. The first Opus run found one real converter bug: contains(@text,"1.5") was marked exact, but a slider at 1.5 has that text only in the page source, so the XPath finds the slider and the UiSelector does not. It is now a close match with a warning. Across the six runs, the models also found 8 locators the converter skipped although an exact UiSelector exists (normalize-space(), string-length(), translate(), if … then … else, and a few more). I checked each one on the emulator and added all 8 to the converter. After that, no model beats the converter on any locator. Android is harder than iOS On iOS, the same three Claude 5.5 models with a plain request made 4–7 wrong class chains out of 676. On Android they made 47–63 out of 680, roughly ten times as many. The gap shows on the real locators too: on iOS the models were within 2 points of the converter (R 94–95 against 96.0), on Android 16 points below it (R 79–80 against 96.5). Part of that is the scoring, since a wrong chain scored 0 on iOS and a wrong UiSelector costs 1 here: at 0, the Android R would be 87–89. The reason is the target language, not the models. Both converters are strict in the same way: a converted locator must find the same elements as the XPath. On iOS, the obvious answer is usually also the strict one. A class chain filter is an NSPredicate, and its string operators behave like XPath's: CONTAINS and BEGINSWITH are case-sensitive unless you add [c]. So [contains(@label,"Total")] becomes `[label CONTAINS "Total"]`, and that is exact. On Android, the obvious answer is often wider than the XPath. UiSelector has methods with XPath-like names and different behaviour: - textContains and textStartsWith ignore case. This one trap is 65 of the 170 wrong answers, and it has no iOS counterpart. - The string literal is not Java: the server unescapes only \", so a Java-style \\ stays two characters. - instance() inside childSelector counts per container, not across the screen. A model that "knows" UiSelector from Java code samples has seen these methods used, but rarely with the details that make them differ from XPath. So the case-insensitive, --loose-like answers are an Android problem: on iOS the same habit would produce the exact chain. Even if textContains counts as fine, the Android models made at least 27–41 wrong answers, about four to ten times the iOS count. The iOS mistakes were mostly of other kinds: raw line breaks inside a chain and positions such as [-1]. The two runs are not identical (iOS ran at low and medium effort, Android at medium; the locators are different), so take the ratio as a direction, not a measurement. The future of UiSelector Google plans to deprecate and remove UiSelector from UiAutomator, and the UiAutomator2 driver warns that every -android uiautomator locator will then stop working. No date or version is set: UiAutomator 2.4.0 (July 2026) still ships it, and the driver's server bundles 2.3.0. Appium has not announced a replacement yet (appium-uiautomator2-driver#812 is open). So converting today is a bet that UiSelector stays until Appium moves on; no date is set yet. XPath locators are not affected, and the converter only rewrites strings, so going back is a git revert. The repository has a monthly job that opens an issue when the driver changes a file the conversion rules depend on. What I would use The short answer: without rules, no Claude model is safe for a bulk migration; with rules, all three get close to the converter. But the rules are what building and device-checking the converter taught me. Without that work, there would be nothing to put in the prompt. 1. The converter first. It is free, offline, takes under a second for 680 locators, gives the same answer every time and made no wrong UiSelector. 2. A model only for what the converter skips, with the rules in the prompt. With the rules, Sonnet 5.5 at medium scored the same as Opus for half the cost. Most skips have no exact UiSelector at all, so expect an explanation rather than a conversion. 3. Never paste a model's UiSelector into a test without running it on the screen it is meant for. Without the rules, about one in twelve was wrong, and none of them looked wrong. 4. Pick the strictness you want. By default the converter writes exact conversions and close ones with a warning (//A//B → childSelector); --exact writes only the exact ones. --loose adds simpler, case-insensitive forms, also as close with a warning. That is roughly how Claude converts without rules (55 of its 65 textContains answers are the same as --loose), except that the converter tells you the locator became wider. If you use Claude Code, the repository has a skill that does exactly that: it runs the converter, then reviews each skipped locator and suggests a rewrite, marked as unverified. Limits - Emulator only. Android 16 (and 17 for part of the checks), AOSP and Google Play images on an M1 Pro. No real phone, no older Android versions, no OEM builds. - Default driver settings only. enforceXPath1, allowInvisibleElements and similar settings change the answers. - Speed was measured on one synthetic View-based app. Compose and WebView screens were not measured. - One run per model. Two runs with the same rules gave Haiku different mistakes. - The rules were fitted on the same 680 locators they are scored on. - Only Claude models. Other LLMs were not tested. Try it - Online converter: domovou69.github.io - npx xpath-to-uiselector ./tests (npm, GitHub) - The full LLM report: audit/llm/report.md - iOS counterpart: xpath-to-class-chain Acknowledgements Thanks to Anthropic: Claude Code helped me build the converter, the checks and the comparison, and the Claude models are also the subject of the test. All numbers come from real runs. --- _Mikhail Yesipchuk, SDET/Senior QA Automation Engineer. Have you moved Android locators off XPath, and what broke? Tell me on LinkedIn or open an issue on GitHub._

  3. You can't guard a value that doesn't exist yet --- title: "You can't guard a value that doesn't exist yet" published: true description: "How a year-old WebDriverAgent drag hang got fixed by finding where XCTest creates the INFINITY, not where WDA receives the input." tags: ios, testing, debugging, opensource cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com canonical_url: --- A drag request with four ordinary numbers in it: fromX: 10, fromY: 700, toX: 10, toY: 300. It works in the first app. Switch to a second app, send the same request, and the phone goes sluggish, the call hangs, and eventually WebDriverAgent returns: unknown error: Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY None of those numbers is infinite. That is appium/WebDriverAgent#1056, opened in September 2025 against WDA 9.15.3 on an iPhone 12 mini running iOS 18.5. The maintainers traced the error into Apple's own drag API and suggested reporting it to Apple or switching to W3C actions. The reporter filed it with Apple. Then it sat for a year. I fixed it in PR #1270. It merged on 29 September 2026 and shipped in appium-webdriveragent 16.13.0 the next day. This is how, starting with the first fix I wrote, which was wrong. {% github WebDriverAgent#1270 %} The first fix guarded the wrong place WDA builds gesture coordinates in FBCoordinateWithAnchorOffset. It normalizes the client's offset against the element's wdFrame and returns an XCUICoordinate. An infinite value in a coordinate points straight at that function, so I hardened it with isfinite checks on the frame and on the computed offset. It went nowhere, for three reasons: - The inputs in the issue are finite. The offset check never fires. - The frame was already guarded. wdFrame swaps an infinite frame for a sentinel rect (-1, -1, 0, 0), which the existing CGRectIsEmpty check rejects. My new check could only ever catch NaN. - The infinite point is created later, inside XCTest. A check that runs while the coordinate is being built cannot see a value that does not exist yet. I dropped it. The third reason turned out to be the whole bug. XCUICoordinate is a recipe, not a point An XCUICoordinate stores "this element, plus this normalized offset". It has no screen position until something reads screenPoint, and reading it means resolving the element through an accessibility snapshot. WDA handed XCTest two of these recipes and called: [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate]; Everything after that line is Apple's code, and the docs say nothing about what it does internally. So I disassembled XCUIAutomation.framework from Xcode 27.0 with otool -tV and followed the call chain. Inside _dispatchEvent:eventBuilder:, XCTest snapshots the app, waits for it to go idle, and only then resolves both points and hands them to its private event builder, XCUIPressHoldDragWithVelocityAndHoldEvent: !Sequence diagram of the old drag path. WebDriverAgent passes two lazy coordinates to XCTest. XCTest requests an accessibility snapshot from the busy app, gets a slow reply with no usable frame, and waits for idle, which is the hang. screenPoint then resolves to INFINITY, XCPointerEventPath asserts, and WebDriverAgent returns HTTP 500. testmanagerd is never reached. After an app switch, the new app's main thread is saturated. The snapshot request crawls, which is the hang. The snapshot comes back without a usable frame, screenPoint resolves to {INFINITY, INFINITY}, the event builder asserts, and WDA's exception handler turns the NSInternalInconsistencyException into a 500. WDA never saw a point. Both points were created and rejected inside a single XCTest call, after XCTest's own snapshot. There was no seam to validate them, time out sooner, or return a better error. Three ways out, and why I took the expensive one | Option | Fixes the hang | Fixes the crash | Cost | |---|---|---|---| | Catch XCTest's exception, return a friendly error | No | Cosmetically | Users still sit through the hang | | Pre-check screenPoint, then call XCTest | No | Partly | XCTest re-resolves later, so it is a race, and every drag pays an extra snapshot | | Resolve once, validate, synthesize the event directly | Yes | Yes, as a clean 400 | Must match XCTest's gesture timing exactly | The first two are cheap and both keep the hang, which is the part users actually feel. The third has one real risk: timing drift from XCTest. Disassembly removed that risk, because I could copy Apple's builder line for line instead of approximating it. The fix A new method on XCUIApplication, fb_pressAtCoordinate:forDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:error:. Both /wda/dragfromtoforduration and /wda/pressAndDragWithVelocity now go through it. !Sequence diagram of the new drag path. WebDriverAgent resolves both screen points once through XCTest, which takes one snapshot. If either point is not finite, WebDriverAgent returns HTTP 400 immediately and sends nothing. Otherwise it builds the event with XCTest's exact touch path and the start element's display ID, sends it straight to testmanagerd with no re-snapshot or idle wait, and returns HTTP 200. The core, trimmed from the merged code: // Resolved once, here, so the points validated are exactly the points sent. CGPoint startPoint = startCoordinate.screenPoint; CGPoint endPoint = endCoordinate.screenPoint; if (!FBIsFinitePoint(startPoint) || !FBIsFinitePoint(endPoint)) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"Cannot resolve the on-screen position of the drag gesture ..."] buildError:error]; } And the event builder, mirroring XCTest's private one: XCPointerEventPath *path = [[XCPointerEventPath alloc] initForTouchAtPoint:start offset:0]; if (pressDuration > 0) { [path moveToPoint:start atOffset:pressDuration]; // hold still } CGFloat distance = (CGFloat)hypot(end.x - start.x, end.y - start.y); NSTimeInterval dragEnd = pressDuration + distance / pointsPerSecond; [path moveToPoint:end atOffset:dragEnd]; // drag [path liftUpAtOffset:dragEnd + holdDuration]; // hold, lift The event goes out through FBXCTestDaemonsProxy, the same path W3C actions use. No second snapshot, no idle wait before the gesture. After the gesture it waits up to animationCoolOffTimeout, again matching W3C actions. Two XCTest details found along the way shaped the code: - The velocity constants are not speeds. XCUIGestureVelocityDefault, Slow and Fast are sentinels: -10, -20, -30. XCTest maps them through an exported but undeclared function, XCUIPixelsPerSecondForDragGestureVelocity, to 500, 250 and 750. The fix looks that function up at runtime with dlsym, so a future Xcode change is picked up automatically, falls back to the measured values if it disappears, and logs once when it does. - "Pixels per second" is points per second. XCTest divides a distance measured in points by that value, with no scale factor. What changed for clients: | Situation | Before | After | |---|---|---| | Busy app after a switch | Long hang, then 500 unknown error | Fast 400 invalid element state telling you the app was not idle, or the drag succeeds if the single resolution works | | velocity: 0 | 500 unknown error, raised inside XCTest | 400 invalid argument | | Negative duration | Passed straight through to XCTest | 400 invalid argument | | Normal drag | Works | Works | Making the test prove something The simulator cannot reproduce a busy third-party app, so I built a stand-in for the broken state: @interface FBUnresolvableCoordinate : XCUICoordinate @end @implementation FBUnresolvableCoordinate - (CGPoint)screenPoint { return CGPointMake(INFINITY, INFINITY); } @end A fake is only worth something if it breaks the old code the same way production did. So before writing the real test, I ran a throwaway one that fed this coordinate to the old XCTest API. It threw NSInternalInconsistencyException with the exact message from #1056. Only then did the fake become the regression test: the same coordinate through the new method returns NO, the expected error, and the list on screen has not moved, so no gesture was sent. Four tests ended up in FBPressAndDragTests: - A real drag between app coordinates scrolls the list. - An unresolvable coordinate fails cleanly and sends nothing. - A velocity of 0 throws invalid argument. - The event builder keeps a non-default display ID and gets the timing right. This one came out of review. On top of that: builds for iOS, tvOS and watchOS, and a manual curl replay of the issue on the simulator. Drag in Settings, go to the home screen, launch Photos, drag again. Both drags succeeded, and invalid velocity and duration returned 400. Review: two maintainers, five comments Round one, mykola-mokhnach, four comments. All code hygiene, in a codebase that builds with warnings as errors: - No float equality comparisons. A <= 0 check on a CGFloat had to go. - Throw a proper argument error where the value enters, instead of returning a 0 sentinel and relying on XCTest to complain. - Compute the preset comparisons once instead of twice. - Log when the dlsym lookup fails, so a renamed symbol does not silently change drag speeds. Fixed in one commit, replied per comment, approved the same day. Round two, KazuCocoa, one comment, and the one that mattered. XCTest's builder passes the start element's display ID into the event record. My version used the W3C path's initializer, which means display 0. On a secondary display, the gesture would land on the wrong screen. He had confirmed the difference by inspecting XCTest himself. The same release added multi-display screenshot support (#1267), so this would have been a real regression, not a theoretical one. The fix kept the "resolve once" property intact. Disassembly showed -[XCUIElement displayID] just reads lastSnapshot.displayID without taking a new snapshot, and screenPoint had refreshed that snapshot a moment earlier. To test it without a second display, I pulled the event-record construction into its own class method and asserted on its output: !Gesture timeline used by the test. Press for 0.3 seconds, drag 400 points at 500 points per second for 0.8 seconds, hold for 0.1 seconds, lift at 1.2 seconds. The test asserts displayID equals 42, one event path, and maximumOffset equals 1.2. Approved and merged on 29 September. Six red CI jobs, none of them mine, each one proven The final CI run: 51 jobs passed, 6 failed. From a first-time contributor, "unrelated failures" is the least credible sentence in open source, so I read every log before saying it: - Functional tests, Xcode 16.4. The simulator needed four boot attempts, the runner never answered /status, and the job was killed at the timeout. The same job passed on my first commit. - tvOS build and iPhone unit tests on the minimum Xcode. No device matched the destination specifier, exit 70, before any code ran. - One integration job on the maximum Xcode. The runner never bootstrapped. Zero tests ran. - Two int_test_3 jobs. Eight failures each, all in FBVoiceOverTests, which fail intermittently on unrelated branches too. Every new test passed wherever the runner started, including Xcode 16.4 on iOS 18.5, the reporter's OS version. What I did not test I said this in the PR and it belongs here too: - The reporter's exact case. A busy third-party app on a real iOS 18.5 device. The fake coordinate covers it synthetically, not on hardware. - A real secondary display. Covered by the builder test only. - originalOffset. XCTest also sets it on the event record. The fix does not. No observed difference; still an open question. And one known gap: /wda/element/:uuid/pressAndDragWithVelocity, the element-to-element variant, still calls XCTest directly and very likely has the same weakness. That is the next PR. What carries over None of this is specific to iOS. 1. Find where the bad value is created before you guard against it. Validation at the edge of your code is useless when the value is born inside a dependency. 2. Validate the thing you actually send. Resolve once, check it, send exactly that. Anything else is a race with a nicer error message. 3. A fake proves nothing until it fails the old code the way production did. Reproduce the exact error first, then trust the test. 4. When the docs stop, read the binary. A maintainer and I both settled design questions by disassembling XCTest instead of arguing about what it probably does. Most of my work is mainframe-to-cloud migration testing, where rule 2 is the entire job: compare what the new system actually emits, not what you assume it emits. Same discipline, different stack. --- - PR: appium/WebDriverAgent#1270 - Issue: appium/WebDriverAgent#1056 - Release: v16.13.0

  4. @morixidong 赶紧去试试,就是可能需要 Xcode 装下 appium/WebDriverAgent

    2 likes · 590 views

Achievements

Awards earned by the repositories appium owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.