From 6aaf7550e644bd344c049bb5eae7c8259bf22ee9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:45:35 -0700 Subject: [PATCH 01/55] ci(deps): bump nick-fields/retry from 3 to 4 (#1122) Bumps [nick-fields/retry](https://github.com/nick-fields/retry) from 3 to 4. - [Release notes](https://github.com/nick-fields/retry/releases) - [Commits](https://github.com/nick-fields/retry/compare/v3...v4) --- updated-dependencies: - dependency-name: nick-fields/retry dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.js.yml b/.github/workflows/publish.js.yml index 29d2a11a6..69b619f19 100644 --- a/.github/workflows/publish.js.yml +++ b/.github/workflows/publish.js.yml @@ -54,7 +54,7 @@ jobs: with: xcode-version: "${{ env.XCODE_VERSION }}" - name: ${{ matrix.config.name }} - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_minutes: 10 max_attempts: 3 From dd15f48d33edabef6aea8ac951cf539946b492f2 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 12 Apr 2026 09:57:11 +0200 Subject: [PATCH 02/55] fix: Avoid keeping strong reference to self instance in delegates (#1123) --- WebDriverAgentLib/Routing/FBTCPSocket.h | 6 +- WebDriverAgentLib/Routing/FBTCPSocket.m | 22 ++++-- WebDriverAgentLib/Routing/FBWebServer.m | 39 ++++++++-- .../Utilities/FBImageProcessor.m | 55 +++++++++----- WebDriverAgentLib/Utilities/FBMjpegServer.h | 5 ++ WebDriverAgentLib/Utilities/FBMjpegServer.m | 75 ++++++++++++++++--- lib/xcodebuild.ts | 8 +- 7 files changed, 165 insertions(+), 45 deletions(-) diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.h b/WebDriverAgentLib/Routing/FBTCPSocket.h index 31adc7e22..72947e2a0 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.h +++ b/WebDriverAgentLib/Routing/FBTCPSocket.h @@ -38,7 +38,11 @@ NS_ASSUME_NONNULL_BEGIN @interface FBTCPSocket : NSObject -@property (nullable, nonatomic) id delegate; +#if __has_feature(objc_arc_weak) +@property (nullable, nonatomic, weak) id delegate; +#else +@property (nullable, nonatomic, assign) id delegate; +#endif /** Creates TCP socket isntance which is going to be started on the specified port diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.m b/WebDriverAgentLib/Routing/FBTCPSocket.m index fabd22074..25a286ddd 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.m +++ b/WebDriverAgentLib/Routing/FBTCPSocket.m @@ -48,11 +48,14 @@ - (BOOL)startWithError:(NSError **)error - (void)stop { @synchronized(self.connectedClients) { - for (NSUInteger i = 0; i < [self.connectedClients count]; i++) { - [[self.connectedClients objectAtIndex:i] disconnect]; + NSArray *clients = self.connectedClients.copy; + [self.connectedClients removeAllObjects]; + for (GCDAsyncSocket *client in clients) { + [client disconnect]; } } + self.delegate = nil; [self.listeningSocket disconnect]; } @@ -66,12 +69,18 @@ - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSo @synchronized(self.connectedClients) { [self.connectedClients addObject:newSocket]; } - [self.delegate didClientConnect:newSocket]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientConnect:newSocket]; + } } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { - [self.delegate didClientSendData:sock]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientSendData:sock]; + } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err @@ -79,7 +88,10 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err @synchronized(self.connectedClients) { [self.connectedClients removeObject:sock]; } - [self.delegate didClientDisconnect:sock]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientDisconnect:sock]; + } } @end diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index 0b82eaff5..29a2e16e6 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -47,10 +47,16 @@ @interface FBWebServer () @property (nonatomic, strong) RoutingHTTPServer *server; @property (atomic, assign) BOOL keepAlive; @property (nonatomic, nullable) FBTCPSocket *screenshotsBroadcaster; +@property (nonatomic, nullable, strong) FBMjpegServer *mjpegServer; @end @implementation FBWebServer +- (void)dealloc +{ + [self stopScreenshotsBroadcaster]; +} + + (NSArray> *)collectCommandHandlerClasses { NSArray *handlersClasses = FBClassesThatConformsToProtocol(@protocol(FBCommandHandler)); @@ -97,7 +103,7 @@ - (void)startHTTPServer [self.server setInterface:bindingIP]; [FBLogger logFmt:@"Using custom binding IP address: %@", bindingIP]; } - + NSError *error; BOOL serverStarted = NO; @@ -117,7 +123,7 @@ - (void)startHTTPServer [FBLogger logFmt:@"Last attempt to start web server failed with error %@", [error description]]; abort(); } - + NSString *serverHost = bindingIP ?: ([XCUIDevice sharedDevice].fb_wifiIPAddress ?: @"127.0.0.1"); [FBLogger logFmt:@"%@http://%@:%d%@", FBServerURLBeginMarker, serverHost, [self.server port], FBServerURLEndMarker]; } @@ -125,12 +131,15 @@ - (void)startHTTPServer - (void)initScreenshotsBroadcaster { [self readMjpegSettingsFromEnv]; + self.mjpegServer = [[FBMjpegServer alloc] init]; self.screenshotsBroadcaster = [[FBTCPSocket alloc] initWithPort:(uint16_t)FBConfiguration.mjpegServerPort]; - self.screenshotsBroadcaster.delegate = [[FBMjpegServer alloc] init]; + self.screenshotsBroadcaster.delegate = self.mjpegServer; NSError *error; if (![self.screenshotsBroadcaster startWithError:&error]) { [FBLogger logFmt:@"Cannot init screenshots broadcaster service on port %@. Original error: %@", @(FBConfiguration.mjpegServerPort), error.description]; + [self.mjpegServer stopStreaming]; + self.mjpegServer = nil; self.screenshotsBroadcaster = nil; } } @@ -138,10 +147,18 @@ - (void)initScreenshotsBroadcaster - (void)stopScreenshotsBroadcaster { if (nil == self.screenshotsBroadcaster) { + self.mjpegServer = nil; return; } + id delegate = self.screenshotsBroadcaster.delegate; + if ([(NSObject *)delegate respondsToSelector:@selector(stopStreaming)]) { + [(FBMjpegServer *)delegate stopStreaming]; + } + self.screenshotsBroadcaster.delegate = nil; [self.screenshotsBroadcaster stop]; + self.screenshotsBroadcaster = nil; + self.mjpegServer = nil; } - (void)readMjpegSettingsFromEnv @@ -164,6 +181,8 @@ - (void)stopServing if (self.server.isRunning) { [self.server stop:NO]; } + self.server = nil; + self.exceptionHandler = nil; self.keepAlive = NO; } @@ -192,10 +211,15 @@ - (BOOL)attemptToStartServer:(RoutingHTTPServer *)server onPort:(NSInteger)port - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses { + __weak typeof(self) weakSelf = self; for (Class commandHandler in commandHandlerClasses) { NSArray *routes = [commandHandler routes]; for (FBRoute *route in routes) { [self.server handleMethod:route.verb withPath:route.path block:^(RouteRequest *request, RouteResponse *response) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } NSDictionary *arguments = [NSJSONSerialization JSONObjectWithData:request.body options:NSJSONReadingMutableContainers error:NULL]; FBRouteRequest *routeParams = [FBRouteRequest routeRequestWithURL:request.url @@ -209,7 +233,7 @@ - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses [route mountRequest:routeParams intoResponse:response]; } @catch (NSException *exception) { - [self handleException:exception forResponse:response]; + [strongSelf handleException:exception forResponse:response]; } }]; } @@ -237,9 +261,14 @@ - (void)registerServerKeyRouteHandlers [response respondWithString:calibrationPage]; }]; + __weak typeof(self) weakSelf = self; [self.server get:@"/wda/shutdown" withBlock:^(RouteRequest *request, RouteResponse *response) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } [response respondWithString:@"Shutting down"]; - [self.delegate webServerDidRequestShutdown:self]; + [strongSelf.delegate webServerDidRequestShutdown:strongSelf]; }]; [self registerRouteHandlers:@[FBUnknownCommands.class]]; diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgentLib/Utilities/FBImageProcessor.m index 7d2ada759..951568a8e 100644 --- a/WebDriverAgentLib/Utilities/FBImageProcessor.m +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -27,6 +27,7 @@ @interface FBImageProcessor () @property (nonatomic) NSData *nextImage; @property (nonatomic, readonly) NSLock *nextImageLock; @property (nonatomic, readonly) dispatch_queue_t scalingQueue; +@property (atomic, assign) BOOL isScalingScheduled; @end @@ -38,6 +39,7 @@ - (id)init if (self) { _nextImageLock = [[NSLock alloc] init]; _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL); + _isScalingScheduled = NO; } return self; } @@ -51,32 +53,45 @@ - (void)submitImageData:(NSData *)image [FBLogger verboseLog:@"Discarding screenshot"]; } self.nextImage = image; + BOOL shouldSchedule = !self.isScalingScheduled; + if (shouldSchedule) { + self.isScalingScheduled = YES; + } [self.nextImageLock unlock]; + if (!shouldSchedule) { + return; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wcompletion-handler" dispatch_async(self.scalingQueue, ^{ - [self.nextImageLock lock]; - NSData *nextImageData = self.nextImage; - self.nextImage = nil; - [self.nextImageLock unlock]; - if (nextImageData == nil) { - return; - } + while (YES) { + @autoreleasepool { + [self.nextImageLock lock]; + NSData *nextImageData = self.nextImage; + self.nextImage = nil; + if (nextImageData == nil) { + self.isScalingScheduled = NO; + [self.nextImageLock unlock]; + return; + } + [self.nextImageLock unlock]; - // We do not want this value to be too high because then we get images larger in size than original ones - // Although, we also don't want to lose too much of the quality on recompression - CGFloat recompressionQuality = MAX(0.9, - MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); - NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData - scalingFactor:scalingFactor - uti:UTTypeJPEG - compressionQuality:recompressionQuality - // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata - // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 - fixOrientation:FBConfiguration.mjpegShouldFixOrientation - desiredOrientation:nil]; - completionHandler(thumbnailData ?: nextImageData); + // We do not want this value to be too high because then we get images larger in size than original ones + // Although, we also don't want to lose too much of the quality on recompression + CGFloat recompressionQuality = MAX(0.9, + MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData + scalingFactor:scalingFactor + uti:UTTypeJPEG + compressionQuality:recompressionQuality + // iOS always returns screenshots in portrait orientation, but puts the real value into the metadata + // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 + fixOrientation:FBConfiguration.mjpegShouldFixOrientation + desiredOrientation:nil]; + completionHandler(thumbnailData ?: nextImageData); + } + } }); #pragma clang diagnostic pop } diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.h b/WebDriverAgentLib/Utilities/FBMjpegServer.h index 294c399f8..a9b47cada 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.h +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.h @@ -19,6 +19,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)init; +/** + Stops screenshot broadcasting and prevents future scheduling. + */ +- (void)stopStreaming; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index 4f061d2e3..75389df50 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -27,6 +27,11 @@ static NSString *const SERVER_NAME = @"WDA MJPEG Server"; static const char *QUEUE_NAME = "JPEG Screenshots Provider Queue"; +static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate) +{ + return (0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate; +} + @interface FBMjpegServer() @@ -35,6 +40,9 @@ @interface FBMjpegServer() @property (nonatomic, readonly) FBImageProcessor *imageProcessor; @property (nonatomic, readonly) long long mainScreenID; @property (nonatomic, assign) NSUInteger consecutiveScreenshotFailures; +@property (atomic, assign) BOOL isStreaming; +@property (nonatomic, assign) NSUInteger sentFramesCount; +@property (nonatomic, assign) NSUInteger sentBytesCount; @end @@ -45,38 +53,49 @@ - (instancetype)init { if ((self = [super init])) { _consecutiveScreenshotFailures = 0; + _isStreaming = YES; + _sentFramesCount = 0; + _sentBytesCount = 0; _listeningClients = [NSMutableArray array]; + _imageProcessor = [[FBImageProcessor alloc] init]; + _mainScreenID = [XCUIScreen.mainScreen displayID]; dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0); _backgroundQueue = dispatch_queue_create(QUEUE_NAME, queueAttributes); + __weak typeof(self) weakSelf = self; dispatch_async(_backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); - _imageProcessor = [[FBImageProcessor alloc] init]; - _mainScreenID = [XCUIScreen.mainScreen displayID]; } return self; } - (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:(uint64_t)timeStarted { + if (!self.isStreaming) { + return; + } uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted; - int64_t nextTickDelta = timerInterval - timeElapsed; + int64_t nextTickDelta = (int64_t)timerInterval - (int64_t)timeElapsed; + __weak typeof(self) weakSelf = self; if (nextTickDelta > 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); } else { // Try to do our best to keep the FPS at a decent level dispatch_async(self.backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); } } - (void)streamScreenshot { - NSUInteger framerate = FBConfiguration.mjpegServerFramerate; - uint64_t timerInterval = (uint64_t)(1.0 / ((0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate) * NSEC_PER_SEC); + if (!self.isStreaming) { + return; + } + NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.mjpegServerFramerate); + uint64_t timerInterval = (uint64_t)(1.0 / framerate * NSEC_PER_SEC); uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); @synchronized (self.listeningClients) { if (0 == self.listeningClients.count) { @@ -106,23 +125,41 @@ - (void)streamScreenshot self.consecutiveScreenshotFailures = 0; CGFloat scalingFactor = FBConfiguration.mjpegScalingFactor / 100.0; + __weak typeof(self) weakSelf = self; [self.imageProcessor submitImageData:screenshotData scalingFactor:scalingFactor completionHandler:^(NSData * _Nonnull scaled) { - [self sendScreenshot:scaled]; + [weakSelf sendScreenshot:scaled]; }]; [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; } - (void)sendScreenshot:(NSData *)screenshotData { + if (!self.isStreaming) { + return; + } NSString *chunkHeader = [NSString stringWithFormat:@"--BoundaryString\r\nContent-type: image/jpeg\r\nContent-Length: %@\r\n\r\n", @(screenshotData.length)]; NSMutableData *chunk = [[chunkHeader dataUsingEncoding:NSUTF8StringEncoding] mutableCopy]; [chunk appendData:screenshotData]; [chunk appendData:(id)[@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; @synchronized (self.listeningClients) { + if (!self.isStreaming || 0 == self.listeningClients.count) { + return; + } + NSUInteger clientCount = self.listeningClients.count; for (GCDAsyncSocket *client in self.listeningClients) { - [client writeData:chunk withTimeout:-1 tag:0]; + // Slow clients should fail/close instead of buffering indefinitely. + [client writeData:chunk withTimeout:FRAME_TIMEOUT tag:0]; + } + self.sentFramesCount++; + self.sentBytesCount += chunk.length * clientCount; + NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.mjpegServerFramerate); + if (0 == self.sentFramesCount % framerate) { + [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@", + @(clientCount), + @(self.sentFramesCount), + @(self.sentBytesCount)]]; } } } @@ -158,4 +195,22 @@ - (void)didClientDisconnect:(GCDAsyncSocket *)client [FBLogger log:@"Disconnected a client from screenshots broadcast"]; } +- (void)stopStreaming +{ + self.isStreaming = NO; + @synchronized (self.listeningClients) { + NSArray *clients = self.listeningClients.copy; + [self.listeningClients removeAllObjects]; + for (GCDAsyncSocket *client in clients) { + [client disconnect]; + } + } +} + +- (void)dealloc +{ + [self stopStreaming]; + [FBLogger verboseLog:@"FBMjpegServer deallocated"]; +} + @end diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index f21289906..754c87943 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -461,18 +461,18 @@ export class XcodeBuild { } const proxyTimeout = noSessionProxy.timeout; - noSessionProxy.timeout = 1000; + (noSessionProxy as any).timeout = 1000; try { currentStatus = (await noSessionProxy.command('/status', 'GET')) as StringRecord; - if (currentStatus && currentStatus.ios && (currentStatus.ios as any).ip) { - this.agentUrl = (currentStatus.ios as any).ip; + if (currentStatus?.ios?.ip) { + this.agentUrl = currentStatus.ios.ip as string; } this.log.debug(`WebDriverAgent information:`); this.log.debug(JSON.stringify(currentStatus, null, 2)); } catch (err: any) { throw new Error(`Unable to connect to running WebDriverAgent: ${err.message}`); } finally { - noSessionProxy.timeout = proxyTimeout; + (noSessionProxy as any).timeout = proxyTimeout; } }); From 71e56598b7052c1047ccfb4ef5f78440850c5eab Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 12 Apr 2026 08:01:27 +0000 Subject: [PATCH 03/55] chore(release): 11.4.2 [skip ci] ## [11.4.2](https://github.com/appium/WebDriverAgent/compare/v11.4.1...v11.4.2) (2026-04-12) ### Bug Fixes * Avoid keeping strong reference to self instance in delegates ([#1123](https://github.com/appium/WebDriverAgent/issues/1123)) ([dd15f48](https://github.com/appium/WebDriverAgent/commit/dd15f48d33edabef6aea8ac951cf539946b492f2)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 853c13024..da302b8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [11.4.2](https://github.com/appium/WebDriverAgent/compare/v11.4.1...v11.4.2) (2026-04-12) + +### Bug Fixes + +* Avoid keeping strong reference to self instance in delegates ([#1123](https://github.com/appium/WebDriverAgent/issues/1123)) ([dd15f48](https://github.com/appium/WebDriverAgent/commit/dd15f48d33edabef6aea8ac951cf539946b492f2)) + ## [11.4.1](https://github.com/appium/WebDriverAgent/compare/v11.4.0...v11.4.1) (2026-03-15) ### Bug Fixes diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 282c2aefd..c244b81b2 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 11.4.1 + 11.4.2 CFBundleSignature ???? CFBundleVersion - 11.4.1 + 11.4.2 NSPrincipalClass diff --git a/package.json b/package.json index 2e43649a3..c2ca4ae69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "11.4.1", + "version": "11.4.2", "description": "Package bundling WebDriverAgent", "main": "./build/index.js", "types": "./build/index.d.ts", From 046b08042df33f507466f55b68b444c91684931a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:05:30 +0200 Subject: [PATCH 04/55] chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 (#1121) * chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2) --- updated-dependencies: - dependency-name: typescript dependency-version: 6.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * moar * format --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mykola Mokhnach --- index.ts | 7 ------- lib/index.ts | 7 +++++++ package.json | 12 +++++------- tsconfig.json | 8 ++++++-- 4 files changed, 18 insertions(+), 16 deletions(-) delete mode 100644 index.ts create mode 100644 lib/index.ts diff --git a/index.ts b/index.ts deleted file mode 100644 index 8a28be3e2..000000000 --- a/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { bundleWDASim } from './lib/check-dependencies'; -export { NoSessionProxy } from './lib/no-session-proxy'; -export { WebDriverAgent } from './lib/webdriveragent'; -export { WDA_BASE_URL, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE } from './lib/constants'; -export { resetTestProcesses, BOOTSTRAP_PATH } from './lib/utils'; - -export * from './lib/types'; diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 000000000..4d78ec618 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,7 @@ +export {bundleWDASim} from './check-dependencies'; +export {NoSessionProxy} from './no-session-proxy'; +export {WebDriverAgent} from './webdriveragent'; +export {WDA_BASE_URL, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE} from './constants'; +export {resetTestProcesses, BOOTSTRAP_PATH} from './utils'; + +export * from './types'; diff --git a/package.json b/package.json index c2ca4ae69..d019c67f7 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "appium-webdriveragent", "version": "11.4.2", "description": "Package bundling WebDriverAgent", - "main": "./build/index.js", - "types": "./build/index.d.ts", + "main": "./build/lib/index.js", + "types": "./build/lib/index.d.ts", "scripts": { "build": "tsc -b", "dev": "npm run build -- --watch", @@ -69,7 +69,7 @@ "semver": "^7.3.7", "sinon": "^21.0.0", "ts-node": "^10.9.1", - "typescript": "^5.4.2" + "typescript": "^6.0.2" }, "dependencies": { "@appium/base-driver": "^10.0.0-rc.1", @@ -85,9 +85,8 @@ "teen_process": "^4.0.7" }, "files": [ - "index.ts", "lib", - "build", + "build/lib", "Scripts/build.sh", "Scripts/*.mjs", "Configurations", @@ -97,7 +96,6 @@ "WebDriverAgentRunner", "WebDriverAgentTests", "XCTWebDriverAgentLib", - "CHANGELOG.md", - "!build/test" + "CHANGELOG.md" ] } diff --git a/tsconfig.json b/tsconfig.json index 0db4e3637..2c2b4cc0b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,14 +2,18 @@ "$schema": "https://json.schemastore.org/tsconfig", "extends": "@appium/tsconfig/tsconfig.json", "compilerOptions": { - "strict": false, // TODO: make this flag true "esModuleInterop": true, "outDir": "build", "types": ["node", "mocha"], "checkJs": true }, + "ts-node": { + "transpileOnly": true, + "compilerOptions": { + "rootDir": "." + } + }, "include": [ - "index.ts", "lib", "test" ] From 0924871e9668883259825d12cf0d297e9c3546cc Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 12 Apr 2026 08:18:29 +0000 Subject: [PATCH 05/55] chore(release): 11.4.3 [skip ci] ## [11.4.3](https://github.com/appium/WebDriverAgent/compare/v11.4.2...v11.4.3) (2026-04-12) ### Miscellaneous Chores * **deps-dev:** bump typescript from 5.9.3 to 6.0.2 ([#1121](https://github.com/appium/WebDriverAgent/issues/1121)) ([046b080](https://github.com/appium/WebDriverAgent/commit/046b08042df33f507466f55b68b444c91684931a)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da302b8a5..5e0813ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [11.4.3](https://github.com/appium/WebDriverAgent/compare/v11.4.2...v11.4.3) (2026-04-12) + +### Miscellaneous Chores + +* **deps-dev:** bump typescript from 5.9.3 to 6.0.2 ([#1121](https://github.com/appium/WebDriverAgent/issues/1121)) ([046b080](https://github.com/appium/WebDriverAgent/commit/046b08042df33f507466f55b68b444c91684931a)) + ## [11.4.2](https://github.com/appium/WebDriverAgent/compare/v11.4.1...v11.4.2) (2026-04-12) ### Bug Fixes diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index c244b81b2..f28826781 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 11.4.2 + 11.4.3 CFBundleSignature ???? CFBundleVersion - 11.4.2 + 11.4.3 NSPrincipalClass diff --git a/package.json b/package.json index d019c67f7..e66c9b673 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "11.4.2", + "version": "11.4.3", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 5072e255faa3538f5ff4c8769bf16fd290ee8af9 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 14 Apr 2026 20:30:57 +0200 Subject: [PATCH 06/55] refactor: remove deprecated WDA settings/capabilities and idb typing (#1124) BREAKING CHANGE: remove idb from AppleDevice; clients must stop passing device.idb. BREAKING CHANGE: remove includeNonModalElements WDA setting; clients must stop sending this setting in /settings. BREAKING CHANGE: remove shouldUseTestManagerForVisibilityDetection capability; clients must stop sending this desired capability. --- .../Commands/FBSessionCommands.m | 9 ----- WebDriverAgentLib/Utilities/FBCapabilities.h | 2 -- WebDriverAgentLib/Utilities/FBCapabilities.m | 1 - WebDriverAgentLib/Utilities/FBConfiguration.h | 15 -------- WebDriverAgentLib/Utilities/FBConfiguration.m | 25 ------------- WebDriverAgentLib/Utilities/FBSettings.h | 1 - WebDriverAgentLib/Utilities/FBSettings.m | 1 - .../Utilities/FBXCodeCompatibility.h | 9 +---- .../Utilities/FBXCodeCompatibility.m | 14 +------- .../Vendor/CocoaAsyncSocket/GCDAsyncSocket.m | 36 +++---------------- lib/types.ts | 4 --- lib/utils.ts | 2 +- test/unit/webdriveragent-specs.ts | 1 - 13 files changed, 8 insertions(+), 112 deletions(-) diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m index c70465073..0522633fd 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.m +++ b/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -101,7 +101,6 @@ + (NSArray *)routes } [FBConfiguration resetSessionSettings]; - [FBConfiguration setShouldUseTestManagerForVisibilityDetection:[capabilities[FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION] boolValue]]; if (capabilities[FB_SETTING_USE_COMPACT_RESPONSES]) { [FBConfiguration setShouldUseCompactResponses:[capabilities[FB_SETTING_USE_COMPACT_RESPONSES] boolValue]]; } @@ -345,7 +344,6 @@ + (NSArray *)routes FB_SETTING_REDUCE_MOTION: @([FBConfiguration reduceMotionEnabled]), FB_SETTING_DEFAULT_ACTIVE_APPLICATION: request.session.defaultActiveApplication, FB_SETTING_ACTIVE_APP_DETECTION_POINT: FBActiveAppDetectionPoint.sharedInstance.stringCoordinates, - FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS: @([FBConfiguration includeNonModalElements]), FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR: FBConfiguration.acceptAlertButtonSelector, FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR: FBConfiguration.dismissAlertButtonSelector, FB_SETTING_AUTO_CLICK_ALERT_SELECTOR: FBConfiguration.autoClickAlertSelector, @@ -428,13 +426,6 @@ + (NSArray *)routes traceback:nil]); } } - if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS]) { - if ([XCUIElement fb_supportsNonModalElementsInclusion]) { - [FBConfiguration setIncludeNonModalElements:[[settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS] boolValue]]; - } else { - [FBLogger logFmt:@"'%@' settings value cannot be assigned, because non modal elements inclusion is not supported by the current iOS SDK", FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS]; - } - } if (nil != [settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]) { [FBConfiguration setAcceptAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]]; } diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.h b/WebDriverAgentLib/Utilities/FBCapabilities.h index d1339c7a6..649a227ce 100644 --- a/WebDriverAgentLib/Utilities/FBCapabilities.h +++ b/WebDriverAgentLib/Utilities/FBCapabilities.h @@ -8,8 +8,6 @@ #import -/** Whether to use alternative elements visivility detection method */ -extern NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION; /** Set the maximum amount of characters that could be typed within a minute (60 by default) */ extern NSString* const FB_CAP_MAX_TYPING_FREQUENCY; /** this setting was needed for some legacy stuff */ diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.m b/WebDriverAgentLib/Utilities/FBCapabilities.m index 351f23e68..4693fc8a2 100644 --- a/WebDriverAgentLib/Utilities/FBCapabilities.m +++ b/WebDriverAgentLib/Utilities/FBCapabilities.m @@ -8,7 +8,6 @@ #import "FBCapabilities.h" -NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION = @"shouldUseTestManagerForVisibilityDetection"; NSString* const FB_CAP_MAX_TYPING_FREQUENCY = @"maxTypingFrequency"; NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER = @"shouldUseSingletonTestManager"; NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS = @"disableAutomaticScreenshots"; diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgentLib/Utilities/FBConfiguration.h index 35fbf165a..e8c7754bf 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.h +++ b/WebDriverAgentLib/Utilities/FBConfiguration.h @@ -18,10 +18,6 @@ extern NSString *const FBSnapshotMaxDepthKey; */ @interface FBConfiguration : NSObject -/*! If set to YES will ask TestManagerDaemon for element visibility */ -+ (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value; -+ (BOOL)shouldUseTestManagerForVisibilityDetection; - /*! If set to YES will use compact (standards-compliant) & faster responses */ + (void)setShouldUseCompactResponses:(BOOL)value; + (BOOL)shouldUseCompactResponses; @@ -276,17 +272,6 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + (void)setAnimationCoolOffTimeout:(NSTimeInterval)timeout; + (NSTimeInterval)animationCoolOffTimeout; -/** - Enforces the page hierarchy to include non modal elements, - like Contacts. By default such elements are not present there. - See https://github.com/appium/appium/issues/13227 - - @param isEnabled Set to YES in order to enable non modal elements inclusion. - Setting this value to YES will have no effect if the current iOS SDK does not support such feature. - */ -+ (void)setIncludeNonModalElements:(BOOL)isEnabled; -+ (BOOL)includeNonModalElements; - /** Sets custom class chain locators for accept/dismiss alert buttons location. This might be useful if the default buttons detection algorithm fails to determine alert buttons properly diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgentLib/Utilities/FBConfiguration.m index e6ce8d52f..dcd1a62e3 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.m +++ b/WebDriverAgentLib/Utilities/FBConfiguration.m @@ -33,7 +33,6 @@ static NSString *const FBKeyboardPredictionKey = @"KeyboardPrediction"; static NSString *const axSettingsClassName = @"AXSettings"; -static BOOL FBShouldUseTestManagerForVisibilityDetection = NO; static BOOL FBShouldUseSingletonTestManager = YES; static BOOL FBShouldRespectSystemAlerts = NO; @@ -48,7 +47,6 @@ static NSUInteger FBScreenshotQuality; static BOOL FBShouldUseFirstMatch; static BOOL FBShouldBoundElementsByIndex; -static BOOL FBIncludeNonModalElements; static NSString *FBAcceptAlertButtonSelector; static NSString *FBDismissAlertButtonSelector; static NSString *FBAutoClickAlertSelector; @@ -188,16 +186,6 @@ + (BOOL)verboseLoggingEnabled return [NSProcessInfo.processInfo.environment[@"VERBOSE_LOGGING"] boolValue]; } -+ (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value -{ - FBShouldUseTestManagerForVisibilityDetection = value; -} - -+ (BOOL)shouldUseTestManagerForVisibilityDetection -{ - return FBShouldUseTestManagerForVisibilityDetection; -} - + (void)setShouldUseCompactResponses:(BOOL)value { FBShouldUseCompactResponses = value; @@ -426,16 +414,6 @@ + (BOOL)boundElementsByIndex return FBShouldBoundElementsByIndex; } -+ (void)setIncludeNonModalElements:(BOOL)isEnabled -{ - FBIncludeNonModalElements = isEnabled; -} - -+ (BOOL)includeNonModalElements -{ - return FBIncludeNonModalElements; -} - + (void)setAcceptAlertButtonSelector:(NSString *)classChainSelector { FBAcceptAlertButtonSelector = classChainSelector; @@ -542,9 +520,6 @@ + (void)resetSessionSettings FBScreenshotQuality = 3; FBShouldUseFirstMatch = NO; FBShouldBoundElementsByIndex = NO; - // This is diabled by default because enabling it prevents the accessbility snapshot to be taken - // (it always errors with kxIllegalArgument error) - FBIncludeNonModalElements = NO; FBAcceptAlertButtonSelector = @""; FBDismissAlertButtonSelector = @""; FBAutoClickAlertSelector = @""; diff --git a/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgentLib/Utilities/FBSettings.h index 08d8a7963..c1fc6e346 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.h +++ b/WebDriverAgentLib/Utilities/FBSettings.h @@ -28,7 +28,6 @@ extern NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX; extern NSString* const FB_SETTING_REDUCE_MOTION; extern NSString* const FB_SETTING_DEFAULT_ACTIVE_APPLICATION; extern NSString* const FB_SETTING_ACTIVE_APP_DETECTION_POINT; -extern NSString* const FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS; extern NSString* const FB_SETTING_DEFAULT_ALERT_ACTION; extern NSString* const FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR; extern NSString* const FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR; diff --git a/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgentLib/Utilities/FBSettings.m index 65c6ed82f..d333c58f1 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.m +++ b/WebDriverAgentLib/Utilities/FBSettings.m @@ -24,7 +24,6 @@ NSString* const FB_SETTING_REDUCE_MOTION = @"reduceMotion"; NSString* const FB_SETTING_DEFAULT_ACTIVE_APPLICATION = @"defaultActiveApplication"; NSString* const FB_SETTING_ACTIVE_APP_DETECTION_POINT = @"activeAppDetectionPoint"; -NSString* const FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS = @"includeNonModalElements"; NSString* const FB_SETTING_DEFAULT_ALERT_ACTION = @"defaultAlertAction"; NSString* const FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR = @"acceptAlertButtonSelector"; NSString* const FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR = @"dismissAlertButtonSelector"; diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h index 75d1ee203..247f056bd 100644 --- a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h @@ -61,17 +61,10 @@ NS_ASSUME_NONNULL_BEGIN @interface XCUIElement (FBCompatibility) -/** - Determines whether current iOS SDK supports non modal elements inlusion into snapshots - - @return Either YES or NO - */ -+ (BOOL)fb_supportsNonModalElementsInclusion; - /** Retrieves element query - @return Element query property extended with non modal elements depending on the actual configuration + @return Element query */ - (XCUIElementQuery *)fb_query; diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m index f2cf03a24..5f88ccb56 100644 --- a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m @@ -45,21 +45,9 @@ - (XCUIElement *)fb_firstMatch @implementation XCUIElement (FBCompatibility) -+ (BOOL)fb_supportsNonModalElementsInclusion -{ - static dispatch_once_t hasIncludingNonModalElements; - static BOOL result; - dispatch_once(&hasIncludingNonModalElements, ^{ - result = [XCUIApplication.fb_systemApplication.query respondsToSelector:@selector(includingNonModalElements)]; - }); - return result; -} - - (XCUIElementQuery *)fb_query { - return FBConfiguration.includeNonModalElements && self.class.fb_supportsNonModalElementsInclusion - ? self.query.includingNonModalElements - : self.query; + return self.query; } @end diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m index 56508e809..cfb23e41e 100755 --- a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m @@ -7104,23 +7104,6 @@ - (void)ssl_startTLS return; } -#if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) - - // Note from Apple's documentation: - // - // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. - // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the - // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus - // SSLSetEnableCertVerify is not available on that platform at all. - - status = SSLSetEnableCertVerify(sslContext, NO); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; - return; - } - -#endif } // Configure SSLContext from given settings @@ -7384,21 +7367,12 @@ - (void)ssl_startTLS value = [tlsSettings objectForKey:GCDAsyncSocketSSLALPN]; if ([value isKindOfClass:[NSArray class]]) { - if (@available(iOS 11.0, macOS 10.13, tvOS 11.0, *)) - { - CFArrayRef protocols = (__bridge CFArrayRef)((NSArray *) value); - status = SSLSetALPNProtocols(sslContext, protocols); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetALPNProtocols"]]; - return; - } - } - else + CFArrayRef protocols = (__bridge CFArrayRef)((NSArray *) value); + status = SSLSetALPNProtocols(sslContext, protocols); + if (status != noErr) { - NSAssert(NO, @"Security option unavailable - GCDAsyncSocketSSLALPN" - @" - iOS 11.0, macOS 10.13 required"); - [self closeWithError:[self otherError:@"Security option unavailable - GCDAsyncSocketSSLALPN"]]; + [self closeWithError:[self otherError:@"Error in SSLSetALPNProtocols"]]; + return; } } else if (value) diff --git a/lib/types.ts b/lib/types.ts index 57524388d..4a724161d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -18,7 +18,6 @@ export interface WDASettings { reduceMotion?: boolean; defaultActiveApplication?: string; activeAppDetectionPoint?: string; - includeNonModalElements?: boolean; defaultAlertAction?: 'accept' | 'dismiss'; acceptAlertButtonSelector?: string; dismissAlertButtonSelector?: string; @@ -42,7 +41,6 @@ export interface WDACapabilities { environment?: Record; eventloopIdleDelaySec?: number; shouldWaitForQuiescence?: boolean; - shouldUseTestManagerForVisibilityDetection?: boolean; maxTypingFrequency?: number; shouldUseSingletonTestManager?: boolean; waitForIdleTimeout?: number; @@ -100,8 +98,6 @@ export interface AppleDevice { udid: string; simctl?: any; devicectl?: any; - /** @deprecated We'll stop supporting idb */ - idb?: any; [key: string]: any; } diff --git a/lib/utils.ts b/lib/utils.ts index bf48f998c..5f8fbe271 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -292,7 +292,7 @@ export async function resetTestProcesses(udid: string, isSimulator: boolean): Pr const processPatterns = [`xcodebuild.*${udid}`]; if (isSimulator) { processPatterns.push(`${udid}.*XCTRunner`); - // The pattern to find in case idb was used + // Some XCTest launches might not include xcodebuild in their command line processPatterns.push(`xctest.*${udid}`); } log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`); diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index 1499c8077..d311c12e8 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -15,7 +15,6 @@ const fakeConstructorArgs: WebDriverAgentArgs = { udid: 'some-sim-udid', simctl: {}, devicectl: {}, - idb: null, }, platformVersion: '9', host: 'me', From 1b6c701b11ec9db32bea1e3902635a57d1dba87d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 Apr 2026 18:40:25 +0000 Subject: [PATCH 07/55] chore(release): 12.0.0 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [12.0.0](https://github.com/appium/WebDriverAgent/compare/v11.4.3...v12.0.0) (2026-04-14) ### ⚠ BREAKING CHANGES * remove idb from AppleDevice; clients must stop passing device.idb. * remove includeNonModalElements WDA setting; clients must stop sending this setting in /settings. * remove shouldUseTestManagerForVisibilityDetection capability; clients must stop sending this desired capability. ### Code Refactoring * remove deprecated WDA settings/capabilities and idb typing ([#1124](https://github.com/appium/WebDriverAgent/issues/1124)) ([5072e25](https://github.com/appium/WebDriverAgent/commit/5072e255faa3538f5ff4c8769bf16fd290ee8af9)) --- CHANGELOG.md | 12 ++++++++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e0813ada..4f02081e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [12.0.0](https://github.com/appium/WebDriverAgent/compare/v11.4.3...v12.0.0) (2026-04-14) + +### ⚠ BREAKING CHANGES + +* remove idb from AppleDevice; clients must stop passing device.idb. +* remove includeNonModalElements WDA setting; clients must stop sending this setting in /settings. +* remove shouldUseTestManagerForVisibilityDetection capability; clients must stop sending this desired capability. + +### Code Refactoring + +* remove deprecated WDA settings/capabilities and idb typing ([#1124](https://github.com/appium/WebDriverAgent/issues/1124)) ([5072e25](https://github.com/appium/WebDriverAgent/commit/5072e255faa3538f5ff4c8769bf16fd290ee8af9)) + ## [11.4.3](https://github.com/appium/WebDriverAgent/compare/v11.4.2...v11.4.3) (2026-04-12) ### Miscellaneous Chores diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index f28826781..e555a0aae 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 11.4.3 + 12.0.0 CFBundleSignature ???? CFBundleVersion - 11.4.3 + 12.0.0 NSPrincipalClass diff --git a/package.json b/package.json index e66c9b673..56d40e999 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "11.4.3", + "version": "12.0.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 2ca4503b6dacfdd1947ba8b8604d76ea91ad0de4 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 19 Apr 2026 19:45:54 +0200 Subject: [PATCH 08/55] ci: Bump ios version (#1126) --- .github/workflows/functional-test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 4f1aa7686..d7647d57a 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -12,18 +12,18 @@ jobs: fail-fast: false matrix: test_targets: - - HOST_OS: 'macos-15' - XCODE_VERSION: '26.1' - IOS_VERSION: '26.1' - IOS_MODEL: iPhone 17 + - HOST_OS: 'macos-26' + XCODE_VERSION: '26.4' + IOS_VERSION: '26.4' + IOS_MODEL: 'iPhone 17' - HOST_OS: 'macos-15' XCODE_VERSION: '16.4' IOS_VERSION: '18.4' - IOS_MODEL: iPhone 16 Plus + IOS_MODEL: 'iPhone 16 Plus' - HOST_OS: 'macos-14' XCODE_VERSION: '15.4' IOS_VERSION: '17.5' - IOS_MODEL: iPhone 15 Plus + IOS_MODEL: 'iPhone 15 Plus' # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md runs-on: ${{matrix.test_targets.HOST_OS}} From a8889cd7cb85c1b58faee306295fb3c5c2a9d0e3 Mon Sep 17 00:00:00 2001 From: muvaffak Date: Fri, 24 Apr 2026 16:11:10 -0700 Subject: [PATCH 09/55] feat(client): add ability to set headers on requests (#1127) Signed-off-by: Muvaffak Onus --- lib/types.ts | 4 ++++ lib/webdriveragent.ts | 3 +++ package.json | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/types.ts b/lib/types.ts index 4a724161d..e748c3bd5 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,3 +1,5 @@ +import {type HTTPHeaders} from '@appium/types'; + // WebDriverAgentLib/Utilities/FBSettings.h export interface WDASettings { elementResponseAttribute?: string; @@ -92,6 +94,7 @@ export interface WebDriverAgentArgs { resultBundleVersion?: string; reqBasePath?: string; launchTimeout?: number; + extraRequestHeaders?: HTTPHeaders; } export interface AppleDevice { @@ -138,4 +141,5 @@ export interface XcodeBuildArgs { allowProvisioningDeviceRegistration?: boolean; resultBundlePath?: string; resultBundleVersion?: string; + extraRequestHeaders?: HTTPHeaders; } diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 95daacf4b..69c8fc52d 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -346,6 +346,7 @@ export class WebDriverAgent { timeout: this.wdaConnectionTimeout, keepAlive: true, scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', + headers: this.args.extraRequestHeaders, }; if (this.args.reqBasePath) { proxyOpts.reqBasePath = this.args.reqBasePath; @@ -560,10 +561,12 @@ export class WebDriverAgent { */ private async getStatus(timeoutMs: number = 0): Promise { const noSessionProxy = new NoSessionProxy({ + scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', server: this.url.hostname ?? undefined, port: parseInt(this.url.port ?? '', 10) || undefined, base: this.basePath, timeout: 3000, + headers: this.args.extraRequestHeaders, }); const sendGetStatus = async () => diff --git a/package.json b/package.json index 56d40e999..eec8ecb0b 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "typescript": "^6.0.2" }, "dependencies": { - "@appium/base-driver": "^10.0.0-rc.1", + "@appium/base-driver": "^10.3.0", "@appium/strongbox": "^1.0.0-rc.1", "@appium/support": "^7.0.0-rc.1", "appium-ios-device": "^3.0.0", From 13d61a83f5aff867d713d9bbb3d069f41ab6b0e6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 24 Apr 2026 23:15:54 +0000 Subject: [PATCH 10/55] chore(release): 12.1.0 [skip ci] ## [12.1.0](https://github.com/appium/WebDriverAgent/compare/v12.0.0...v12.1.0) (2026-04-24) ### Features * **client:** add ability to set headers on requests ([#1127](https://github.com/appium/WebDriverAgent/issues/1127)) ([a8889cd](https://github.com/appium/WebDriverAgent/commit/a8889cd7cb85c1b58faee306295fb3c5c2a9d0e3)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f02081e7..83020e47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [12.1.0](https://github.com/appium/WebDriverAgent/compare/v12.0.0...v12.1.0) (2026-04-24) + +### Features + +* **client:** add ability to set headers on requests ([#1127](https://github.com/appium/WebDriverAgent/issues/1127)) ([a8889cd](https://github.com/appium/WebDriverAgent/commit/a8889cd7cb85c1b58faee306295fb3c5c2a9d0e3)) + ## [12.0.0](https://github.com/appium/WebDriverAgent/compare/v11.4.3...v12.0.0) (2026-04-14) ### ⚠ BREAKING CHANGES diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index e555a0aae..4a7492368 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 12.0.0 + 12.1.0 CFBundleSignature ???? CFBundleVersion - 12.0.0 + 12.1.0 NSPrincipalClass diff --git a/package.json b/package.json index eec8ecb0b..babe96710 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "12.0.0", + "version": "12.1.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 76d59e85c75680c97abe9e67fdf4a70cacd46418 Mon Sep 17 00:00:00 2001 From: Swastik Baranwal Date: Mon, 27 Apr 2026 19:13:12 +0530 Subject: [PATCH 11/55] chore(compile): fix compilation (#1129) --- PrivateHeaders/XCTest/CDStructures.h | 3 +++ PrivateHeaders/XCTest/XCTestCase.h | 3 +++ 2 files changed, 6 insertions(+) diff --git a/PrivateHeaders/XCTest/CDStructures.h b/PrivateHeaders/XCTest/CDStructures.h index eaf3f46fa..56078e9aa 100644 --- a/PrivateHeaders/XCTest/CDStructures.h +++ b/PrivateHeaders/XCTest/CDStructures.h @@ -24,5 +24,8 @@ typedef struct { unsigned short _field3[1]; } CDStruct_27a325c0; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-identifier" int _XCTSetApplicationStateTimeout(double timeout); double _XCTApplicationStateTimeout(void); +#pragma clang diagnostic pop diff --git a/PrivateHeaders/XCTest/XCTestCase.h b/PrivateHeaders/XCTest/XCTestCase.h index d68d949c5..8e4a52161 100644 --- a/PrivateHeaders/XCTest/XCTestCase.h +++ b/PrivateHeaders/XCTest/XCTestCase.h @@ -8,7 +8,10 @@ #import +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-identifier" @class NSInvocation, XCTestCaseRun, XCTestContext, _XCTestCaseImplementation; +#pragma clang diagnostic pop @interface XCTestCase() { From 86469aeeb8b6829f338b74b7a1ea26fb1fa05ec1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 27 Apr 2026 13:48:15 +0000 Subject: [PATCH 12/55] chore(release): 12.1.1 [skip ci] ## [12.1.1](https://github.com/appium/WebDriverAgent/compare/v12.1.0...v12.1.1) (2026-04-27) ### Miscellaneous Chores * **compile:** fix compilation ([#1129](https://github.com/appium/WebDriverAgent/issues/1129)) ([76d59e8](https://github.com/appium/WebDriverAgent/commit/76d59e85c75680c97abe9e67fdf4a70cacd46418)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83020e47a..82c167575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [12.1.1](https://github.com/appium/WebDriverAgent/compare/v12.1.0...v12.1.1) (2026-04-27) + +### Miscellaneous Chores + +* **compile:** fix compilation ([#1129](https://github.com/appium/WebDriverAgent/issues/1129)) ([76d59e8](https://github.com/appium/WebDriverAgent/commit/76d59e85c75680c97abe9e67fdf4a70cacd46418)) + ## [12.1.0](https://github.com/appium/WebDriverAgent/compare/v12.0.0...v12.1.0) (2026-04-24) ### Features diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 4a7492368..87791c148 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 12.1.0 + 12.1.1 CFBundleSignature ???? CFBundleVersion - 12.1.0 + 12.1.1 NSPrincipalClass diff --git a/package.json b/package.json index babe96710..644b46cbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "12.1.0", + "version": "12.1.1", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 88998951f004daed1d22ce2c06eec89a08129e4f Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 29 Apr 2026 08:36:12 +0200 Subject: [PATCH 13/55] feat: Ditch bluebird and lodash (#1130) --- Scripts/build-webdriveragent.mjs | 11 +- Scripts/fetch-prebuilt-wda.mjs | 18 +- lib/check-dependencies.ts | 33 ++-- lib/utils.ts | 137 ++++++++++----- lib/webdriveragent.ts | 249 ++++++++++++++------------- lib/xcodebuild.ts | 32 ++-- package.json | 6 +- test/functional/helpers/simulator.ts | 3 +- test/unit/webdriveragent-specs.ts | 83 ++++----- 9 files changed, 331 insertions(+), 241 deletions(-) diff --git a/Scripts/build-webdriveragent.mjs b/Scripts/build-webdriveragent.mjs index 7fd864385..cd82cd622 100644 --- a/Scripts/build-webdriveragent.mjs +++ b/Scripts/build-webdriveragent.mjs @@ -20,6 +20,11 @@ const WDA_BUNDLE_TV_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'De const TARGETS = ['runner', 'tv_runner']; const SDKS = ['sim', 'tv_sim']; +/** + * Build WebDriverAgent and pack the app bundle into a zip archive. + * + * @param {string} [xcodeVersion] Xcode version to include in archive name. + */ async function buildWebDriverAgent (xcodeVersion) { const target = process.env.TARGET; const sdk = process.env.SDK; @@ -77,10 +82,12 @@ async function buildWebDriverAgent (xcodeVersion) { } if (isMainModule) { - buildWebDriverAgent().catch((e) => { + try { + await buildWebDriverAgent(); + } catch (e) { LOG.error(e); process.exit(1); - }); + } } export default buildWebDriverAgent; diff --git a/Scripts/fetch-prebuilt-wda.mjs b/Scripts/fetch-prebuilt-wda.mjs index 0fa1df409..782cec9d4 100644 --- a/Scripts/fetch-prebuilt-wda.mjs +++ b/Scripts/fetch-prebuilt-wda.mjs @@ -3,8 +3,6 @@ import { fileURLToPath } from 'node:url'; import { readFileSync } from 'node:fs'; import axios from 'axios'; import { logger, fs, mkdirp, net } from '@appium/support'; -import _ from 'lodash'; -import B from 'bluebird'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -12,6 +10,9 @@ const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === __file const log = logger.getLogger('WDA'); +/** + * Download all prebuilt WebDriverAgent archives for the current package version. + */ async function fetchPrebuiltWebDriverAgentAssets () { const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')); const tag = packageJson.version; @@ -51,20 +52,25 @@ async function fetchPrebuiltWebDriverAgentAssets () { const url = asset.browser_download_url; log.info(`Downloading: ${url}`); try { - const nameOfAgent = _.last(url.split('/')); + const nameOfAgent = url.split('/').at(-1); + if (!nameOfAgent) { + continue; + } agentsDownloading.push(downloadAgent(url, path.join(webdriveragentsDir, nameOfAgent))); } catch { } } // Wait for them all to finish - return await B.all(agentsDownloading); + return await Promise.all(agentsDownloading); } if (isMainModule) { - fetchPrebuiltWebDriverAgentAssets().catch((e) => { + try { + await fetchPrebuiltWebDriverAgentAssets(); + } catch (e) { log.error(e); process.exit(1); - }); + } } export default fetchPrebuiltWebDriverAgentAssets; diff --git a/lib/check-dependencies.ts b/lib/check-dependencies.ts index 44c204d2c..d4d3390b5 100644 --- a/lib/check-dependencies.ts +++ b/lib/check-dependencies.ts @@ -5,21 +5,9 @@ import {WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP} from './constants'; import {BOOTSTRAP_PATH} from './utils'; import type {XcodeBuild} from './xcodebuild'; -async function buildWDASim(): Promise { - const args = [ - '-project', - path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'), - '-scheme', - WDA_SCHEME, - '-sdk', - SDK_SIMULATOR, - 'CODE_SIGN_IDENTITY=""', - 'CODE_SIGNING_REQUIRED="NO"', - 'GCC_TREAT_WARNINGS_AS_ERRORS=0', - ]; - await exec('xcodebuild', args); -} - +/** + * Ensure simulator WDA is built and return the resulting app bundle path. + */ export async function bundleWDASim(xcodebuild: XcodeBuild): Promise { const derivedDataPath = await xcodebuild.retrieveDerivedDataPath(); if (!derivedDataPath) { @@ -38,3 +26,18 @@ export async function bundleWDASim(xcodebuild: XcodeBuild): Promise { await buildWDASim(); return wdaBundlePath; } + +async function buildWDASim(): Promise { + const args = [ + '-project', + path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'), + '-scheme', + WDA_SCHEME, + '-sdk', + SDK_SIMULATOR, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED="NO"', + 'GCC_TREAT_WARNINGS_AS_ERRORS=0', + ]; + await exec('xcodebuild', args); +} diff --git a/lib/utils.ts b/lib/utils.ts index 5f8fbe271..f94b442ce 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -3,9 +3,7 @@ import {exec, SubProcess} from 'teen_process'; import path, {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import {log} from './logger'; -import _ from 'lodash'; import {PLATFORM_NAME_TVOS} from './constants'; -import B from 'bluebird'; import _fs from 'node:fs'; import {waitForCondition} from 'asyncbox'; import {arch} from 'node:os'; @@ -19,13 +17,18 @@ const currentFilename = const currentDirname = dirname(currentFilename); +let moduleRootCache: string | undefined; + /** * Calculates the path to the current module's root folder * * @returns {string} The full path to module root * @throws {Error} If the current module root folder cannot be determined */ -const getModuleRoot = _.memoize(function getModuleRoot(): string { +const getModuleRoot = function getModuleRoot(): string { + if (moduleRootCache) { + return moduleRootCache; + } let currentDir = currentDirname; let isAtFsRoot = false; while (!isAtFsRoot) { @@ -35,6 +38,7 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string { _fs.existsSync(manifestPath) && JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent' ) { + moduleRootCache = currentDir; return currentDir; } } catch {} @@ -42,15 +46,29 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string { isAtFsRoot = currentDir.length <= path.dirname(currentDir).length; } throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module'); -}); +}; export const BOOTSTRAP_PATH = getModuleRoot(); +/** + * Arguments for setting xctestrun file + */ +export interface XctestrunFileArgs { + deviceInfo: DeviceInfo; + sdkVersion: string; + bootstrapPath: string; + wdaRemotePort: number | string; + wdaBindingIP?: string; +} + +/** + * Find and terminate all processes matching the given pgrep pattern. + */ export async function killAppUsingPattern(pgrepPattern: string): Promise { const signals = [2, 15, 9]; for (const signal of signals) { const matchedPids = await getPIDsUsingPattern(pgrepPattern); - if (_.isEmpty(matchedPids)) { + if (matchedPids.length === 0) { return; } const args = [`-${signal}`, ...matchedPids]; @@ -59,21 +77,24 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise { } catch (err: any) { log.debug(`kill ${args.join(' ')} -> ${err.message}`); } - if (signal === _.last(signals)) { + if (signal === signals[signals.length - 1]) { // there is no need to wait after SIGKILL return; } try { await waitForCondition( async () => { - const pidCheckPromises = matchedPids.map((pid) => - exec('kill', ['-0', pid]) + const pidCheckPromises = matchedPids.map(async (pid) => { + try { + await exec('kill', ['-0', pid]); // the process is still alive - .then(() => false) + return false; + } catch { // the process is dead - .catch(() => true), - ); - return (await B.all(pidCheckPromises)).every((x) => x === true); + return true; + } + }); + return (await Promise.all(pidCheckPromises)).every((x) => x === true); }, { waitMs: 1000, @@ -93,9 +114,12 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise { * @returns Return true if the platformName is tvOS */ export function isTvOS(platformName: string): boolean { - return _.toLower(platformName) === _.toLower(PLATFORM_NAME_TVOS); + return platformName?.toLowerCase() === PLATFORM_NAME_TVOS.toLowerCase(); } +/** + * Configure keychain access required for real-device code signing. + */ export async function setRealDeviceSecurity( keychainPath: string, keychainPassword: string, @@ -106,17 +130,6 @@ export async function setRealDeviceSecurity( await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]); } -/** - * Arguments for setting xctestrun file - */ -export interface XctestrunFileArgs { - deviceInfo: DeviceInfo; - sdkVersion: string; - bootstrapPath: string; - wdaRemotePort: number | string; - wdaBindingIP?: string; -} - /** * Creates xctestrun file per device & platform version. * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device @@ -140,7 +153,7 @@ export async function setXctestrunFile(args: XctestrunFileArgs): Promise wdaRemotePort, wdaBindingIP, ); - const newXctestRunContent = _.merge(xctestRunContent, updateWDAPort); + const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort); await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); return xctestrunFilePath; @@ -285,6 +298,23 @@ export async function getWDAUpgradeTimestamp(): Promise { return mtime.getTime(); } +/** + * Escape regular expression metacharacters in a string. + */ +export function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Truncate a string to the given length and append ellipsis if needed. + */ +export function truncateString(value: string, length: number): string { + if (value.length <= length) { + return value; + } + return `${value.slice(0, Math.max(0, length - 1))}…`; +} + /** * Kills running XCTest processes for the particular device. */ @@ -296,7 +326,7 @@ export async function resetTestProcesses(udid: string, isSimulator: boolean): Pr processPatterns.push(`xctest.*${udid}`); } log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`); - await B.all(processPatterns.map(killAppUsingPattern)); + await Promise.all(processPatterns.map(killAppUsingPattern)); } /** @@ -329,22 +359,25 @@ export async function getPIDsListeningOnPort( return result; } - if (!_.isFunction(filteringFunc)) { + if (typeof filteringFunc !== 'function') { return result; } - return await B.filter(result, async (pid) => { - let stdout: string; - try { - ({stdout} = await exec('ps', ['-p', pid, '-o', 'command'])); - } catch (e: any) { - if (e.code === 1) { - // The process does not exist anymore, there's nothing to filter - return false; + const filtered = await Promise.all( + result.map(async (pid) => { + let stdout: string; + try { + ({stdout} = await exec('ps', ['-p', pid, '-o', 'command'])); + } catch (e: any) { + if (e.code === 1) { + // The process does not exist anymore, there's nothing to filter + return null; + } + throw e; } - throw e; - } - return await filteringFunc(stdout); - }); + return (await filteringFunc(stdout)) ? pid : null; + }), + ); + return filtered.filter((pid): pid is string => Boolean(pid)); } // Private functions @@ -359,7 +392,7 @@ async function getPIDsUsingPattern(pattern: string): Promise { return stdout .split(/\s+/) .map((x) => parseInt(x, 10)) - .filter(_.isInteger) + .filter(Number.isInteger) .map((x) => `${x}`); } catch (err: any) { log.debug( @@ -368,3 +401,27 @@ async function getPIDsUsingPattern(pattern: string): Promise { return []; } } + +function mergeObjects, U extends Record>( + target: T, + source: U, +): T & U { + const output: Record = {...target}; + for (const [key, sourceValue] of Object.entries(source)) { + const targetValue = output[key]; + if (isPlainObject(targetValue) && isPlainObject(sourceValue)) { + output[key] = mergeObjects(targetValue, sourceValue); + continue; + } + output[key] = sourceValue; + } + return output as T & U; +} + +function isPlainObject(value: unknown): value is Record { + if (value == null || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 69c8fc52d..1be2622f8 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -1,8 +1,5 @@ import {waitForCondition} from 'asyncbox'; -import _ from 'lodash'; import path from 'node:path'; -import url from 'node:url'; -import B from 'bluebird'; import {JWProxy} from '@appium/base-driver'; import {fs, util, plist} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; @@ -34,41 +31,42 @@ const WDA_AGENT_PORT = 8100; const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner'; const SHARED_RESOURCES_GUARD = new AsyncLock(); const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; +const URL_PROTOCOL_SEPARATOR = '://'; export class WebDriverAgent { bootstrapPath: string; agentPath: string; readonly args: WebDriverAgentArgs; - private readonly log: AppiumLogger; readonly device: AppleDevice; readonly platformVersion?: string; readonly platformName?: string; readonly iosSdkVersion?: string; readonly host?: string; readonly isRealDevice: boolean; - private readonly wdaBundlePath?: string; - private readonly wdaLocalPort?: number; readonly wdaRemotePort: number; readonly wdaBaseUrl: string; readonly wdaBindingIP?: string; - private readonly prebuildWDA?: boolean; webDriverAgentUrl?: string; started: boolean; + updatedWDABundleId?: string; + noSessionProxy?: NoSessionProxy; + jwproxy?: JWProxy; + proxyReqRes?: any; + private readonly log: AppiumLogger; + private readonly wdaBundlePath?: string; + private readonly wdaLocalPort?: number; + private readonly prebuildWDA?: boolean; private readonly wdaConnectionTimeout?: number; private readonly useXctestrunFile?: boolean; private readonly usePrebuiltWDA?: boolean; private readonly derivedDataPath?: string; private readonly mjpegServerPort?: number; - updatedWDABundleId?: string; private readonly wdaLaunchTimeout: number; private readonly usePreinstalledWDA?: boolean; - private xctestApiClient?: Xctest | null; private readonly updatedWDABundleIdSuffix: string; + private xctestApiClient?: Xctest | null; private _xcodebuild?: XcodeBuild | null; - noSessionProxy?: NoSessionProxy; - jwproxy?: JWProxy; - proxyReqRes?: any; - private _url?: url.UrlWithStringQuery; + private _url?: URL; /** * Creates a new WebDriverAgent instance. @@ -76,7 +74,7 @@ export class WebDriverAgent { * @param log - Optional logger instance */ constructor(args: WebDriverAgentArgs, log: AppiumLogger | null = null) { - this.args = _.clone(args); + this.args = {...args}; this.log = log ?? defaultLogger; this.device = args.device; @@ -185,6 +183,62 @@ export class WebDriverAgent { return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`; } + /** + * Gets the base path for the WebDriverAgent URL. + * @returns The base path (empty string if root path) + */ + get basePath(): string { + if (this.url.pathname === '/') { + return ''; + } + return this.url.pathname || ''; + } + + /** + * Gets the WebDriverAgent URL. + * Constructs the URL from webDriverAgentUrl if provided, otherwise + * builds it from wdaBaseUrl, wdaBindingIP, and wdaLocalPort. + * @returns The parsed URL object + */ + get url(): URL { + if (!this._url) { + if (this.webDriverAgentUrl) { + this._url = this.toUrl(this.webDriverAgentUrl); + } else { + const port = this.wdaLocalPort || WDA_AGENT_PORT; + const parsedBaseUrl = this.toUrl(this.wdaBaseUrl || WDA_BASE_URL); + this._url = new URL( + `${parsedBaseUrl.protocol}//${this.wdaBindingIP || parsedBaseUrl.hostname}:${port}`, + ); + } + } + return this._url; + } + + /** + * Gets whether WebDriverAgent has fully started. + * @returns `true` if WDA has started, `false` otherwise + */ + get fullyStarted(): boolean { + return this.started; + } + + /** + * Sets whether WebDriverAgent has fully started. + * @param started - `true` if WDA has started, `false` otherwise + */ + set fullyStarted(started: boolean) { + this.started = started ?? false; + } + + /** + * Sets the WebDriverAgent URL. + * @param _url - The URL string to parse and set + */ + set url(_url: string) { + this._url = this.toUrl(_url); + } + /** * Cleans up obsolete cached processes from previous WDA sessions * that are listening on the same port but belong to different devices. @@ -197,7 +251,7 @@ export class WebDriverAgent { !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase()), ); - if (_.isEmpty(obsoletePids)) { + if (obsoletePids.length === 0) { this.log.debug( `No obsolete cached processes from previous WDA sessions ` + `listening on port ${this.url.port} have been found`, @@ -220,14 +274,6 @@ export class WebDriverAgent { } /** - * Gets the base path for the WebDriverAgent URL. - * @returns The base path (empty string if root path) - */ - get basePath(): string { - if (this.url.path === '/') { - return ''; - } - return this.url.path || ''; } /** @@ -307,56 +353,10 @@ export class WebDriverAgent { * @returns `true` if source is fresh (all required files exist), `false` otherwise */ async isSourceFresh(): Promise { - const existsPromises = ['Resources', `Resources${path.sep}WebDriverAgent.bundle`].map( + const existsPromises = ['Resources', path.join('Resources', 'WebDriverAgent.bundle')].map( (subPath) => fs.exists(path.resolve(this.bootstrapPath, subPath)), ); - return (await B.all(existsPromises)).some((v) => v === false); - } - - private async parseBundleId(wdaBundlePath: string): Promise { - const infoPlistPath = path.join(wdaBundlePath, 'Info.plist'); - const infoPlist = (await plist.parsePlist(await fs.readFile(infoPlistPath))) as { - CFBundleIdentifier?: string; - }; - if (!infoPlist.CFBundleIdentifier) { - throw new Error(`Could not find bundle id in '${infoPlistPath}'`); - } - return infoPlist.CFBundleIdentifier; - } - - private async fetchWDABundle(): Promise { - if (!this.derivedDataPath) { - return await bundleWDASim(this.xcodebuild); - } - const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, { - absolute: true, - }); - if (_.isEmpty(wdaBundlePaths)) { - throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`); - } - return wdaBundlePaths[0]; - } - - private setupProxies(sessionId: string): void { - const proxyOpts: any = { - log: this.log, - server: this.url.hostname ?? undefined, - port: parseInt(this.url.port ?? '', 10) || undefined, - base: this.basePath, - timeout: this.wdaConnectionTimeout, - keepAlive: true, - scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', - headers: this.args.extraRequestHeaders, - }; - if (this.args.reqBasePath) { - proxyOpts.reqBasePath = this.args.reqBasePath; - } - - this.jwproxy = new JWProxy(proxyOpts); - this.jwproxy.sessionId = sessionId; - this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); - - this.noSessionProxy = new NoSessionProxy(proxyOpts); + return (await Promise.all(existsPromises)).every((v) => v === true); } /** @@ -401,49 +401,6 @@ export class WebDriverAgent { } } - /** - * Gets the WebDriverAgent URL. - * Constructs the URL from webDriverAgentUrl if provided, otherwise - * builds it from wdaBaseUrl, wdaBindingIP, and wdaLocalPort. - * @returns The parsed URL object - */ - get url(): url.UrlWithStringQuery { - if (!this._url) { - if (this.webDriverAgentUrl) { - this._url = url.parse(this.webDriverAgentUrl); - } else { - const port = this.wdaLocalPort || WDA_AGENT_PORT; - const {protocol, hostname} = url.parse(this.wdaBaseUrl || WDA_BASE_URL); - this._url = url.parse(`${protocol}//${this.wdaBindingIP || hostname}:${port}`); - } - } - return this._url; - } - - /** - * Sets the WebDriverAgent URL. - * @param _url - The URL string to parse and set - */ - set url(_url: string) { - this._url = url.parse(_url); - } - - /** - * Gets whether WebDriverAgent has fully started. - * @returns `true` if WDA has started, `false` otherwise - */ - get fullyStarted(): boolean { - return this.started; - } - - /** - * Sets whether WebDriverAgent has fully started. - * @param started - `true` if WDA has started, `false` otherwise - */ - set fullyStarted(started: boolean) { - this.started = started ?? false; - } - /** * Retrieves the Xcode derived data path for WebDriverAgent. * @returns The derived data path, or `undefined` if xcodebuild is skipped @@ -497,7 +454,7 @@ export class WebDriverAgent { if ( actualUpgradeTimestamp && upgradedAt && - _.toLower(`${actualUpgradeTimestamp}`) !== _.toLower(`${upgradedAt}`) + `${actualUpgradeTimestamp}`.toLowerCase() !== `${upgradedAt}`.toLowerCase() ) { this.log.info( 'Will uninstall running WDA since it has different version in comparison to the one ' + @@ -523,6 +480,64 @@ export class WebDriverAgent { await this.uninstall(); } + private async parseBundleId(wdaBundlePath: string): Promise { + const infoPlistPath = path.join(wdaBundlePath, 'Info.plist'); + const infoPlist = (await plist.parsePlist(await fs.readFile(infoPlistPath))) as { + CFBundleIdentifier?: string; + }; + if (!infoPlist.CFBundleIdentifier) { + throw new Error(`Could not find bundle id in '${infoPlistPath}'`); + } + return infoPlist.CFBundleIdentifier; + } + + private async fetchWDABundle(): Promise { + if (!this.derivedDataPath) { + return await bundleWDASim(this.xcodebuild); + } + const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, { + absolute: true, + }); + if (wdaBundlePaths.length === 0) { + throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`); + } + return wdaBundlePaths[0]; + } + + private setupProxies(sessionId: string): void { + const proxyOpts: any = { + log: this.log, + server: this.url.hostname ?? undefined, + port: parseInt(this.url.port ?? '', 10) || undefined, + base: this.basePath, + timeout: this.wdaConnectionTimeout, + keepAlive: true, + scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http', + headers: this.args.extraRequestHeaders, + }; + if (this.args.reqBasePath) { + proxyOpts.reqBasePath = this.args.reqBasePath; + } + + this.jwproxy = new JWProxy(proxyOpts); + this.jwproxy.sessionId = sessionId; + this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy); + + this.noSessionProxy = new NoSessionProxy(proxyOpts); + } + + private toUrl(value: string): URL { + // Treat values without `://` as host/path inputs and normalize to http. + if (!value.includes(URL_PROTOCOL_SEPARATOR)) { + return new URL(`http://${value}`); + } + try { + return new URL(value); + } catch { + throw new Error(`Invalid URL: ${value}`); + } + } + private setWDAPaths(bootstrapPath?: string, agentPath?: string): void { // allow the user to specify a place for WDA. This is undocumented and // only here for the purposes of testing development of WDA @@ -572,7 +587,7 @@ export class WebDriverAgent { const sendGetStatus = async () => (await noSessionProxy.command('/status', 'GET')) as StringRecord; - if (_.isNil(timeoutMs) || timeoutMs <= 0) { + if (timeoutMs == null || timeoutMs <= 0) { try { return await sendGetStatus(); } catch (err: any) { @@ -619,7 +634,7 @@ export class WebDriverAgent { private async uninstall(): Promise { try { const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME); - if (_.isEmpty(bundleIds)) { + if (bundleIds.length === 0) { this.log.debug('No WDAs on the device.'); return; } diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 754c87943..df89a52ca 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -3,15 +3,15 @@ import {SubProcess, exec} from 'teen_process'; import {logger, timing} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; import {log as defaultLogger} from './logger'; -import B from 'bluebird'; import { setRealDeviceSecurity, setXctestrunFile, killProcess, getWDAUpgradeTimestamp, isTvOS, + escapeRegExp, + truncateString, } from './utils'; -import _ from 'lodash'; import path from 'node:path'; import {WDA_RUNNER_BUNDLE_ID} from './constants'; import type {AppleDevice, XcodeBuildArgs} from './types'; @@ -30,7 +30,7 @@ const IGNORED_ERRORS = [ 'Failed to remove screenshot at path', ]; const IGNORED_ERRORS_PATTERN = new RegExp( - '(' + IGNORED_ERRORS.map((errStr) => _.escapeRegExp(errStr)).join('|') + ')', + '(' + IGNORED_ERRORS.map((errStr) => escapeRegExp(errStr)).join('|') + ')', ); const RUNNER_SCHEME_TV = 'WebDriverAgentRunner_tvOS'; @@ -44,27 +44,28 @@ const xcodeLog = logger.getLogger('Xcode'); export class XcodeBuild { xcodebuild?: SubProcess; readonly device: AppleDevice; - private readonly log: AppiumLogger; readonly realDevice: boolean; readonly agentPath: string; readonly bootstrapPath: string; readonly platformVersion?: string; readonly platformName?: string; readonly iosSdkVersion?: string; + readonly xcodeSigningId: string; + usePrebuiltWDA?: boolean; + derivedDataPath?: string; + agentUrl?: string; + private readonly log: AppiumLogger; private readonly showXcodeLog?: boolean; private readonly xcodeConfigFile?: string; private readonly xcodeOrgId?: string; - readonly xcodeSigningId: string; private readonly keychainPath?: string; private readonly keychainPassword?: string; - usePrebuiltWDA?: boolean; private readonly useSimpleBuildTest?: boolean; private readonly useXctestrunFile?: boolean; private readonly launchTimeout?: number; private readonly wdaRemotePort?: number; private readonly wdaBindingIP?: string; private readonly updatedWDABundleId?: string; - derivedDataPath?: string; private readonly mjpegServerPort?: number; private readonly prebuildDelay: number; private readonly allowProvisioningDeviceRegistration?: boolean; @@ -75,7 +76,6 @@ export class XcodeBuild { private _derivedDataPathPromise?: Promise; private noSessionProxy?: NoSessionProxy; private xctestrunFilePath?: string; - agentUrl?: string; /** * Creates a new XcodeBuild instance. @@ -119,7 +119,8 @@ export class XcodeBuild { this.mjpegServerPort = args.mjpegServerPort; - this.prebuildDelay = _.isNumber(args.prebuildDelay) ? args.prebuildDelay : PREBUILD_DELAY; + this.prebuildDelay = + typeof args.prebuildDelay === 'number' ? args.prebuildDelay : PREBUILD_DELAY; this.allowProvisioningDeviceRegistration = args.allowProvisioningDeviceRegistration; @@ -183,7 +184,7 @@ export class XcodeBuild { const pattern = /^\s*BUILD_DIR\s+=\s+(\/.*)/m; const match = pattern.exec(stdout); if (!match) { - this.log.warn(`Cannot parse WDA build dir from ${_.truncate(stdout, {length: 300})}`); + this.log.warn(`Cannot parse WDA build dir from ${truncateString(stdout, 300)}`); return; } this.log.debug(`Parsed BUILD_DIR configuration value: '${match[1]}'`); @@ -207,7 +208,7 @@ export class XcodeBuild { if (this.prebuildDelay > 0) { // pause a moment - await B.delay(this.prebuildDelay); + await new Promise((resolve) => setTimeout(resolve, this.prebuildDelay)); } } @@ -242,7 +243,7 @@ export class XcodeBuild { throw new Error('xcodebuild subprocess was not created'); } const xcodebuild = this.xcodebuild; - return await new B((resolve, reject) => { + return await new Promise((resolve, reject) => { xcodebuild.once('exit', (code, signal) => { xcodeLog.error(`xcodebuild exited with code '${code}' and signal '${signal}'`); xcodebuild.removeAllListeners(); @@ -415,9 +416,10 @@ export class XcodeBuild { }); let logXcodeOutput = !!this.showXcodeLog; - const logMsg = _.isBoolean(this.showXcodeLog) - ? `Output from xcodebuild ${this.showXcodeLog ? 'will' : 'will not'} be logged` - : 'Output from xcodebuild will only be logged if any errors are present there'; + const logMsg = + typeof this.showXcodeLog === 'boolean' + ? `Output from xcodebuild ${this.showXcodeLog ? 'will' : 'will not'} be logged` + : 'Output from xcodebuild will only be logged if any errors are present there'; this.log.debug(`${logMsg}. To change this, use 'showXcodeLog' desired capability`); const onStreamLine = (line: string) => { diff --git a/package.json b/package.json index 644b46cbc..43e4cc434 100644 --- a/package.json +++ b/package.json @@ -54,16 +54,14 @@ "@appium/types": "^1.0.0-rc.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", - "@types/bluebird": "^3.5.38", - "@types/lodash": "^4.14.196", "@types/mocha": "^10.0.1", "@types/node": "^25.0.0", "appium-xcode": "^6.0.0", "chai": "^6.0.0", "chai-as-promised": "^8.0.0", "conventional-changelog-conventionalcommits": "^9.0.0", - "node-simctl": "^8.0.0", "mocha": "^11.0.1", + "node-simctl": "^8.0.0", "prettier": "^3.0.0", "semantic-release": "^25.0.2", "semver": "^7.3.7", @@ -80,8 +78,6 @@ "async-lock": "^1.0.0", "asyncbox": "^6.1.0", "axios": "^1.4.0", - "bluebird": "^3.5.5", - "lodash": "^4.17.11", "teen_process": "^4.0.7" }, "files": [ diff --git a/test/functional/helpers/simulator.ts b/test/functional/helpers/simulator.ts index 36a4df3e1..040367f86 100644 --- a/test/functional/helpers/simulator.ts +++ b/test/functional/helpers/simulator.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import {Simctl} from 'node-simctl'; import {retryInterval} from 'asyncbox'; import {killAllSimulators as simKill} from 'appium-ios-simulator'; @@ -7,7 +6,7 @@ import type {AppleDevice} from '../../../lib/types'; export async function killAllSimulators(): Promise { const simctl = new Simctl(); - const allDevices = _.flatMap(_.values(await simctl.getDevices())); + const allDevices = Object.values(await simctl.getDevices()).flat(); const bootedDevices = allDevices.filter((device) => device.state === 'Booted'); for (const {udid} of bootedDevices) { diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index d311c12e8..725822318 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -4,7 +4,6 @@ import {BOOTSTRAP_PATH} from '../../lib/utils'; import {WebDriverAgent} from '../../lib/webdriveragent'; import * as utils from '../../lib/utils'; import path from 'node:path'; -import _ from 'lodash'; import sinon from 'sinon'; import type {WebDriverAgentArgs, AppleDevice} from '../../lib/types'; @@ -34,39 +33,27 @@ describe('WebDriverAgent', function () { expect(agent.agentPath).to.eql(defaultAgentPath); }); it('should have custom wda bootstrap and default agent if only bootstrap specified', function () { - const agent = new WebDriverAgent( - _.defaults( - { - bootstrapPath: customBootstrapPath, - }, - fakeConstructorArgs, - ), - ); + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + bootstrapPath: customBootstrapPath, + }); expect(agent.bootstrapPath).to.eql(customBootstrapPath); expect(agent.agentPath).to.eql(path.resolve(customBootstrapPath, 'WebDriverAgent.xcodeproj')); }); it('should have custom wda bootstrap and agent if both specified', function () { - const agent = new WebDriverAgent( - _.defaults( - { - bootstrapPath: customBootstrapPath, - agentPath: customAgentPath, - }, - fakeConstructorArgs, - ), - ); + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + bootstrapPath: customBootstrapPath, + agentPath: customAgentPath, + }); expect(agent.bootstrapPath).to.eql(customBootstrapPath); expect(agent.agentPath).to.eql(customAgentPath); }); it('should have custom derivedDataPath if specified', function () { - const agent = new WebDriverAgent( - _.defaults( - { - derivedDataPath: customDerivedDataPath, - }, - fakeConstructorArgs, - ), - ); + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + derivedDataPath: customDerivedDataPath, + }); if (agent.xcodebuild) { expect(agent.xcodebuild.derivedDataPath).to.eql(customDerivedDataPath); } @@ -117,7 +104,7 @@ describe('WebDriverAgent', function () { expect(agent.url.port).to.eql('8100'); expect(agent.url.hostname).to.eql('127.0.0.1'); - expect(agent.url.path).to.eql('/aabbccdd'); + expect(agent.url.pathname).to.eql('/aabbccdd'); if (agent.jwproxy) { expect(agent.jwproxy.server).to.eql('127.0.0.1'); expect(agent.jwproxy.port).to.eql(8100); @@ -221,6 +208,24 @@ describe('WebDriverAgent', function () { expect(agent.noSessionProxy.scheme).to.eql('https'); } }); + + it('should accept scheme-less webDriverAgentUrl values', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = 'localhost:8100/aabbccdd'; + const agent = new WebDriverAgent(args); + expect(agent.url.href).to.eql('http://localhost:8100/aabbccdd'); + (agent as any).setupProxies('mysession'); + if (agent.jwproxy) { + expect(agent.jwproxy.scheme).to.eql('http'); + } + }); + + it('should throw for invalid webDriverAgentUrl with explicit scheme', function () { + const args = Object.assign({}, fakeConstructorArgs); + args.webDriverAgentUrl = 'http://'; + const agent = new WebDriverAgent(args); + expect(() => agent.url).to.throw(); + }); }); describe('setupCaching()', function () { @@ -247,19 +252,19 @@ describe('WebDriverAgent', function () { wdaStub.callsFake(function () { return null; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; expect(wdaStubUninstall.notCalled).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).to.be.true; + expect(wda.webDriverAgentUrl === undefined).to.be.true; }); it('should not call uninstall since running WDA has only time', async function () { wdaStub.callsFake(function () { return {build: {time: 'Jun 24 2018 17:08:21'}}; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -276,12 +281,12 @@ describe('WebDriverAgent', function () { }, }; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; expect(wdaStubUninstall.calledOnce).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).to.be.true; + expect(wda.webDriverAgentUrl === undefined).to.be.true; }); it('should call uninstall once since bundle id is different with updatedWDABundleId capability', async function () { @@ -294,12 +299,12 @@ describe('WebDriverAgent', function () { }; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; expect(wdaStubUninstall.calledOnce).to.be.true; - expect(_.isUndefined(wda.webDriverAgentUrl)).to.be.true; + expect(wda.webDriverAgentUrl === undefined).to.be.true; }); it('should not call uninstall since bundle id is equal to updatedWDABundleId capability', async function () { @@ -319,7 +324,7 @@ describe('WebDriverAgent', function () { }; }); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -332,7 +337,7 @@ describe('WebDriverAgent', function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(() => '2'); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -344,7 +349,7 @@ describe('WebDriverAgent', function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(() => '1'); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -356,7 +361,7 @@ describe('WebDriverAgent', function () { return {build: {}}; }); getTimestampStub.callsFake(() => '1'); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; @@ -368,7 +373,7 @@ describe('WebDriverAgent', function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(() => null); - wdaStubUninstall.callsFake(_.noop); + wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); expect(wdaStub.calledOnce).to.be.true; From 381e75ce6ad1afb97b46cc3aa72e758193af1353 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 29 Apr 2026 06:41:03 +0000 Subject: [PATCH 14/55] chore(release): 12.2.0 [skip ci] ## [12.2.0](https://github.com/appium/WebDriverAgent/compare/v12.1.1...v12.2.0) (2026-04-29) ### Features * Ditch bluebird and lodash ([#1130](https://github.com/appium/WebDriverAgent/issues/1130)) ([8899895](https://github.com/appium/WebDriverAgent/commit/88998951f004daed1d22ce2c06eec89a08129e4f)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82c167575..ab2882e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [12.2.0](https://github.com/appium/WebDriverAgent/compare/v12.1.1...v12.2.0) (2026-04-29) + +### Features + +* Ditch bluebird and lodash ([#1130](https://github.com/appium/WebDriverAgent/issues/1130)) ([8899895](https://github.com/appium/WebDriverAgent/commit/88998951f004daed1d22ce2c06eec89a08129e4f)) + ## [12.1.1](https://github.com/appium/WebDriverAgent/compare/v12.1.0...v12.1.1) (2026-04-27) ### Miscellaneous Chores diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 87791c148..52633d852 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 12.1.1 + 12.2.0 CFBundleSignature ???? CFBundleVersion - 12.1.1 + 12.2.0 NSPrincipalClass diff --git a/package.json b/package.json index 43e4cc434..5ca06674e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "12.1.1", + "version": "12.2.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 11c579b7ed3a9995715d65590a2959763871aa6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 16:43:42 +0200 Subject: [PATCH 15/55] chore(deps-dev): bump sinon from 21.1.2 to 22.0.0 (#1133) Bumps [sinon](https://github.com/sinonjs/sinon) from 21.1.2 to 22.0.0. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](https://github.com/sinonjs/sinon/compare/v21.1.2...v22.0.0) --- updated-dependencies: - dependency-name: sinon dependency-version: 22.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ca06674e..f69c166e6 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "prettier": "^3.0.0", "semantic-release": "^25.0.2", "semver": "^7.3.7", - "sinon": "^21.0.0", + "sinon": "^22.0.0", "ts-node": "^10.9.1", "typescript": "^6.0.2" }, From abe0e0c79c845e859c2efbb6f07c0b1a81710aa0 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 6 May 2026 14:49:59 +0000 Subject: [PATCH 16/55] chore(release): 12.2.1 [skip ci] ## [12.2.1](https://github.com/appium/WebDriverAgent/compare/v12.2.0...v12.2.1) (2026-05-06) ### Miscellaneous Chores * **deps-dev:** bump sinon from 21.1.2 to 22.0.0 ([#1133](https://github.com/appium/WebDriverAgent/issues/1133)) ([11c579b](https://github.com/appium/WebDriverAgent/commit/11c579b7ed3a9995715d65590a2959763871aa6d)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2882e57..612e4f68e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [12.2.1](https://github.com/appium/WebDriverAgent/compare/v12.2.0...v12.2.1) (2026-05-06) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 21.1.2 to 22.0.0 ([#1133](https://github.com/appium/WebDriverAgent/issues/1133)) ([11c579b](https://github.com/appium/WebDriverAgent/commit/11c579b7ed3a9995715d65590a2959763871aa6d)) + ## [12.2.0](https://github.com/appium/WebDriverAgent/compare/v12.1.1...v12.2.0) (2026-04-29) ### Features diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 52633d852..e78a8eaa5 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 12.2.0 + 12.2.1 CFBundleSignature ???? CFBundleVersion - 12.2.0 + 12.2.1 NSPrincipalClass diff --git a/package.json b/package.json index f69c166e6..5140dca60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "12.2.0", + "version": "12.2.1", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 2bd181628a1d4525a8f1c459ea295ac0541b514c Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 8 May 2026 17:42:51 +0200 Subject: [PATCH 17/55] fix: linter (#1134) --- Scripts/fetch-prebuilt-wda.mjs | 6 ++++-- lib/appium-ios-device.d.ts | 7 +++++++ lib/no-session-proxy.ts | 8 ++------ lib/utils.ts | 3 ++- lib/webdriveragent.ts | 6 +++--- lib/xcodebuild.ts | 14 ++++++++------ package.json | 6 +++++- test/unit/utils-specs.ts | 4 ++-- test/unit/webdriveragent-specs.ts | 12 ++++++------ tsconfig.json | 3 ++- 10 files changed, 41 insertions(+), 28 deletions(-) create mode 100644 lib/appium-ios-device.d.ts diff --git a/Scripts/fetch-prebuilt-wda.mjs b/Scripts/fetch-prebuilt-wda.mjs index 782cec9d4..2171e85c7 100644 --- a/Scripts/fetch-prebuilt-wda.mjs +++ b/Scripts/fetch-prebuilt-wda.mjs @@ -29,7 +29,7 @@ async function fetchPrebuiltWebDriverAgentAssets () { }, })).data; } catch (e) { - throw new Error(`Could not fetch endpoint ${downloadUrl}. Reason: ${e.message}`); + throw new Error(`Could not fetch endpoint ${downloadUrl}. Reason: ${e.message}`, {cause: e}); } const webdriveragentsDir = path.resolve(__dirname, '..', 'prebuilt-agents'); @@ -42,7 +42,9 @@ async function fetchPrebuiltWebDriverAgentAssets () { try { await net.downloadFile(url, targetPath); } catch (err) { - throw new Error(`Problem downloading webdriveragent from url ${url}: ${err.message}`); + throw new Error(`Problem downloading webdriveragent from url ${url}: ${err.message}`, { + cause: err, + }); } } diff --git a/lib/appium-ios-device.d.ts b/lib/appium-ios-device.d.ts new file mode 100644 index 000000000..5aa65bffd --- /dev/null +++ b/lib/appium-ios-device.d.ts @@ -0,0 +1,7 @@ +declare module 'appium-ios-device' { + export class Xctest { + constructor(...args: any[]); + start(): Promise; + stop(): void; + } +} diff --git a/lib/no-session-proxy.ts b/lib/no-session-proxy.ts index 963624de4..f6d3f42a2 100644 --- a/lib/no-session-proxy.ts +++ b/lib/no-session-proxy.ts @@ -11,13 +11,9 @@ export class NoSessionProxy extends JWProxy { url = '/'; } const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`; - let remainingUrl = ''; if (new RegExp('^/').test(url)) { - remainingUrl = url; - } else { - throw new Error(`Did not know what to do with url '${url}'`); + return proxyBase + url.replace(/\/$/, ''); // can't have trailing slashes } - remainingUrl = remainingUrl.replace(/\/$/, ''); // can't have trailing slashes - return proxyBase + remainingUrl; + throw new Error(`Did not know what to do with url '${url}'`); } } diff --git a/lib/utils.ts b/lib/utils.ts index f94b442ce..a4782c069 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,6 @@ import {fs, plist} from '@appium/support'; -import {exec, SubProcess} from 'teen_process'; +import {exec} from 'teen_process'; +import type {SubProcess} from 'teen_process'; import path, {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import {log} from './logger'; diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 1be2622f8..b0d763506 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -34,8 +34,8 @@ const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; const URL_PROTOCOL_SEPARATOR = '://'; export class WebDriverAgent { - bootstrapPath: string; - agentPath: string; + bootstrapPath!: string; + agentPath!: string; readonly args: WebDriverAgentArgs; readonly device: AppleDevice; readonly platformVersion?: string; @@ -621,7 +621,7 @@ export class WebDriverAgent { `Failed to get the status endpoint in ${timeoutMs} ms. ` + `The last error while accessing ${this.url.href}: ${lastError}. Original error:: ${err.message}.`, ); - throw new Error(`WDA was not ready in ${timeoutMs} ms.`); + throw new Error(`WDA was not ready in ${timeoutMs} ms.`, {cause: err}); } return status; } diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index df89a52ca..00899abdd 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -337,11 +337,10 @@ export class XcodeBuild { } args.push('-destination', `id=${this.device.udid}`); - let versionMatch: RegExpMatchArray | null = null; - if ( - this.platformVersion && - (versionMatch = new RegExp(/^(\d+)\.(\d+)/).exec(this.platformVersion)) - ) { + const versionMatch = this.platformVersion + ? new RegExp(/^(\d+)\.(\d+)/).exec(this.platformVersion) + : null; + if (versionMatch) { args.push( `${isTvOS(this.platformName || '') ? 'TV' : 'IPHONE'}OS_DEPLOYMENT_TARGET=${versionMatch[1]}.${versionMatch[2]}`, ); @@ -472,7 +471,9 @@ export class XcodeBuild { this.log.debug(`WebDriverAgent information:`); this.log.debug(JSON.stringify(currentStatus, null, 2)); } catch (err: any) { - throw new Error(`Unable to connect to running WebDriverAgent: ${err.message}`); + throw new Error(`Unable to connect to running WebDriverAgent: ${err.message}`, { + cause: err, + }); } finally { (noSessionProxy as any).timeout = proxyTimeout; } @@ -491,6 +492,7 @@ export class XcodeBuild { throw new Error( `We were not able to retrieve the /status response from the WebDriverAgent server after ${timeout}ms timeout.` + `Try to increase the value of 'appium:wdaLaunchTimeout' capability as a possible workaround.`, + {cause: err}, ); } return currentStatus; diff --git a/package.json b/package.json index 5140dca60..62c351330 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,12 @@ "@appium/types": "^1.0.0-rc.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", + "@types/async-lock": "^1.4.2", + "@types/chai": "^5.2.3", + "@types/chai-as-promised": "^8.0.2", "@types/mocha": "^10.0.1", "@types/node": "^25.0.0", + "@types/sinon": "^21.0.1", "appium-xcode": "^6.0.0", "chai": "^6.0.0", "chai-as-promised": "^8.0.0", @@ -77,7 +81,7 @@ "appium-ios-simulator": "^8.0.0", "async-lock": "^1.0.0", "asyncbox": "^6.1.0", - "axios": "^1.4.0", + "axios": "^1.16.0", "teen_process": "^4.0.7" }, "files": [ diff --git a/test/unit/utils-specs.ts b/test/unit/utils-specs.ts index d7e3ef01c..d29863200 100644 --- a/test/unit/utils-specs.ts +++ b/test/unit/utils-specs.ts @@ -42,7 +42,7 @@ describe('utils', function () { await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)).to.eventually.equal( path.resolve(`${bootstrapPath}/${udid}_${sdkVersion}.xctestrun`), ); - sandbox.assert.notCalled(fs.copyFile); + sandbox.assert.notCalled(fs.copyFile as any); }); it('should return sdk based path without udid, copy them', async function () { @@ -102,7 +102,7 @@ describe('utils', function () { await expect(getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath)).to.eventually.equal( path.resolve(`${bootstrapPath}/${udid}_${platformVersion}.xctestrun`), ); - sandbox.assert.notCalled(fs.copyFile); + sandbox.assert.notCalled(fs.copyFile as any); }); it('should return platform based path without udid, copy them', async function () { diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index 725822318..cb8cf611f 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -236,7 +236,7 @@ describe('WebDriverAgent', function () { beforeEach(function () { wda = new WebDriverAgent(fakeConstructorArgs); - wdaStub = sinon.stub(wda, 'getStatus'); + wdaStub = sinon.stub(wda as any, 'getStatus'); wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); }); @@ -312,7 +312,7 @@ describe('WebDriverAgent', function () { ...fakeConstructorArgs, updatedWDABundleId: 'com.example.WebDriverAgent', }); - wdaStub = sinon.stub(wda, 'getStatus'); + wdaStub = sinon.stub(wda as any, 'getStatus'); wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); wdaStub.callsFake(function () { @@ -336,7 +336,7 @@ describe('WebDriverAgent', function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); - getTimestampStub.callsFake(() => '2'); + getTimestampStub.callsFake(async () => 2); wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); @@ -348,7 +348,7 @@ describe('WebDriverAgent', function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); - getTimestampStub.callsFake(() => '1'); + getTimestampStub.callsFake(async () => 1); wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); @@ -360,7 +360,7 @@ describe('WebDriverAgent', function () { wdaStub.callsFake(function () { return {build: {}}; }); - getTimestampStub.callsFake(() => '1'); + getTimestampStub.callsFake(async () => 1); wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); @@ -372,7 +372,7 @@ describe('WebDriverAgent', function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); - getTimestampStub.callsFake(() => null); + getTimestampStub.callsFake(async () => null); wdaStubUninstall.callsFake(() => {}); await wda.setupCaching(); diff --git a/tsconfig.json b/tsconfig.json index 2c2b4cc0b..8636ea8c6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "esModuleInterop": true, "outDir": "build", "types": ["node", "mocha"], - "checkJs": true + "checkJs": true, + "strict": true }, "ts-node": { "transpileOnly": true, From b4c7f59207bc273c5fc3e7d64b3d6c915ac0eaff Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 8 May 2026 15:48:44 +0000 Subject: [PATCH 18/55] chore(release): 12.2.2 [skip ci] ## [12.2.2](https://github.com/appium/WebDriverAgent/compare/v12.2.1...v12.2.2) (2026-05-08) ### Bug Fixes * linter ([#1134](https://github.com/appium/WebDriverAgent/issues/1134)) ([2bd1816](https://github.com/appium/WebDriverAgent/commit/2bd181628a1d4525a8f1c459ea295ac0541b514c)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 612e4f68e..f6ac763c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [12.2.2](https://github.com/appium/WebDriverAgent/compare/v12.2.1...v12.2.2) (2026-05-08) + +### Bug Fixes + +* linter ([#1134](https://github.com/appium/WebDriverAgent/issues/1134)) ([2bd1816](https://github.com/appium/WebDriverAgent/commit/2bd181628a1d4525a8f1c459ea295ac0541b514c)) + ## [12.2.1](https://github.com/appium/WebDriverAgent/compare/v12.2.0...v12.2.1) (2026-05-06) ### Miscellaneous Chores diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index e78a8eaa5..1b74d462e 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 12.2.1 + 12.2.2 CFBundleSignature ???? CFBundleVersion - 12.2.1 + 12.2.2 NSPrincipalClass diff --git a/package.json b/package.json index 62c351330..e81f94a9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "12.2.1", + "version": "12.2.2", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 1791c801c4eae7acede6190279e14d954af5f3c4 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 12 May 2026 21:38:58 -0700 Subject: [PATCH 19/55] ci: add note about Xcode versions (#1136) * ci: add not about Xcode versions Added comments regarding Xcode version compatibility and simulator availability. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/publish.js.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.js.yml b/.github/workflows/publish.js.yml index 69b619f19..250a42e3e 100644 --- a/.github/workflows/publish.js.yml +++ b/.github/workflows/publish.js.yml @@ -15,8 +15,12 @@ permissions: id-token: write # to enable use of OIDC for trusted publishing and npm provenance env: + # DO NOT USE 26.4+ for a while since it could drop lower iOS versions forcefully + # while the project config allows such lower versions. + # (at least WDA failed to start on iOS 15) + # Xcode 26.3 looks like it's working as expected. XCODE_VERSION: '16.4' - # Available destination for simulators depend on Xcode version. + # Available destination for simulators depends on Xcode version. DESTINATION_SIM: platform=iOS Simulator,name=iPhone 17 DESTINATION_SIM_TVOS: platform=tvOS Simulator,name=Apple TV 4K (3rd generation) From 8995d24e16634a4624918319996839993502c7b4 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 17 May 2026 19:11:48 +0200 Subject: [PATCH 20/55] feat: Drop legacy APIs (#1137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: quitAndUninstall() removed — use quit() only. App uninstall is out of scope for this module. BREAKING CHANGE: uninstall() removed — WDA must not be uninstalled from this package; callers (e.g. xcuitest-driver) should own that if needed. BREAKING CHANGE: setupCaching() no longer uninstalls WDA — on bundle-id or version mismatch it logs and skips caching instead of removing apps from the device. Also, it now returns the cached url on success. BREAKING CHANGE: appium-ios-device dependency removed — preinstalled WDA on real devices always launches via devicectl (no iOS < 17 Xctest fallback). --- .github/workflows/functional-test.yml | 3 +- lib/appium-ios-device.d.ts | 7 - lib/index.ts | 2 +- lib/types.ts | 7 +- lib/webdriveragent.ts | 137 +++++--------------- package.json | 2 +- test/functional/helpers/simulator.ts | 2 +- test/functional/webdriveragent-e2e-specs.ts | 5 +- test/unit/webdriveragent-specs.ts | 128 ++++-------------- 9 files changed, 72 insertions(+), 221 deletions(-) delete mode 100644 lib/appium-ios-device.d.ts diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index d7647d57a..4291078fd 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -18,7 +18,7 @@ jobs: IOS_MODEL: 'iPhone 17' - HOST_OS: 'macos-15' XCODE_VERSION: '16.4' - IOS_VERSION: '18.4' + IOS_VERSION: '18.5' IOS_MODEL: 'iPhone 16 Plus' - HOST_OS: 'macos-14' XCODE_VERSION: '15.4' @@ -46,6 +46,7 @@ jobs: DEVICE_NAME: ${{matrix.test_targets.IOS_MODEL}} PLATFORM_VERSION: ${{matrix.test_targets.IOS_VERSION}} run: | + xcrun simctl list devices available open -Fn "$(xcode-select -p)/Applications/Simulator.app" udid=$(xcrun simctl list devices available -j | \ node -p "Object.entries(JSON.parse(fs.readFileSync(0)).devices).filter((x) => x[0].includes('$PLATFORM_VERSION'.replace('.', '-'))).reduce((acc, x) => [...acc, ...x[1]], []).find(({name}) => name === '$DEVICE_NAME').udid") diff --git a/lib/appium-ios-device.d.ts b/lib/appium-ios-device.d.ts deleted file mode 100644 index 5aa65bffd..000000000 --- a/lib/appium-ios-device.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module 'appium-ios-device' { - export class Xctest { - constructor(...args: any[]); - start(): Promise; - stop(): void; - } -} diff --git a/lib/index.ts b/lib/index.ts index 4d78ec618..0bb3f0806 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,7 +1,7 @@ export {bundleWDASim} from './check-dependencies'; export {NoSessionProxy} from './no-session-proxy'; export {WebDriverAgent} from './webdriveragent'; -export {WDA_BASE_URL, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE} from './constants'; +export {WDA_BASE_URL, WDA_RUNNER_APP, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE} from './constants'; export {resetTestProcesses, BOOTSTRAP_PATH} from './utils'; export * from './types'; diff --git a/lib/types.ts b/lib/types.ts index e748c3bd5..6778739c1 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,6 @@ import {type HTTPHeaders} from '@appium/types'; +import type {Simctl} from 'node-simctl'; +import type {Devicectl} from 'node-devicectl'; // WebDriverAgentLib/Utilities/FBSettings.h export interface WDASettings { @@ -99,9 +101,8 @@ export interface WebDriverAgentArgs { export interface AppleDevice { udid: string; - simctl?: any; - devicectl?: any; - [key: string]: any; + simctl?: Simctl; + devicectl?: Devicectl; } /** diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index b0d763506..750c30a27 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -1,7 +1,7 @@ import {waitForCondition} from 'asyncbox'; import path from 'node:path'; import {JWProxy} from '@appium/base-driver'; -import {fs, util, plist} from '@appium/support'; +import {fs, util} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; import {log as defaultLogger} from './logger'; import {NoSessionProxy} from './no-session-proxy'; @@ -14,21 +14,19 @@ import { import {XcodeBuild} from './xcodebuild'; import AsyncLock from 'async-lock'; import {exec} from 'teen_process'; -import {bundleWDASim} from './check-dependencies'; import { WDA_RUNNER_BUNDLE_ID, - WDA_RUNNER_APP, WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, DEFAULT_TEST_BUNDLE_SUFFIX, } from './constants'; -import {Xctest} from 'appium-ios-device'; import {strongbox} from '@appium/strongbox'; import type {WebDriverAgentArgs, AppleDevice} from './types'; +import type {Simctl} from 'node-simctl'; +import type {Devicectl} from 'node-devicectl'; const WDA_LAUNCH_TIMEOUT = 60 * 1000; const WDA_AGENT_PORT = 8100; -const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner'; const SHARED_RESOURCES_GUARD = new AsyncLock(); const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; const URL_PROTOCOL_SEPARATOR = '://'; @@ -64,7 +62,6 @@ export class WebDriverAgent { private readonly wdaLaunchTimeout: number; private readonly usePreinstalledWDA?: boolean; private readonly updatedWDABundleIdSuffix: string; - private xctestApiClient?: Xctest | null; private _xcodebuild?: XcodeBuild | null; private _url?: URL; @@ -112,7 +109,6 @@ export class WebDriverAgent { this.wdaLaunchTimeout = args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT; this.usePreinstalledWDA = args.usePreinstalledWDA; - this.xctestApiClient = null; this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? DEFAULT_TEST_BUNDLE_SUFFIX; this._xcodebuild = this.canSkipXcodebuild @@ -366,15 +362,14 @@ export class WebDriverAgent { async quit(): Promise { if (this.usePreinstalledWDA) { this.log.info('Stopping the XCTest session'); - if (this.xctestApiClient) { - this.xctestApiClient.stop(); - this.xctestApiClient = null; - } else { - try { + try { + if (this.device.simctl) { await this.device.simctl.terminateApp(this.bundleIdForXctest); - } catch (e: any) { - this.log.warn(e.message); + } else if (this.device.devicectl) { + await this.device.devicectl.terminateApp(this.bundleIdForXctest); } + } catch (e: any) { + this.log.warn(e.message); } } else if (!this.args.webDriverAgentUrl) { this.log.info('Shutting down sub-processes'); @@ -383,8 +378,7 @@ export class WebDriverAgent { } } else { this.log.debug( - 'Do not stop xcodebuild nor XCTest session ' + - 'since the WDA session is managed by outside this driver.', + 'Stopping neither xcodebuild nor XCTest session since WDA lifecycle is not managed by this driver', ); } @@ -415,13 +409,14 @@ export class WebDriverAgent { /** * Reuse running WDA if it has the same bundle id with updatedWDABundleId. * Or reuse it if it has the default id without updatedWDABundleId. - * Uninstall it if the method faces an exception for the above situation. + * + * @returns The WDA URL used for caching on success, or `undefined` if caching was skipped. */ - async setupCaching(): Promise { + async setupCaching(): Promise { const status = await this.getStatus(0); if (!status || !status.build) { this.log.debug('WDA is currently not running. There is nothing to cache'); - return; + return undefined; } const {productBundleIdentifier, upgradedAt} = status.build as any; @@ -432,9 +427,9 @@ export class WebDriverAgent { this.updatedWDABundleId !== productBundleIdentifier ) { this.log.info( - `Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`, + `Will not reuse running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`, ); - return await this.uninstall(); + return undefined; } // for simulator if ( @@ -443,9 +438,9 @@ export class WebDriverAgent { WDA_RUNNER_BUNDLE_ID !== productBundleIdentifier ) { this.log.info( - `Will uninstall running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`, + `Will not reuse running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`, ); - return await this.uninstall(); + return undefined; } const actualUpgradeTimestamp = await getWDAUpgradeTimestamp(); @@ -457,51 +452,21 @@ export class WebDriverAgent { `${actualUpgradeTimestamp}`.toLowerCase() !== `${upgradedAt}`.toLowerCase() ) { this.log.info( - 'Will uninstall running WDA since it has different version in comparison to the one ' + + 'Will not reuse running WDA since it has different version in comparison to the one ' + `which is bundled with appium-xcuitest-driver module (${actualUpgradeTimestamp} != ${upgradedAt})`, ); - return await this.uninstall(); + return undefined; } + const cachedUrl = this.url.href; const message = util.hasValue(productBundleIdentifier) - ? `Will reuse previously cached WDA instance at '${this.url.href}' with '${productBundleIdentifier}'` - : `Will reuse previously cached WDA instance at '${this.url.href}'`; + ? `Will reuse previously cached WDA instance at '${cachedUrl}' with '${productBundleIdentifier}'` + : `Will reuse previously cached WDA instance at '${cachedUrl}'`; this.log.info( `${message}. Set the wdaLocalPort capability to a value different from ${this.url.port} if this is an undesired behavior.`, ); - this.webDriverAgentUrl = this.url.href; - } - - /** - * Quit and uninstall running WDA. - */ - async quitAndUninstall(): Promise { - await this.quit(); - await this.uninstall(); - } - - private async parseBundleId(wdaBundlePath: string): Promise { - const infoPlistPath = path.join(wdaBundlePath, 'Info.plist'); - const infoPlist = (await plist.parsePlist(await fs.readFile(infoPlistPath))) as { - CFBundleIdentifier?: string; - }; - if (!infoPlist.CFBundleIdentifier) { - throw new Error(`Could not find bundle id in '${infoPlistPath}'`); - } - return infoPlist.CFBundleIdentifier; - } - - private async fetchWDABundle(): Promise { - if (!this.derivedDataPath) { - return await bundleWDASim(this.xcodebuild); - } - const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, { - absolute: true, - }); - if (wdaBundlePaths.length === 0) { - throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`); - } - return wdaBundlePaths[0]; + this.webDriverAgentUrl = cachedUrl; + return cachedUrl; } private setupProxies(sessionId: string): void { @@ -549,10 +514,6 @@ export class WebDriverAgent { this.log.info(`Using WDA agent: '${this.agentPath}'`); } - private async isRunning(): Promise { - return !!(await this.getStatus()); - } - /** * Return current running WDA's status like below * { @@ -626,32 +587,6 @@ export class WebDriverAgent { return status; } - /** - * Uninstall WDAs from the test device. - * Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA. - * Appium does not expect multiple WDAs are running on a device. - */ - private async uninstall(): Promise { - try { - const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME); - if (bundleIds.length === 0) { - this.log.debug('No WDAs on the device.'); - return; - } - - this.log.debug(`Uninstalling WDAs: '${bundleIds}'`); - for (const bundleId of bundleIds) { - await this.device.removeApp(bundleId); - } - } catch (e: any) { - this.log.debug(e); - this.log.warn( - `WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` + - `Original error: ${e.message}`, - ); - } - } - private async _cleanupProjectIfFresh(): Promise { if (this.canSkipXcodebuild) { return; @@ -725,9 +660,6 @@ export class WebDriverAgent { * https://github.com/appium/WebDriverAgent/releases * with proper sign for this case. * - * When we implement launching XCTest service via appium-ios-device, - * this implementation can be replaced with it. - * * @param opts launching WDA with devicectl command options. */ private async _launchViaDevicectl( @@ -735,7 +667,10 @@ export class WebDriverAgent { ): Promise { const {env} = opts; - await this.device.devicectl.launchApp(this.bundleIdForXctest, {env, terminateExisting: true}); + await (this.device.devicectl as Devicectl).launchApp(this.bundleIdForXctest, { + env, + terminateExisting: true, + }); } /** @@ -755,19 +690,9 @@ export class WebDriverAgent { } this.log.info('Launching WebDriverAgent on the device without xcodebuild'); if (this.isRealDevice) { - // Current method to launch WDA process can be done via 'xcrun devicectl', - // but it has limitation about the WDA preinstalled package. - // https://github.com/appium/appium/issues/19206#issuecomment-2014182674 - if (this.platformVersion && util.compareVersions(this.platformVersion, '>=', '17.0')) { - await this._launchViaDevicectl({env: xctestEnv}); - } else { - this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, { - env: xctestEnv, - }); - await this.xctestApiClient.start(); - } + await this._launchViaDevicectl({env: xctestEnv}); } else { - await this.device.simctl.exec('launch', { + await (this.device.simctl as Simctl).exec('launch', { args: ['--terminate-running-process', this.device.udid, this.bundleIdForXctest], env: xctestEnv, }); diff --git a/package.json b/package.json index e81f94a9a..26a0bb660 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "chai-as-promised": "^8.0.0", "conventional-changelog-conventionalcommits": "^9.0.0", "mocha": "^11.0.1", + "node-devicectl": "^1.4.0", "node-simctl": "^8.0.0", "prettier": "^3.0.0", "semantic-release": "^25.0.2", @@ -77,7 +78,6 @@ "@appium/base-driver": "^10.3.0", "@appium/strongbox": "^1.0.0-rc.1", "@appium/support": "^7.0.0-rc.1", - "appium-ios-device": "^3.0.0", "appium-ios-simulator": "^8.0.0", "async-lock": "^1.0.0", "asyncbox": "^6.1.0", diff --git a/test/functional/helpers/simulator.ts b/test/functional/helpers/simulator.ts index 040367f86..166d4910e 100644 --- a/test/functional/helpers/simulator.ts +++ b/test/functional/helpers/simulator.ts @@ -22,7 +22,7 @@ export async function killAllSimulators(): Promise { export async function shutdownSimulator(device: AppleDevice): Promise { // stop XCTest processes if running to avoid unexpected side effects await resetTestProcesses(device.udid, true); - await device.shutdown(); + await (device.simctl as Simctl).shutdownDevice(); } export async function deleteDeviceWithRetry(udid: string): Promise { diff --git a/test/functional/webdriveragent-e2e-specs.ts b/test/functional/webdriveragent-e2e-specs.ts index 5d98329c1..7eee7eac7 100644 --- a/test/functional/webdriveragent-e2e-specs.ts +++ b/test/functional/webdriveragent-e2e-specs.ts @@ -67,7 +67,10 @@ describe('WebDriverAgent', function () { this.timeout(6 * 60 * 1000); beforeEach(async function () { await killAllSimulators(); - await device.run({startupTimeout: SIM_STARTUP_TIMEOUT_MS}); + await (device.simctl as Simctl).startBootMonitor({ + shouldPreboot: true, + timeout: SIM_STARTUP_TIMEOUT_MS, + }); }); afterEach(async function () { try { diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index cb8cf611f..ec5433ada 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -5,15 +5,17 @@ import {WebDriverAgent} from '../../lib/webdriveragent'; import * as utils from '../../lib/utils'; import path from 'node:path'; import sinon from 'sinon'; -import type {WebDriverAgentArgs, AppleDevice} from '../../lib/types'; +import type {WebDriverAgentArgs} from '../../lib/types'; +import type {Simctl} from 'node-simctl'; +import type {Devicectl} from 'node-devicectl'; chai.use(chaiAsPromised); const fakeConstructorArgs: WebDriverAgentArgs = { device: { udid: 'some-sim-udid', - simctl: {}, - devicectl: {}, + simctl: {} as Simctl, + devicectl: {} as Devicectl, }, platformVersion: '9', host: 'me', @@ -231,48 +233,42 @@ describe('WebDriverAgent', function () { describe('setupCaching()', function () { let wda: WebDriverAgent; let wdaStub: sinon.SinonStub; - let wdaStubUninstall: sinon.SinonStub; const getTimestampStub = sinon.stub(utils, 'getWDAUpgradeTimestamp'); beforeEach(function () { wda = new WebDriverAgent(fakeConstructorArgs); wdaStub = sinon.stub(wda as any, 'getStatus'); - wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); }); afterEach(function () { - for (const stub of [wdaStub, wdaStubUninstall, getTimestampStub]) { + for (const stub of [wdaStub, getTimestampStub]) { if (stub) { stub.reset(); } } }); - it('should not call uninstall since no Running WDA', async function () { + it('should not cache when no WDA is running', async function () { wdaStub.callsFake(function () { return null; }); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.be.undefined; expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); - it('should not call uninstall since running WDA has only time', async function () { + it('should cache when running WDA has only time', async function () { wdaStub.callsFake(function () { return {build: {time: 'Jun 24 2018 17:08:21'}}; }); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); - it('should call uninstall once since bundle id is not default without updatedWDABundleId capability', async function () { + it('should not cache when bundle id is not default without updatedWDABundleId capability', async function () { wdaStub.callsFake(function () { return { build: { @@ -281,15 +277,13 @@ describe('WebDriverAgent', function () { }, }; }); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.be.undefined; expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); - it('should call uninstall once since bundle id is different with updatedWDABundleId capability', async function () { + it('should not cache when bundle id is different with updatedWDABundleId capability', async function () { wdaStub.callsFake(function () { return { build: { @@ -299,21 +293,17 @@ describe('WebDriverAgent', function () { }; }); - wdaStubUninstall.callsFake(() => {}); - - await wda.setupCaching(); + expect(await wda.setupCaching()).to.be.undefined; expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; expect(wda.webDriverAgentUrl === undefined).to.be.true; }); - it('should not call uninstall since bundle id is equal to updatedWDABundleId capability', async function () { + it('should cache when bundle id is equal to updatedWDABundleId capability', async function () { wda = new WebDriverAgent({ ...fakeConstructorArgs, updatedWDABundleId: 'com.example.WebDriverAgent', }); wdaStub = sinon.stub(wda as any, 'getStatus'); - wdaStubUninstall = sinon.stub(wda as any, 'uninstall'); wdaStub.callsFake(function () { return { @@ -324,115 +314,53 @@ describe('WebDriverAgent', function () { }; }); - wdaStubUninstall.callsFake(() => {}); - - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); - it('should call uninstall if current revision differs from the bundled one', async function () { + it('should not cache if current revision differs from the bundled one', async function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(async () => 2); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.be.undefined; expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.calledOnce).to.be.true; + expect(wda.webDriverAgentUrl === undefined).to.be.true; }); - it('should not call uninstall if current revision is the same as the bundled one', async function () { + it('should cache if current revision is the same as the bundled one', async function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(async () => 1); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; + expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); - it('should not call uninstall if current revision cannot be retrieved from WDA status', async function () { + it('should cache if current revision cannot be retrieved from WDA status', async function () { wdaStub.callsFake(function () { return {build: {}}; }); getTimestampStub.callsFake(async () => 1); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; + expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); - it('should not call uninstall if current revision cannot be retrieved from the file system', async function () { + it('should cache if current revision cannot be retrieved from the file system', async function () { wdaStub.callsFake(function () { return {build: {upgradedAt: '1'}}; }); getTimestampStub.callsFake(async () => null); - wdaStubUninstall.callsFake(() => {}); - await wda.setupCaching(); + expect(await wda.setupCaching()).to.equal('http://127.0.0.1:8100/'); expect(wdaStub.calledOnce).to.be.true; - expect(wdaStubUninstall.notCalled).to.be.true; - }); - - describe('uninstall', function () { - let device: AppleDevice; - let wda: WebDriverAgent; - let deviceGetBundleIdsStub: sinon.SinonStub; - let deviceRemoveAppStub: sinon.SinonStub; - - beforeEach(function () { - device = { - getUserInstalledBundleIdsByBundleName: () => {}, - removeApp: () => {}, - } as any; - wda = new WebDriverAgent({device} as WebDriverAgentArgs); - deviceGetBundleIdsStub = sinon.stub(device, 'getUserInstalledBundleIdsByBundleName'); - deviceRemoveAppStub = sinon.stub(device, 'removeApp'); - }); - - afterEach(function () { - for (const stub of [deviceGetBundleIdsStub, deviceRemoveAppStub]) { - if (stub) { - stub.reset(); - } - } - }); - - it('should not call uninstall', async function () { - deviceGetBundleIdsStub.callsFake(() => []); - - await (wda as any).uninstall(); - expect(deviceGetBundleIdsStub.calledOnce).to.be.true; - expect(deviceRemoveAppStub.notCalled).to.be.true; - }); - - it('should call uninstall once', async function () { - const uninstalledBundIds: string[] = []; - deviceGetBundleIdsStub.callsFake(() => ['com.appium.WDA1']); - deviceRemoveAppStub.callsFake((id: string) => uninstalledBundIds.push(id)); - - await (wda as any).uninstall(); - expect(deviceGetBundleIdsStub.calledOnce).to.be.true; - expect(deviceRemoveAppStub.calledOnce).to.be.true; - expect(uninstalledBundIds).to.eql(['com.appium.WDA1']); - }); - - it('should call uninstall twice', async function () { - const uninstalledBundIds: string[] = []; - deviceGetBundleIdsStub.callsFake(() => ['com.appium.WDA1', 'com.appium.WDA2']); - deviceRemoveAppStub.callsFake((id: string) => uninstalledBundIds.push(id)); - - await (wda as any).uninstall(); - expect(deviceGetBundleIdsStub.calledOnce).to.be.true; - expect(deviceRemoveAppStub.calledTwice).to.be.true; - expect(uninstalledBundIds).to.eql(['com.appium.WDA1', 'com.appium.WDA2']); - }); + expect(wda.webDriverAgentUrl).to.equal('http://127.0.0.1:8100/'); }); }); From daad857ce1bb5a6ebae4c2c7e3db971e81c7d4b1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 17 May 2026 17:15:29 +0000 Subject: [PATCH 21/55] chore(release): 13.0.0 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [13.0.0](https://github.com/appium/WebDriverAgent/compare/v12.2.2...v13.0.0) (2026-05-17) ### ⚠ BREAKING CHANGES * quitAndUninstall() removed — use quit() only. App uninstall is out of scope for this module. * uninstall() removed — WDA must not be uninstalled from this package; callers (e.g. xcuitest-driver) should own that if needed. * setupCaching() no longer uninstalls WDA — on bundle-id or version mismatch it logs and skips caching instead of removing apps from the device. Also, it now returns the cached url on success. * appium-ios-device dependency removed — preinstalled WDA on real devices always launches via devicectl (no iOS < 17 Xctest fallback). ### Features * Drop legacy APIs ([#1137](https://github.com/appium/WebDriverAgent/issues/1137)) ([8995d24](https://github.com/appium/WebDriverAgent/commit/8995d24e16634a4624918319996839993502c7b4)) --- CHANGELOG.md | 13 +++++++++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ac763c2..7ebd15997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [13.0.0](https://github.com/appium/WebDriverAgent/compare/v12.2.2...v13.0.0) (2026-05-17) + +### ⚠ BREAKING CHANGES + +* quitAndUninstall() removed — use quit() only. App uninstall is out of scope for this module. +* uninstall() removed — WDA must not be uninstalled from this package; callers (e.g. xcuitest-driver) should own that if needed. +* setupCaching() no longer uninstalls WDA — on bundle-id or version mismatch it logs and skips caching instead of removing apps from the device. Also, it now returns the cached url on success. +* appium-ios-device dependency removed — preinstalled WDA on real devices always launches via devicectl (no iOS < 17 Xctest fallback). + +### Features + +* Drop legacy APIs ([#1137](https://github.com/appium/WebDriverAgent/issues/1137)) ([8995d24](https://github.com/appium/WebDriverAgent/commit/8995d24e16634a4624918319996839993502c7b4)) + ## [12.2.2](https://github.com/appium/WebDriverAgent/compare/v12.2.1...v12.2.2) (2026-05-08) ### Bug Fixes diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 1b74d462e..635efa166 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 12.2.2 + 13.0.0 CFBundleSignature ???? CFBundleVersion - 12.2.2 + 13.0.0 NSPrincipalClass diff --git a/package.json b/package.json index 26a0bb660..74459b0aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "12.2.2", + "version": "13.0.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 56b5f384ed9ba1a014d4b642ddf26b8573ceaafe Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Thu, 21 May 2026 05:05:14 +0200 Subject: [PATCH 22/55] feat: Add helper method to fetch build settings (#1139) --- lib/types.ts | 29 +++++++ lib/webdriveragent.ts | 27 ++++-- lib/xcodebuild.ts | 140 ++++++++++++++++++++++-------- test/unit/webdriveragent-specs.ts | 4 +- 4 files changed, 156 insertions(+), 44 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 6778739c1..f3604718f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -115,6 +115,35 @@ export interface DeviceInfo { platformName: string; } +/** Xcode build setting key/value pairs from `xcodebuild -showBuildSettings -json`. */ +export type XcodeBuildSettings = Record; + +/** A single target entry returned by `xcodebuild -showBuildSettings -json`. */ +export interface XcodeShowBuildSettingsEntry { + action: string; + buildSettings: XcodeBuildSettings; + target: string; +} + +export type WdaScheme = + | 'WebDriverAgentRunner' + | 'WebDriverAgentLib' + | 'WebDriverAgentRunner_tvOS' + | 'WebDriverAgentLib_tvOS'; + +export type WdaSdk = 'iphonesimulator' | 'iphoneos' | 'appletvsimulator' | 'appletvos'; + +export type WdaBuildConfiguration = 'Debug' | 'Release'; + +/** Options passed to {@link XcodeBuild.retrieveBuildSettings}. */ +export interface RetrieveBuildSettingsOptions { + scheme?: WdaScheme; + sdk?: WdaSdk; + configuration?: WdaBuildConfiguration; + /** `-destination` value (e.g. `id=` or a full destination specifier). */ + destination?: string; +} + export interface XcodeBuildArgs { realDevice: boolean; // Required agentPath: string; // Required diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 750c30a27..95216023d 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -21,7 +21,12 @@ import { DEFAULT_TEST_BUNDLE_SUFFIX, } from './constants'; import {strongbox} from '@appium/strongbox'; -import type {WebDriverAgentArgs, AppleDevice} from './types'; +import type { + WebDriverAgentArgs, + AppleDevice, + XcodeBuildSettings, + RetrieveBuildSettingsOptions, +} from './types'; import type {Simctl} from 'node-simctl'; import type {Devicectl} from 'node-devicectl'; @@ -51,13 +56,11 @@ export class WebDriverAgent { jwproxy?: JWProxy; proxyReqRes?: any; private readonly log: AppiumLogger; - private readonly wdaBundlePath?: string; private readonly wdaLocalPort?: number; private readonly prebuildWDA?: boolean; private readonly wdaConnectionTimeout?: number; private readonly useXctestrunFile?: boolean; private readonly usePrebuiltWDA?: boolean; - private readonly derivedDataPath?: string; private readonly mjpegServerPort?: number; private readonly wdaLaunchTimeout: number; private readonly usePreinstalledWDA?: boolean; @@ -80,7 +83,6 @@ export class WebDriverAgent { this.iosSdkVersion = args.iosSdkVersion; this.host = args.host; this.isRealDevice = !!args.realDevice; - this.wdaBundlePath = args.wdaBundlePath; this.setWDAPaths(args.bootstrapPath, args.agentPath); @@ -102,7 +104,6 @@ export class WebDriverAgent { this.useXctestrunFile = args.useXctestrunFile; this.usePrebuiltWDA = args.usePrebuiltWDA; - this.derivedDataPath = args.derivedDataPath; this.mjpegServerPort = args.mjpegServerPort; this.updatedWDABundleId = args.updatedWDABundleId; @@ -396,7 +397,21 @@ export class WebDriverAgent { } /** - * Retrieves the Xcode derived data path for WebDriverAgent. + * Retrieves Xcode build settings. + * @param options - Optional scheme, SDK, configuration, or destination + * @returns Build settings, or `undefined` if xcodebuild is skipped or settings cannot be determined + */ + async retrieveBuildSettings( + options?: RetrieveBuildSettingsOptions, + ): Promise { + if (this.canSkipXcodebuild) { + return; + } + return await this.xcodebuild.retrieveBuildSettings(options); + } + + /** + * @deprecated Use {@link retrieveBuildSettings} instead. Will be removed in a future release. * @returns The derived data path, or `undefined` if xcodebuild is skipped */ async retrieveDerivedDataPath(): Promise { diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 00899abdd..14d66ff1d 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -14,7 +14,13 @@ import { } from './utils'; import path from 'node:path'; import {WDA_RUNNER_BUNDLE_ID} from './constants'; -import type {AppleDevice, XcodeBuildArgs} from './types'; +import type { + AppleDevice, + RetrieveBuildSettingsOptions, + XcodeBuildArgs, + XcodeBuildSettings, + XcodeShowBuildSettingsEntry, +} from './types'; import type {NoSessionProxy} from './no-session-proxy'; const DEFAULT_SIGNING_ID = 'iPhone Developer'; @@ -42,7 +48,6 @@ const REAL_DEVICES_CONFIG_DOCS_LINK = const xcodeLog = logger.getLogger('Xcode'); export class XcodeBuild { - xcodebuild?: SubProcess; readonly device: AppleDevice; readonly realDevice: boolean; readonly agentPath: string; @@ -51,9 +56,9 @@ export class XcodeBuild { readonly platformName?: string; readonly iosSdkVersion?: string; readonly xcodeSigningId: string; - usePrebuiltWDA?: boolean; - derivedDataPath?: string; - agentUrl?: string; + private xcodebuild?: SubProcess; + private usePrebuiltWDA?: boolean; + private derivedDataPath?: string; private readonly log: AppiumLogger; private readonly showXcodeLog?: boolean; private readonly xcodeConfigFile?: string; @@ -73,7 +78,10 @@ export class XcodeBuild { private readonly resultBundleVersion?: string; private _didBuildFail: boolean; private _didProcessExit: boolean; - private _derivedDataPathPromise?: Promise; + private readonly _buildSettingsPromises = new Map< + string, + Promise + >(); private noSessionProxy?: NoSessionProxy; private xctestrunFilePath?: string; @@ -158,8 +166,23 @@ export class XcodeBuild { } /** - * Retrieves the Xcode derived data path for the build. - * Uses cached value if available, otherwise queries xcodebuild for BUILD_DIR. + * Retrieves Xcode build settings via `xcodebuild -showBuildSettings -json`. + * @param options - Optional scheme, SDK, configuration, or destination + * @returns Build settings for the `build` action, or `undefined` if they cannot be determined + */ + async retrieveBuildSettings( + options?: RetrieveBuildSettingsOptions, + ): Promise { + const cacheKey = buildSettingsCacheKey(options); + let promise = this._buildSettingsPromises.get(cacheKey); + if (!promise) { + promise = this.fetchBuildSettings(options); + this._buildSettingsPromises.set(cacheKey, promise); + } + return await promise; + } + + /** * @returns The derived data path, or `undefined` if it cannot be determined */ async retrieveDerivedDataPath(): Promise { @@ -167,33 +190,18 @@ export class XcodeBuild { return this.derivedDataPath; } - // avoid race conditions - if (this._derivedDataPathPromise) { - return await this._derivedDataPathPromise; + const buildSettings = await this.retrieveBuildSettings(); + const buildDir = buildSettings?.BUILD_DIR; + if (!buildDir) { + this.log.warn('Cannot parse WDA BUILD_DIR from build settings'); + return; } - this._derivedDataPathPromise = (async () => { - let stdout: string; - try { - ({stdout} = await exec('xcodebuild', ['-project', this.agentPath, '-showBuildSettings'])); - } catch (err: any) { - this.log.warn(`Cannot retrieve WDA build settings. Original error: ${err.message}`); - return; - } - - const pattern = /^\s*BUILD_DIR\s+=\s+(\/.*)/m; - const match = pattern.exec(stdout); - if (!match) { - this.log.warn(`Cannot parse WDA build dir from ${truncateString(stdout, 300)}`); - return; - } - this.log.debug(`Parsed BUILD_DIR configuration value: '${match[1]}'`); - // Derived data root is two levels higher over the build dir - this.derivedDataPath = path.dirname(path.dirname(path.normalize(match[1]))); - this.log.debug(`Got derived data root: '${this.derivedDataPath}'`); - return this.derivedDataPath; - })(); - return await this._derivedDataPathPromise; + this.log.debug(`Parsed BUILD_DIR configuration value: '${buildDir}'`); + // Derived data root is two levels higher over the build dir + this.derivedDataPath = path.dirname(path.dirname(path.normalize(buildDir))); + this.log.debug(`Got derived data root: '${this.derivedDataPath}'`); + return this.derivedDataPath; } /** @@ -297,6 +305,45 @@ export class XcodeBuild { await killProcess('xcodebuild', this.xcodebuild); } + private async fetchBuildSettings( + options?: RetrieveBuildSettingsOptions, + ): Promise { + const schemeLabel = options?.scheme ?? 'default'; + let stdout: string; + try { + ({stdout} = await exec('xcodebuild', [ + '-project', + this.agentPath, + '-showBuildSettings', + '-json', + ...buildSettingsArgsFromOptions(options), + ])); + } catch (err: any) { + this.log.warn( + `Cannot retrieve WDA build settings for scheme '${schemeLabel}'. Original error: ${err.message}`, + ); + return; + } + + let entries: XcodeShowBuildSettingsEntry[]; + try { + entries = JSON.parse(stdout) as XcodeShowBuildSettingsEntry[]; + } catch (err: any) { + this.log.warn( + `Cannot parse WDA build settings for scheme '${schemeLabel}' from ${truncateString(stdout, 300)}. ` + + `Original error: ${err.message}`, + ); + return; + } + + const entry = entries.find(({action}) => action === 'build') ?? entries[0]; + if (!entry?.buildSettings) { + this.log.warn(`Cannot find build settings for scheme '${schemeLabel}'`); + return; + } + return entry.buildSettings; + } + private getCommand(buildOnly: boolean = false): {cmd: string; args: string[]} { const cmd = 'xcodebuild'; const args: string[] = []; @@ -465,9 +512,6 @@ export class XcodeBuild { (noSessionProxy as any).timeout = 1000; try { currentStatus = (await noSessionProxy.command('/status', 'GET')) as StringRecord; - if (currentStatus?.ios?.ip) { - this.agentUrl = currentStatus.ios.ip as string; - } this.log.debug(`WebDriverAgent information:`); this.log.debug(JSON.stringify(currentStatus, null, 2)); } catch (err: any) { @@ -498,3 +542,27 @@ export class XcodeBuild { return currentStatus; } } + +function buildSettingsArgsFromOptions(options?: RetrieveBuildSettingsOptions): string[] { + const args: string[] = []; + if (!options) { + return args; + } + if (options.scheme) { + args.push('-scheme', options.scheme); + } + if (options.sdk) { + args.push('-sdk', options.sdk); + } + if (options.configuration) { + args.push('-configuration', options.configuration); + } + if (options.destination) { + args.push('-destination', options.destination); + } + return args; +} + +function buildSettingsCacheKey(options?: RetrieveBuildSettingsOptions): string { + return buildSettingsArgsFromOptions(options).join('\0'); +} diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index ec5433ada..560de6dfc 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -51,13 +51,13 @@ describe('WebDriverAgent', function () { expect(agent.bootstrapPath).to.eql(customBootstrapPath); expect(agent.agentPath).to.eql(customAgentPath); }); - it('should have custom derivedDataPath if specified', function () { + it('should have custom derivedDataPath if specified', async function () { const agent = new WebDriverAgent({ ...fakeConstructorArgs, derivedDataPath: customDerivedDataPath, }); if (agent.xcodebuild) { - expect(agent.xcodebuild.derivedDataPath).to.eql(customDerivedDataPath); + expect(await agent.retrieveDerivedDataPath()).to.eql(customDerivedDataPath); } }); }); From fe8adc89923994428783397170de850e11ebb3c6 Mon Sep 17 00:00:00 2001 From: Sri Harsha <12621691+harsha509@users.noreply.github.com> Date: Wed, 20 May 2026 23:05:49 -0400 Subject: [PATCH 23/55] feat: add app icon to WebDriverAgentRunner (#1138) --- Scripts/embed-runner-icon.sh | 87 ++++++++++++++++++ WebDriverAgent.xcodeproj/project.pbxproj | 6 ++ .../xcschemes/WebDriverAgentRunner.xcscheme | 18 ++++ .../AppIcon.appiconset/Contents.json | 14 +++ .../AppIcon.appiconset/icon-1024.png | Bin 0 -> 63450 bytes .../Assets.xcassets/Contents.json | 6 ++ 6 files changed, 131 insertions(+) create mode 100755 Scripts/embed-runner-icon.sh create mode 100644 WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/icon-1024.png create mode 100644 WebDriverAgentRunner/Assets.xcassets/Contents.json diff --git a/Scripts/embed-runner-icon.sh b/Scripts/embed-runner-icon.sh new file mode 100755 index 000000000..b71ba0ab5 --- /dev/null +++ b/Scripts/embed-runner-icon.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Embed the WDA app icon into the wrapping XCTRunner host app so the +# installed WebDriverAgent shows the Appium logo on the iOS home screen +# instead of a blank icon. +# +# Apple's USES_XCTRUNNER auto-generates a Runner.app around UI-testing +# .xctest bundles but does not inherit icons from the test bundle's +# asset catalog. actool produces AppIcon*.png + Assets.car inside +# PlugIns/.xctest/ where iOS never looks. This script lifts +# them up to the Runner.app root and patches Info.plist accordingly. +# +# Limitations: +# - Touches XCTRunner internals; may need updates across Xcode versions. +# - iOS only; tvOS uses different "Brand Assets" and is not handled. +# - Cloud device farms that re-sign WDA must preserve these changes. + +set -euo pipefail + +RUNNER_APP="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}-Runner.app" +XCTEST="${RUNNER_APP}/PlugIns/${PRODUCT_NAME}.xctest" + +if [ ! -d "$RUNNER_APP" ]; then + echo "warning: ${PRODUCT_NAME}-Runner.app not found at $RUNNER_APP; skipping icon embed" + exit 0 +fi + +if [ ! -d "$XCTEST" ]; then + echo "warning: ${PRODUCT_NAME}.xctest not found inside Runner.app; skipping icon embed" + exit 0 +fi + +shopt -s nullglob +ICONS=("$XCTEST"/AppIcon*.png) +if [ ${#ICONS[@]} -eq 0 ]; then + echo "warning: no compiled AppIcon*.png found inside $XCTEST; skipping icon embed" + exit 0 +fi + +cp -f "${ICONS[@]}" "$RUNNER_APP/" +if [ -f "$XCTEST/Assets.car" ]; then + cp -f "$XCTEST/Assets.car" "$RUNNER_APP/" +fi + +PLIST="$RUNNER_APP/Info.plist" +/usr/libexec/PlistBuddy -c "Delete :CFBundleIcons" "$PLIST" 2>/dev/null || true +/usr/libexec/PlistBuddy -c "Delete :CFBundleIcons~ipad" "$PLIST" 2>/dev/null || true + +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons dict" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon dict" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconName string AppIcon" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles array" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles:0 string AppIcon60x60" "$PLIST" + +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad dict" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon dict" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon:CFBundleIconName string AppIcon" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon:CFBundleIconFiles array" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon:CFBundleIconFiles:0 string AppIcon60x60" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon:CFBundleIconFiles:1 string AppIcon76x76" "$PLIST" + +# Re-codesign since we modified the bundle after Xcode signed it. +# In a scheme post-action context Xcode's CODE_SIGN_* env vars are not exposed, +# so discover the existing signing identity from the already-signed bundle. +if [ -d "$RUNNER_APP/_CodeSignature" ]; then + # Capture the signature info once. Piping codesign straight into + # `awk ... exit` makes awk close the pipe early, killing codesign with + # SIGPIPE -- which `set -o pipefail` turns into a fatal error. That trips + # only when an Authority line exists, i.e. on every real-device build. + SIGN_INFO=$(codesign -dvv "$RUNNER_APP" 2>&1 || true) + EXISTING_IDENT="${EXPANDED_CODE_SIGN_IDENTITY:-}" + if [ -z "$EXISTING_IDENT" ]; then + EXISTING_IDENT=$(awk -F'=' '/^Authority/ {print $2; exit}' <<< "$SIGN_INFO") + fi + # Simulator builds are ad-hoc signed: there is no Authority line, but the + # bundle can still be re-signed ad-hoc with an identity of "-". + if [ -z "$EXISTING_IDENT" ] && grep -q '^Signature=adhoc' <<< "$SIGN_INFO"; then + EXISTING_IDENT="-" + fi + if [ -n "$EXISTING_IDENT" ]; then + codesign --force --sign "$EXISTING_IDENT" \ + --preserve-metadata=identifier,entitlements "$RUNNER_APP" + else + echo "warning: bundle is signed but no identity discovered; signature will be invalid" + fi +fi + +echo "embedded icon into $RUNNER_APP" diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index 2d353269e..82f506bb8 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -826,6 +826,7 @@ F59CD6D52EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */ = {isa = PBXBuildFile; fileRef = F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */; }; F59CD6D62EF16E5E00F91287 /* XCUIElement+FBCustomActions.h in Headers */ = {isa = PBXBuildFile; fileRef = F59CD6D22EF16E5E00F91287 /* XCUIElement+FBCustomActions.h */; }; F59CD6D72EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */ = {isa = PBXBuildFile; fileRef = F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */; }; + A1B2C3D4E5F600000000001B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F600000000001A /* Assets.xcassets */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1368,6 +1369,7 @@ EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRuntimeUtils.m; sourceTree = ""; }; EE9AB7FC1CAEE048008C271F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = WebDriverAgentRunner/Info.plist; sourceTree = SOURCE_ROOT; }; EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UITestingUITests.m; path = WebDriverAgentRunner/UITestingUITests.m; sourceTree = SOURCE_ROOT; }; + A1B2C3D4E5F600000000001A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = WebDriverAgentRunner/Assets.xcassets; sourceTree = SOURCE_ROOT; }; EE9AB8031CAEE182008C271F /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; EE9B75D41CF7956C00275851 /* IntegrationApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; EE9B75EC1CF7956C00275851 /* IntegrationTests_1.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_1.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2294,6 +2296,7 @@ children = ( EE9AB7FC1CAEE048008C271F /* Info.plist */, EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */, + A1B2C3D4E5F600000000001A /* Assets.xcassets */, ); name = WebDriverAgentRunner; path = XCTUITestRunner; @@ -3110,6 +3113,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1B2C3D4E5F600000000001B /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4346,6 +4350,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_STATIC_ANALYZER_MODE = deep; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_TESTING_SEARCH_PATHS = YES; @@ -4399,6 +4404,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_STATIC_ANALYZER_MODE = deep; ENABLE_TESTING_SEARCH_PATHS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme index da77fd577..157829787 100644 --- a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + I@SHKcW`RoDAHd6-ms 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 z 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 SJs<7Ay{oLLRH*p$#s2}mrgG~5 literal 0 HcmV?d00001 diff --git a/WebDriverAgentRunner/Assets.xcassets/Contents.json b/WebDriverAgentRunner/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/WebDriverAgentRunner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} From b60e3c3bac99dd4c0e86d8b250528673013a3bb0 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 21 May 2026 03:11:11 +0000 Subject: [PATCH 24/55] chore(release): 13.1.0 [skip ci] ## [13.1.0](https://github.com/appium/WebDriverAgent/compare/v13.0.0...v13.1.0) (2026-05-21) ### Features * add app icon to WebDriverAgentRunner ([#1138](https://github.com/appium/WebDriverAgent/issues/1138)) ([fe8adc8](https://github.com/appium/WebDriverAgent/commit/fe8adc89923994428783397170de850e11ebb3c6)) * Add helper method to fetch build settings ([#1139](https://github.com/appium/WebDriverAgent/issues/1139)) ([56b5f38](https://github.com/appium/WebDriverAgent/commit/56b5f384ed9ba1a014d4b642ddf26b8573ceaafe)) --- CHANGELOG.md | 7 +++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ebd15997..eddc8e3f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [13.1.0](https://github.com/appium/WebDriverAgent/compare/v13.0.0...v13.1.0) (2026-05-21) + +### Features + +* add app icon to WebDriverAgentRunner ([#1138](https://github.com/appium/WebDriverAgent/issues/1138)) ([fe8adc8](https://github.com/appium/WebDriverAgent/commit/fe8adc89923994428783397170de850e11ebb3c6)) +* Add helper method to fetch build settings ([#1139](https://github.com/appium/WebDriverAgent/issues/1139)) ([56b5f38](https://github.com/appium/WebDriverAgent/commit/56b5f384ed9ba1a014d4b642ddf26b8573ceaafe)) + ## [13.0.0](https://github.com/appium/WebDriverAgent/compare/v12.2.2...v13.0.0) (2026-05-17) ### ⚠ BREAKING CHANGES diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 635efa166..052aa3e91 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.0.0 + 13.1.0 CFBundleSignature ???? CFBundleVersion - 13.0.0 + 13.1.0 NSPrincipalClass diff --git a/package.json b/package.json index 74459b0aa..5dfb8daba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.0.0", + "version": "13.1.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 17ac1c16a0890ee0fbfe73504a3ff570dfe1a7bf Mon Sep 17 00:00:00 2001 From: Sri Harsha <12621691+harsha509@users.noreply.github.com> Date: Fri, 22 May 2026 02:17:21 -0400 Subject: [PATCH 25/55] fix: ship Scripts/embed-runner-icon.sh in the npm package (#1141) #1138 added Scripts/embed-runner-icon.sh and wired it as a WebDriverAgentRunner build post-action, but the package.json `files` allowlist only ships `Scripts/build.sh` and `Scripts/*.mjs`. The new script matches neither pattern, so it is excluded from the published npm tarball. As a result, builds from the published appium-webdriveragent package run the scheme post-action against a missing script: the Appium app icon is never embedded into Runner.app, and WebDriverAgentRunner installs on the home screen with a blank/white icon. Add an explicit `Scripts/embed-runner-icon.sh` entry to `files` so the script ships in the package. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 5dfb8daba..e5a4cfe9a 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "lib", "build/lib", "Scripts/build.sh", + "Scripts/embed-runner-icon.sh", "Scripts/*.mjs", "Configurations", "PrivateHeaders", From 3b78b0ee716e48c8b6b49d482d8ca3644beb7436 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 22 May 2026 06:48:04 +0000 Subject: [PATCH 26/55] chore(release): 13.1.1 [skip ci] ## [13.1.1](https://github.com/appium/WebDriverAgent/compare/v13.1.0...v13.1.1) (2026-05-22) ### Bug Fixes * ship Scripts/embed-runner-icon.sh in the npm package ([#1141](https://github.com/appium/WebDriverAgent/issues/1141)) ([17ac1c1](https://github.com/appium/WebDriverAgent/commit/17ac1c16a0890ee0fbfe73504a3ff570dfe1a7bf)), closes [#1138](https://github.com/appium/WebDriverAgent/issues/1138) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eddc8e3f6..6475a676e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.1.1](https://github.com/appium/WebDriverAgent/compare/v13.1.0...v13.1.1) (2026-05-22) + +### Bug Fixes + +* ship Scripts/embed-runner-icon.sh in the npm package ([#1141](https://github.com/appium/WebDriverAgent/issues/1141)) ([17ac1c1](https://github.com/appium/WebDriverAgent/commit/17ac1c16a0890ee0fbfe73504a3ff570dfe1a7bf)), closes [#1138](https://github.com/appium/WebDriverAgent/issues/1138) + ## [13.1.0](https://github.com/appium/WebDriverAgent/compare/v13.0.0...v13.1.0) (2026-05-21) ### Features diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 052aa3e91..8d74f79c8 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.1.0 + 13.1.1 CFBundleSignature ???? CFBundleVersion - 13.1.0 + 13.1.1 NSPrincipalClass diff --git a/package.json b/package.json index e5a4cfe9a..2f2a8314a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.1.0", + "version": "13.1.1", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From f1f9976f4a0a0fb8a8aa3ee1f2483b25275600e6 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 23 May 2026 21:00:55 +0200 Subject: [PATCH 27/55] fix: Address compilation warnings (#1143) --- .gitignore | 1 + .../Categories/XCUIDevice+FBHelpers.m | 2 +- .../Categories/XCUIDevice+FBRotation.m | 25 +++++- .../Categories/XCUIElement+FBUtilities.m | 2 +- .../Categories/XCUIElement+FBVisibleFrame.m | 15 +++- WebDriverAgentLib/Commands/FBCustomCommands.m | 4 +- .../Commands/FBElementCommands.m | 2 +- .../Commands/FBOrientationCommands.m | 2 +- .../Commands/FBScreenshotCommands.m | 2 +- .../Routing/FBScreenRecordingRequest.m | 2 +- WebDriverAgentLib/Routing/FBTCPSocket.m | 2 +- .../Utilities/FBClassChainQueryParser.m | 2 +- .../Utilities/FBImageProcessor.m | 15 ++-- WebDriverAgentLib/Utilities/FBMjpegServer.m | 4 +- WebDriverAgentLib/Utilities/FBScreenshot.m | 2 +- .../Utilities/XCTestPrivateSymbols.h | 4 +- .../Utilities/XCTestPrivateSymbols.m | 83 +++++++++++-------- .../Vendor/CocoaAsyncSocket/GCDAsyncSocket.m | 32 +++---- .../CocoaAsyncSocket/GCDAsyncUdpSocket.m | 24 +++--- .../CocoaHTTPServer/Categories/DDRange.m | 18 ++-- .../Vendor/CocoaHTTPServer/HTTPLogging.h | 24 +++--- .../Vendor/CocoaHTTPServer/HTTPMessage.m | 4 +- .../RoutingHTTPServer/RoutingHTTPServer.m | 10 +-- 23 files changed, 162 insertions(+), 119 deletions(-) diff --git a/.gitignore b/.gitignore index bc5a943fe..75359860f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/ clang/ DerivedData +wdaBuild/ ## Various settings *.pbxuser diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index 7835a1403..bcde01972 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -232,7 +232,7 @@ - (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error NSString *description = [NSString stringWithFormat:@"Cannot open '%@' with the default application assigned for it. Consider upgrading to Xcode 14.3+/iOS 16.4+", url]; return [[[FBErrorBuilder builder] withDescriptionFormat:@"%@", description] - buildError:error];; + buildError:error]; } - (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m index 164b23e7b..269d77f6c 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m @@ -30,19 +30,38 @@ - (BOOL)fb_setDeviceRotation:(NSDictionary *)rotationObj if (keysForRotationObj.count == 0) { return NO; } - NSInteger orientation = keysForRotationObj.firstObject.integerValue; + UIDeviceOrientation orientation = (UIDeviceOrientation)keysForRotationObj.firstObject.integerValue; XCUIApplication *application = XCUIApplication.fb_activeApplication; [XCUIDevice sharedDevice].orientation = orientation; return [self waitUntilInterfaceIsAtOrientation:orientation application:application]; } -- (BOOL)waitUntilInterfaceIsAtOrientation:(NSInteger)orientation application:(XCUIApplication *)application +static UIInterfaceOrientation FBInterfaceOrientationFromDeviceOrientation(UIDeviceOrientation orientation) +{ + switch (orientation) { + case UIDeviceOrientationPortrait: + return UIInterfaceOrientationPortrait; + case UIDeviceOrientationPortraitUpsideDown: + return UIInterfaceOrientationPortraitUpsideDown; + case UIDeviceOrientationLandscapeLeft: + return UIInterfaceOrientationLandscapeRight; + case UIDeviceOrientationLandscapeRight: + return UIInterfaceOrientationLandscapeLeft; + case UIDeviceOrientationUnknown: + case UIDeviceOrientationFaceUp: + case UIDeviceOrientationFaceDown: + default: + return UIInterfaceOrientationUnknown; + } +} + +- (BOOL)waitUntilInterfaceIsAtOrientation:(UIDeviceOrientation)orientation application:(XCUIApplication *)application { // Tapping elements immediately after rotation may fail due to way UIKit is handling touches. // We should wait till UI cools off, before continuing [application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; - return application.interfaceOrientation == orientation; + return application.interfaceOrientation == FBInterfaceOrientationFromDeviceOrientation(orientation); } - (NSDictionary *)fb_rotationMapping diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m index b627fc862..35b9b7da5 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m @@ -113,7 +113,7 @@ @implementation XCUIElement (FBUtilities) XCUIElementType type = XCUIElementTypeAny; NSArray *uniqueTypes = [snapshots valueForKeyPath:[NSString stringWithFormat:@"@distinctUnionOfObjects.%@", FBStringify(XCUIElement, elementType)]]; if (uniqueTypes && [uniqueTypes count] == 1) { - type = [uniqueTypes.firstObject intValue]; + type = (XCUIElementType)[uniqueTypes.firstObject intValue]; } XCUIElementQuery *query = onlyChildren ? [self.fb_query childrenMatchingType:type] diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m index 8ba5b0eac..09ce6a769 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m @@ -29,13 +29,19 @@ - (CGRect)fb_visibleFrame { CGRect thisVisibleFrame = [self visibleFrame]; if (!CGRectIsEmpty(thisVisibleFrame)) { - return thisVisibleFrame; + return CGRectMake(CGRectGetMinX(thisVisibleFrame), + CGRectGetMinY(thisVisibleFrame), + CGRectGetWidth(thisVisibleFrame), + CGRectGetHeight(thisVisibleFrame)); } NSDictionary *visibleFrameDict = [self fb_attributeValue:FB_XCAXAVisibleFrameAttributeName error:nil]; if (nil == visibleFrameDict) { - return thisVisibleFrame; + return CGRectMake(CGRectGetMinX(thisVisibleFrame), + CGRectGetMinY(thisVisibleFrame), + CGRectGetWidth(thisVisibleFrame), + CGRectGetHeight(thisVisibleFrame)); } id x = [visibleFrameDict objectForKey:@"X"]; @@ -46,7 +52,10 @@ - (CGRect)fb_visibleFrame return CGRectMake([x doubleValue], [y doubleValue], [width doubleValue], [height doubleValue]); } - return thisVisibleFrame; + return CGRectMake(CGRectGetMinX(thisVisibleFrame), + CGRectGetMinY(thisVisibleFrame), + CGRectGetWidth(thisVisibleFrame), + CGRectGetHeight(thisVisibleFrame)); } @end diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgentLib/Commands/FBCustomCommands.m index d20c485d0..b1490d921 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.m +++ b/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -253,7 +253,7 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app if (nil == result) { return FBResponseWithUnknownError(error); } - return FBResponseWithObject([result base64EncodedStringWithOptions:0]); + return FBResponseWithObject([result base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]); } + (id)handleGetBatteryInfo:(FBRouteRequest *)request @@ -599,7 +599,7 @@ + (NSString *)timeZone modifierFlags = [(NSNumber *)modifiers unsignedIntValue]; } NSString *keyValue = [FBKeyboard keyValueForName:item] ?: key; - [destination typeKey:keyValue modifierFlags:modifierFlags]; + [destination typeKey:keyValue modifierFlags:(XCUIKeyModifierFlags)modifierFlags]; } else { NSString *message = @"All items of the 'keys' array must be either dictionaries or strings"; return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message diff --git a/WebDriverAgentLib/Commands/FBElementCommands.m b/WebDriverAgentLib/Commands/FBElementCommands.m index 9c2ea009c..3efaf9ab2 100644 --- a/WebDriverAgentLib/Commands/FBElementCommands.m +++ b/WebDriverAgentLib/Commands/FBElementCommands.m @@ -574,7 +574,7 @@ + (NSArray *)routes traceback:nil]); } } - NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; screenshotData = nil; return FBResponseWithObject(screenshot); } diff --git a/WebDriverAgentLib/Commands/FBOrientationCommands.m b/WebDriverAgentLib/Commands/FBOrientationCommands.m index 8e0bea439..bfadd984d 100644 --- a/WebDriverAgentLib/Commands/FBOrientationCommands.m +++ b/WebDriverAgentLib/Commands/FBOrientationCommands.m @@ -139,7 +139,7 @@ + (BOOL)setDeviceOrientation:(NSString *)orientation forApplication:(XCUIApplica if (orientationValue == nil) { return NO; } - return [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientationValue.integerValue]; + return [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:(UIDeviceOrientation)orientationValue.integerValue]; } + (NSDictionary *)_orientationsMapping diff --git a/WebDriverAgentLib/Commands/FBScreenshotCommands.m b/WebDriverAgentLib/Commands/FBScreenshotCommands.m index 71d6ba594..e2b090722 100644 --- a/WebDriverAgentLib/Commands/FBScreenshotCommands.m +++ b/WebDriverAgentLib/Commands/FBScreenshotCommands.m @@ -33,7 +33,7 @@ + (NSArray *)routes if (nil == screenshotData) { return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:error.description traceback:nil]); } - NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; return FBResponseWithObject(screenshot); } diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m index 32c5b0579..06f9d7790 100644 --- a/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m +++ b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m @@ -46,7 +46,7 @@ - (nullable id)createVideoEncodingWithError:(NSError **)error [videoEncodingInitInvocation setSelector:videoEncodingConstructorSelector]; long long codec = self.codec; [videoEncodingInitInvocation setArgument:&codec atIndex:2]; - double frameRate = self.fps; + double frameRate = (double)self.fps; [videoEncodingInitInvocation setArgument:&frameRate atIndex:3]; [videoEncodingInitInvocation invokeWithTarget:videoEncodingAllocated]; id __unsafe_unretained result; diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.m b/WebDriverAgentLib/Routing/FBTCPSocket.m index 25a286ddd..e23876d83 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.m +++ b/WebDriverAgentLib/Routing/FBTCPSocket.m @@ -39,7 +39,7 @@ - (instancetype)initWithPort:(uint16_t)port - (BOOL)startWithError:(NSError **)error { if (![self.listeningSocket acceptOnPort:self.port error:error]) { - return NO;; + return NO; } return YES; diff --git a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m index 933d0ad89..219706b8f 100644 --- a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m +++ b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m @@ -117,7 +117,7 @@ - (void)appendChar:(unichar)character { NSMutableString *value = [NSMutableString stringWithString:self.asString]; [value appendFormat:@"%C", character]; - self.asString = value.copy;; + self.asString = value.copy; } - (nullable FBBaseClassChainToken*)followingTokenBasedOn:(unichar)character diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgentLib/Utilities/FBImageProcessor.m index 951568a8e..fe197c940 100644 --- a/WebDriverAgentLib/Utilities/FBImageProcessor.m +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -25,6 +25,7 @@ @interface FBImageProcessor () @property (nonatomic) NSData *nextImage; +@property (nonatomic) NSMutableArray *pendingCompletionHandlers; @property (nonatomic, readonly) NSLock *nextImageLock; @property (nonatomic, readonly) dispatch_queue_t scalingQueue; @property (atomic, assign) BOOL isScalingScheduled; @@ -38,6 +39,7 @@ - (id)init self = [super init]; if (self) { _nextImageLock = [[NSLock alloc] init]; + _pendingCompletionHandlers = [NSMutableArray array]; _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL); _isScalingScheduled = NO; } @@ -53,6 +55,7 @@ - (void)submitImageData:(NSData *)image [FBLogger verboseLog:@"Discarding screenshot"]; } self.nextImage = image; + [self.pendingCompletionHandlers addObject:[completionHandler copy]]; BOOL shouldSchedule = !self.isScalingScheduled; if (shouldSchedule) { self.isScalingScheduled = YES; @@ -62,14 +65,14 @@ - (void)submitImageData:(NSData *)image return; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wcompletion-handler" dispatch_async(self.scalingQueue, ^{ while (YES) { @autoreleasepool { [self.nextImageLock lock]; NSData *nextImageData = self.nextImage; self.nextImage = nil; + NSArray *handlers = [self.pendingCompletionHandlers copy]; + [self.pendingCompletionHandlers removeAllObjects]; if (nextImageData == nil) { self.isScalingScheduled = NO; [self.nextImageLock unlock]; @@ -80,7 +83,7 @@ - (void)submitImageData:(NSData *)image // We do not want this value to be too high because then we get images larger in size than original ones // Although, we also don't want to lose too much of the quality on recompression CGFloat recompressionQuality = MAX(0.9, - MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + MIN(FBMaxCompressionQuality, (double)FBConfiguration.mjpegServerScreenshotQuality / 100.0)); NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData scalingFactor:scalingFactor uti:UTTypeJPEG @@ -89,11 +92,13 @@ - (void)submitImageData:(NSData *)image // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 fixOrientation:FBConfiguration.mjpegShouldFixOrientation desiredOrientation:nil]; - completionHandler(thumbnailData ?: nextImageData); + NSData *processedImageData = thumbnailData ?: nextImageData; + for (void (^handler)(NSData *) in handlers) { + handler(processedImageData); + } } } }); -#pragma clang diagnostic pop } + (nullable NSData *)fixedImageDataWithImageData:(NSData *)imageData diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index 75389df50..6d6250422 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -95,7 +95,7 @@ - (void)streamScreenshot return; } NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.mjpegServerFramerate); - uint64_t timerInterval = (uint64_t)(1.0 / framerate * NSEC_PER_SEC); + uint64_t timerInterval = (uint64_t)(1.0 / (double)framerate * NSEC_PER_SEC); uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); @synchronized (self.listeningClients) { if (0 == self.listeningClients.count) { @@ -106,7 +106,7 @@ - (void)streamScreenshot NSError *error; CGFloat compressionQuality = MAX(FBMinCompressionQuality, - MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + MIN(FBMaxCompressionQuality, (double)FBConfiguration.mjpegServerScreenshotQuality / 100.0)); NSData *screenshotData = [FBScreenshot takeInOriginalResolutionWithScreenID:self.mainScreenID compressionQuality:compressionQuality uti:UTTypeJPEG diff --git a/WebDriverAgentLib/Utilities/FBScreenshot.m b/WebDriverAgentLib/Utilities/FBScreenshot.m index f13f96738..618fa308f 100644 --- a/WebDriverAgentLib/Utilities/FBScreenshot.m +++ b/WebDriverAgentLib/Utilities/FBScreenshot.m @@ -126,7 +126,7 @@ + (NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID withDescription:timeoutMsg] buildError:error]; } - }; + } if (nil != error && nil != innerError) { *error = innerError; } diff --git a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h index 39cdf6b76..d6460b131 100644 --- a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h +++ b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h @@ -50,8 +50,8 @@ extern NSArray *(*XCAXAccessibilityAttributesForStringAttributes)(id */ void *FBRetrieveXCTestSymbol(const char *name); -/*! Static constructor that will retrieve XCTest private symbols */ -__attribute__((constructor)) void FBLoadXCTestSymbols(void); +/*! Loads XCTest private symbols. Safe to call multiple times. */ +void FBLoadXCTestSymbols(void); /** Method is used to tranform attribute names into the format, which diff --git a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m index 21a9ba300..c001f28ba 100644 --- a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m +++ b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m @@ -30,41 +30,56 @@ NSArray *(*XCAXAccessibilityAttributesForStringAttributes)(id); -__attribute__((constructor)) void FBLoadXCTestSymbols(void) +@interface FBXCTestSymbolsLoader : NSObject +@end + +@implementation FBXCTestSymbolsLoader + ++ (void)load +{ + FBLoadXCTestSymbols(); +} + +@end + +void FBLoadXCTestSymbols(void) { - NSString *XC_kAXXCAttributeIsVisible = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsVisibleAttributeName UTF8String]); - NSString *XC_kAXXCAttributeIsElement = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsElementAttributeName UTF8String]); - - XCAXAccessibilityAttributesForStringAttributes = - (NSArray *(*)(id))FBRetrieveXCTestSymbol("XCAXAccessibilityAttributesForStringAttributes"); - - XCSetDebugLogger = (void (*)(id ))FBRetrieveXCTestSymbol("XCSetDebugLogger"); - XCDebugLogger = (id(*)(void))FBRetrieveXCTestSymbol("XCDebugLogger"); - - NSArray *accessibilityAttributes = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeIsVisible, XC_kAXXCAttributeIsElement]); - FB_XCAXAIsVisibleAttribute = accessibilityAttributes[0]; - FB_XCAXAIsElementAttribute = accessibilityAttributes[1]; - - NSCAssert(FB_XCAXAIsVisibleAttribute != nil , @"Failed to retrieve FB_XCAXAIsVisibleAttribute", FB_XCAXAIsVisibleAttribute); - NSCAssert(FB_XCAXAIsElementAttribute != nil , @"Failed to retrieve FB_XCAXAIsElementAttribute", FB_XCAXAIsElementAttribute); - - NSString *XC_kAXXCAttributeMinValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMinValueAttributeName UTF8String]); - NSString *XC_kAXXCAttributeMaxValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMaxValueAttributeName UTF8String]); - - NSString *XC_kAXXCAttributeCustomActions = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomActionsAttributeName UTF8String]); - - NSArray *customAttrs = XCAXAccessibilityAttributesForStringAttributes(@[ - XC_kAXXCAttributeMinValue, - XC_kAXXCAttributeMaxValue, - XC_kAXXCAttributeCustomActions - ]); - FB_XCAXACustomMinValueAttribute = customAttrs[0]; - FB_XCAXACustomMaxValueAttribute = customAttrs[1]; - FB_XCAXACustomActionsAttribute = customAttrs[2]; - - NSCAssert(FB_XCAXACustomMinValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMinValueAttribute", FB_XCAXACustomMinValueAttribute); - NSCAssert(FB_XCAXACustomMaxValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMaxValueAttribute", FB_XCAXACustomMaxValueAttribute); - NSCAssert(FB_XCAXACustomActionsAttribute != nil, @"Failed to retrieve FB_XCAXACustomActionsAttribute", FB_XCAXACustomActionsAttribute); + static dispatch_once_t loadOnceToken; + dispatch_once(&loadOnceToken, ^{ + NSString *XC_kAXXCAttributeIsVisible = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsVisibleAttributeName UTF8String]); + NSString *XC_kAXXCAttributeIsElement = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsElementAttributeName UTF8String]); + + XCAXAccessibilityAttributesForStringAttributes = + (NSArray *(*)(id))FBRetrieveXCTestSymbol("XCAXAccessibilityAttributesForStringAttributes"); + + XCSetDebugLogger = (void (*)(id ))FBRetrieveXCTestSymbol("XCSetDebugLogger"); + XCDebugLogger = (id(*)(void))FBRetrieveXCTestSymbol("XCDebugLogger"); + + NSArray *accessibilityAttributes = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeIsVisible, XC_kAXXCAttributeIsElement]); + FB_XCAXAIsVisibleAttribute = accessibilityAttributes[0]; + FB_XCAXAIsElementAttribute = accessibilityAttributes[1]; + + NSCAssert(FB_XCAXAIsVisibleAttribute != nil , @"Failed to retrieve FB_XCAXAIsVisibleAttribute", FB_XCAXAIsVisibleAttribute); + NSCAssert(FB_XCAXAIsElementAttribute != nil , @"Failed to retrieve FB_XCAXAIsElementAttribute", FB_XCAXAIsElementAttribute); + + NSString *XC_kAXXCAttributeMinValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMinValueAttributeName UTF8String]); + NSString *XC_kAXXCAttributeMaxValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMaxValueAttributeName UTF8String]); + + NSString *XC_kAXXCAttributeCustomActions = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomActionsAttributeName UTF8String]); + + NSArray *customAttrs = XCAXAccessibilityAttributesForStringAttributes(@[ + XC_kAXXCAttributeMinValue, + XC_kAXXCAttributeMaxValue, + XC_kAXXCAttributeCustomActions + ]); + FB_XCAXACustomMinValueAttribute = customAttrs[0]; + FB_XCAXACustomMaxValueAttribute = customAttrs[1]; + FB_XCAXACustomActionsAttribute = customAttrs[2]; + + NSCAssert(FB_XCAXACustomMinValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMinValueAttribute", FB_XCAXACustomMinValueAttribute); + NSCAssert(FB_XCAXACustomMaxValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMaxValueAttribute", FB_XCAXACustomMaxValueAttribute); + NSCAssert(FB_XCAXACustomActionsAttribute != nil, @"Failed to retrieve FB_XCAXACustomActionsAttribute", FB_XCAXACustomActionsAttribute); + }); } void *FBRetrieveXCTestSymbol(const char *name) diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m index cfb23e41e..a30d261b4 100755 --- a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m @@ -92,18 +92,18 @@ // Logging Disabled -#define LogError(frmt, ...) {} -#define LogWarn(frmt, ...) {} -#define LogInfo(frmt, ...) {} -#define LogVerbose(frmt, ...) {} +#define LogError(frmt, ...) do {} while (0) +#define LogWarn(frmt, ...) do {} while (0) +#define LogInfo(frmt, ...) do {} while (0) +#define LogVerbose(frmt, ...) do {} while (0) -#define LogCError(frmt, ...) {} -#define LogCWarn(frmt, ...) {} -#define LogCInfo(frmt, ...) {} -#define LogCVerbose(frmt, ...) {} +#define LogCError(frmt, ...) do {} while (0) +#define LogCWarn(frmt, ...) do {} while (0) +#define LogCInfo(frmt, ...) do {} while (0) +#define LogCVerbose(frmt, ...) do {} while (0) -#define LogTrace() {} -#define LogCTrace(frmt, ...) {} +#define LogTrace() do {} while (0) +#define LogCTrace(frmt, ...) do {} while (0) #endif @@ -251,7 +251,7 @@ - (instancetype)initWithCapacity:(size_t)numBytes if ((self = [super init])) { preBufferSize = numBytes; - preBuffer = malloc(preBufferSize); + preBuffer = (uint8_t *)malloc(preBufferSize); readPointer = preBuffer; writePointer = preBuffer; @@ -274,7 +274,7 @@ - (void)ensureCapacityForWrite:(size_t)numBytes size_t additionalBytes = numBytes - availableSpace; size_t newPreBufferSize = preBufferSize + additionalBytes; - uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); + uint8_t *newPreBuffer = (uint8_t *)realloc(preBuffer, newPreBufferSize); size_t readPointerOffset = readPointer - preBuffer; size_t writePointerOffset = writePointer - preBuffer; @@ -794,7 +794,7 @@ - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes // The implementation of this method is very similar to the above method. // See the above method for a discussion of the algorithm used here. - uint8_t *buff = [buffer mutableBytes]; + uint8_t *buff = (uint8_t *)[buffer mutableBytes]; NSUInteger buffLength = bytesDone + numBytes; const void *termBuff = [term bytes]; @@ -8693,7 +8693,7 @@ + (BOOL)isIPv4Address:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { - const struct sockaddr *sockaddrX = [address bytes]; + const struct sockaddr *sockaddrX = (const struct sockaddr *)(const void *)[address bytes]; if (sockaddrX->sa_family == AF_INET) { return YES; @@ -8707,7 +8707,7 @@ + (BOOL)isIPv6Address:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { - const struct sockaddr *sockaddrX = [address bytes]; + const struct sockaddr *sockaddrX = (const struct sockaddr *)(const void *)[address bytes]; if (sockaddrX->sa_family == AF_INET6) { return YES; @@ -8726,7 +8726,7 @@ + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_ { if ([address length] >= sizeof(struct sockaddr)) { - const struct sockaddr *sockaddrX = [address bytes]; + const struct sockaddr *sockaddrX = (const struct sockaddr *)(const void *)[address bytes]; if (sockaddrX->sa_family == AF_INET) { diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m index 529aa13ff..23d05e730 100755 --- a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m @@ -70,18 +70,18 @@ // Logging Disabled -#define LogError(frmt, ...) {} -#define LogWarn(frmt, ...) {} -#define LogInfo(frmt, ...) {} -#define LogVerbose(frmt, ...) {} +#define LogError(frmt, ...) do {} while (0) +#define LogWarn(frmt, ...) do {} while (0) +#define LogInfo(frmt, ...) do {} while (0) +#define LogVerbose(frmt, ...) do {} while (0) -#define LogCError(frmt, ...) {} -#define LogCWarn(frmt, ...) {} -#define LogCInfo(frmt, ...) {} -#define LogCVerbose(frmt, ...) {} +#define LogCError(frmt, ...) do {} while (0) +#define LogCWarn(frmt, ...) do {} while (0) +#define LogCInfo(frmt, ...) do {} while (0) +#define LogCVerbose(frmt, ...) do {} while (0) -#define LogTrace() {} -#define LogCTrace(frmt, ...) {} +#define LogTrace() do {} while (0) +#define LogCTrace(frmt, ...) do {} while (0) #endif @@ -4310,12 +4310,12 @@ - (void)doSend if (currentSend->addressFamily == AF_INET) { - result = sendto(socket4FD, buffer, length, 0, dst, dstSize); + result = sendto(socket4FD, buffer, length, 0, (const struct sockaddr *)dst, dstSize); LogVerbose(@"sendto(socket4FD) = %d", result); } else { - result = sendto(socket6FD, buffer, length, 0, dst, dstSize); + result = sendto(socket6FD, buffer, length, 0, (const struct sockaddr *)dst, dstSize); LogVerbose(@"sendto(socket6FD) = %d", result); } } diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m index 4aef974d7..d8c8c70ca 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m @@ -5,27 +5,21 @@ DDRange DDUnionRange(DDRange range1, DDRange range2) { - DDRange result; - - result.location = MIN(range1.location, range2.location); - result.length = MAX(DDMaxRange(range1), DDMaxRange(range2)) - result.location; - - return result; + UInt64 location = MIN(range1.location, range2.location); + UInt64 length = MAX(DDMaxRange(range1), DDMaxRange(range2)) - location; + + return DDMakeRange(location, length); } DDRange DDIntersectionRange(DDRange range1, DDRange range2) { - DDRange result; - if((DDMaxRange(range1) < range2.location) || (DDMaxRange(range2) < range1.location)) { return DDMakeRange(0, 0); } - result.location = MAX(range1.location, range2.location); - result.length = MIN(DDMaxRange(range1), DDMaxRange(range2)) - result.location; - - return result; + return DDMakeRange(MAX(range1.location, range2.location), + MIN(DDMaxRange(range1), DDMaxRange(range2)) - MAX(range1.location, range2.location)); } NSString *DDStringFromRange(DDRange range) diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h index 84ee8da04..4c277f1db 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h @@ -95,28 +95,28 @@ // Define logging primitives. -#define HTTPLogError(...) { } +#define HTTPLogError(...) do {} while (0) -#define HTTPLogWarn(...) { } +#define HTTPLogWarn(...) do {} while (0) -#define HTTPLogInfo(...) { } +#define HTTPLogInfo(...) do {} while (0) -#define HTTPLogVerbose(...) { } +#define HTTPLogVerbose(...) do {} while (0) -#define HTTPLogTrace() { } +#define HTTPLogTrace() do {} while (0) -#define HTTPLogTrace2(...) { } +#define HTTPLogTrace2(...) do {} while (0) -#define HTTPLogCError(...) { } +#define HTTPLogCError(...) do {} while (0) -#define HTTPLogCWarn(...) { } +#define HTTPLogCWarn(...) do {} while (0) -#define HTTPLogCInfo(...) { } +#define HTTPLogCInfo(...) do {} while (0) -#define HTTPLogCVerbose(...) { } +#define HTTPLogCVerbose(...) do {} while (0) -#define HTTPLogCTrace() { } +#define HTTPLogCTrace() do {} while (0) -#define HTTPLogCTrace2(...) { } +#define HTTPLogCTrace2(...) do {} while (0) diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m index 920f7a280..44eaee180 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m @@ -68,13 +68,13 @@ - (BOOL)appendData:(NSData *)data { // Look for the end of headers (CRLF CRLF or LF LF) NSData *headerEndMarker = [@"\r\n\r\n" dataUsingEncoding:NSASCIIStringEncoding]; - NSRange headerEndRange = [_rawData rangeOfData:headerEndMarker options:0 range:NSMakeRange(0, [_rawData length])]; + NSRange headerEndRange = [_rawData rangeOfData:headerEndMarker options:(NSDataSearchOptions)0 range:NSMakeRange(0, [_rawData length])]; if (headerEndRange.location == NSNotFound) { // Also check for LF LF (some clients use this) NSData *lfMarker = [@"\n\n" dataUsingEncoding:NSASCIIStringEncoding]; - headerEndRange = [_rawData rangeOfData:lfMarker options:0 range:NSMakeRange(0, [_rawData length])]; + headerEndRange = [_rawData rangeOfData:lfMarker options:(NSDataSearchOptions)0 range:NSMakeRange(0, [_rawData length])]; } if (headerEndRange.location != NSNotFound) diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m index 56df1cd3f..68e6a274a 100644 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m @@ -149,16 +149,16 @@ - (Route *)routeWithPath:(NSString *)path { NSRegularExpression *regex = nil; // Escape regex characters - regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:0 error:nil]; - path = [regex stringByReplacingMatchesInString:path options:0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"]; + regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:(NSRegularExpressionOptions)0 error:nil]; + path = [regex stringByReplacingMatchesInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"]; // Parse any :parameters and * in the path regex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)" - options:0 + options:(NSRegularExpressionOptions)0 error:nil]; NSMutableString *regexPath = [NSMutableString stringWithString:path]; __block NSInteger diff = 0; - [regex enumerateMatchesInString:path options:0 range:NSMakeRange(0, path.length) + [regex enumerateMatchesInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length); NSString *replacementString; @@ -219,7 +219,7 @@ - (RouteResponse *)routeMethod:(NSString *)method return nil; for (Route *route in methodRoutes) { - NSTextCheckingResult *result = [route.regex firstMatchInString:path options:0 range:NSMakeRange(0, path.length)]; + NSTextCheckingResult *result = [route.regex firstMatchInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length)]; if (!result) continue; From 161ca7d05582bedbda8af0ae6392b9138b472482 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 23 May 2026 19:04:53 +0000 Subject: [PATCH 28/55] chore(release): 13.1.2 [skip ci] ## [13.1.2](https://github.com/appium/WebDriverAgent/compare/v13.1.1...v13.1.2) (2026-05-23) ### Bug Fixes * Address compilation warnings ([#1143](https://github.com/appium/WebDriverAgent/issues/1143)) ([f1f9976](https://github.com/appium/WebDriverAgent/commit/f1f9976f4a0a0fb8a8aa3ee1f2483b25275600e6)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6475a676e..c4b127d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.1.2](https://github.com/appium/WebDriverAgent/compare/v13.1.1...v13.1.2) (2026-05-23) + +### Bug Fixes + +* Address compilation warnings ([#1143](https://github.com/appium/WebDriverAgent/issues/1143)) ([f1f9976](https://github.com/appium/WebDriverAgent/commit/f1f9976f4a0a0fb8a8aa3ee1f2483b25275600e6)) + ## [13.1.1](https://github.com/appium/WebDriverAgent/compare/v13.1.0...v13.1.1) (2026-05-22) ### Bug Fixes diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 8d74f79c8..372812ffc 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.1.1 + 13.1.2 CFBundleSignature ???? CFBundleVersion - 13.1.1 + 13.1.2 NSPrincipalClass diff --git a/package.json b/package.json index 2f2a8314a..a3a8fd616 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.1.1", + "version": "13.1.2", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 9ada5f6fe2af29278c488e845f8714f22fabfeee Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 24 May 2026 10:18:44 +0200 Subject: [PATCH 29/55] fix: Scheme for derived data path retrieval (#1142) --- lib/xcodebuild.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 14d66ff1d..c7fb0cb6a 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -190,7 +190,10 @@ export class XcodeBuild { return this.derivedDataPath; } - const buildSettings = await this.retrieveBuildSettings(); + // iOS/tvOS share the same derived data path + const buildSettings = await this.retrieveBuildSettings({ + scheme: 'WebDriverAgentRunner', + }); const buildDir = buildSettings?.BUILD_DIR; if (!buildDir) { this.log.warn('Cannot parse WDA BUILD_DIR from build settings'); From 1807fa8160a991341dc4815c7cb5ea4ee7e260f9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 24 May 2026 08:23:31 +0000 Subject: [PATCH 30/55] chore(release): 13.1.3 [skip ci] ## [13.1.3](https://github.com/appium/WebDriverAgent/compare/v13.1.2...v13.1.3) (2026-05-24) ### Bug Fixes * Scheme for derived data path retrieval ([#1142](https://github.com/appium/WebDriverAgent/issues/1142)) ([9ada5f6](https://github.com/appium/WebDriverAgent/commit/9ada5f6fe2af29278c488e845f8714f22fabfeee)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b127d24..3bd689392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.1.3](https://github.com/appium/WebDriverAgent/compare/v13.1.2...v13.1.3) (2026-05-24) + +### Bug Fixes + +* Scheme for derived data path retrieval ([#1142](https://github.com/appium/WebDriverAgent/issues/1142)) ([9ada5f6](https://github.com/appium/WebDriverAgent/commit/9ada5f6fe2af29278c488e845f8714f22fabfeee)) + ## [13.1.2](https://github.com/appium/WebDriverAgent/compare/v13.1.1...v13.1.2) (2026-05-23) ### Bug Fixes diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 372812ffc..89875d0d0 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.1.2 + 13.1.3 CFBundleSignature ???? CFBundleVersion - 13.1.2 + 13.1.3 NSPrincipalClass diff --git a/package.json b/package.json index a3a8fd616..ad7f12f2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.1.2", + "version": "13.1.3", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From a975b89ac998d31a72bf3723b843d85af8867cf0 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 26 May 2026 07:14:01 +0200 Subject: [PATCH 31/55] feat: Add XPath extensions (#1144) --- WebDriverAgent.xcodeproj/project.pbxproj | 14 +- WebDriverAgentLib/Utilities/FBXPath-Private.h | 5 + WebDriverAgentLib/Utilities/FBXPath.m | 40 +- .../Utilities/FBXPathExtensions.h | 40 ++ .../Utilities/FBXPathExtensions.m | 418 ++++++++++++++++++ .../Utilities/XCTestPrivateSymbols.m | 5 + .../Responses/HTTPErrorResponse.m | 4 +- .../FBXPathIntegrationTests.m | 103 +++++ WebDriverAgentTests/UnitTests/FBXPathTests.m | 125 ++++++ 9 files changed, 744 insertions(+), 10 deletions(-) create mode 100644 WebDriverAgentLib/Utilities/FBXPathExtensions.h create mode 100644 WebDriverAgentLib/Utilities/FBXPathExtensions.m diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index 82f506bb8..904461564 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */; }; 641EE5DA2240C5CA00173FCB /* XCUIApplicationProcessDelay.m in Sources */ = {isa = PBXBuildFile; fileRef = 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */; }; 641EE5DB2240C5CA00173FCB /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; + 71B2E0042733FB970074B004 /* FBXPathExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B2E0022733FB970074B002 /* FBXPathExtensions.m */; }; 641EE5DC2240C5CA00173FCB /* XCUIApplication+FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8FB2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m */; }; 641EE5DE2240C5CA00173FCB /* XCUIApplication+FBTouchAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */; }; 641EE5DF2240C5CA00173FCB /* FBWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB78D1CAEDF0C008C271F /* FBWebServer.m */; }; @@ -316,6 +317,7 @@ 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */; }; 711084441DA3AA7500F913D6 /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 711084421DA3AA7500F913D6 /* FBXPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; + 71B2E0062733FB970074B006 /* FBXPathExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B2E0022733FB970074B002 /* FBXPathExtensions.m */; }; 7119097C2152580600BA3C7E /* XCUIScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 7119097B2152580600BA3C7E /* XCUIScreen.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7119E1EC1E891F8600D0B125 /* FBPickerWheelSelectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */; }; 711CD03425ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */; }; @@ -976,6 +978,8 @@ 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FBTVNavigationTracker-Private.h"; sourceTree = ""; }; 711084421DA3AA7500F913D6 /* FBXPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBXPath.h; sourceTree = ""; }; 711084431DA3AA7500F913D6 /* FBXPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPath.m; sourceTree = ""; }; + 71B2E0012733FB970074B001 /* FBXPathExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXPathExtensions.h; sourceTree = ""; }; + 71B2E0022733FB970074B002 /* FBXPathExtensions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXPathExtensions.m; sourceTree = ""; }; 7119097B2152580600BA3C7E /* XCUIScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIScreen.h; sourceTree = ""; }; 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBPickerWheelSelectTests.m; sourceTree = ""; }; 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIScreenDataSource-Protocol.h"; sourceTree = ""; }; @@ -2000,9 +2004,11 @@ 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */, 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */, 712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */, - 711084421DA3AA7500F913D6 /* FBXPath.h */, - 711084431DA3AA7500F913D6 /* FBXPath.m */, - EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */, + 711084421DA3AA7500F913D6 /* FBXPath.h */, + 711084431DA3AA7500F913D6 /* FBXPath.m */, + 71B2E0012733FB970074B001 /* FBXPathExtensions.h */, + 71B2E0022733FB970074B002 /* FBXPathExtensions.m */, + EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */, EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */, 633E904A220DEE7F007CADF9 /* XCUIApplicationProcessDelay.h */, 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */, @@ -3156,6 +3162,7 @@ 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */, 641EE5DA2240C5CA00173FCB /* XCUIApplicationProcessDelay.m in Sources */, 641EE5DB2240C5CA00173FCB /* FBXPath.m in Sources */, + 71B2E0042733FB970074B004 /* FBXPathExtensions.m in Sources */, 71C8E55425399A6B008572C1 /* XCUIApplication+FBQuiescence.m in Sources */, 641EE5DC2240C5CA00173FCB /* XCUIApplication+FBAlert.m in Sources */, 641EE70F2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */, @@ -3277,6 +3284,7 @@ 6385F4A7220A40760095BBDB /* XCUIApplicationProcessDelay.m in Sources */, 71A5C67529A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */, + 71B2E0062733FB970074B006 /* FBXPathExtensions.m in Sources */, 719CD8FD2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m in Sources */, 13DE7A45287C2A8D003243C6 /* FBXCAccessibilityElement.m in Sources */, 641EE70E2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */, diff --git a/WebDriverAgentLib/Utilities/FBXPath-Private.h b/WebDriverAgentLib/Utilities/FBXPath-Private.h index db9732c41..fa4b16265 100644 --- a/WebDriverAgentLib/Utilities/FBXPath-Private.h +++ b/WebDriverAgentLib/Utilities/FBXPath-Private.h @@ -51,6 +51,11 @@ NS_ASSUME_NONNULL_BEGIN document:(xmlDocPtr)doc contextNode:(nullable xmlNodePtr)contextNode; ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode + errorMessage:(NSString * _Nullable * _Nullable)errorMessage; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgentLib/Utilities/FBXPath.m index 68db53ff2..18f01a3fc 100644 --- a/WebDriverAgentLib/Utilities/FBXPath.m +++ b/WebDriverAgentLib/Utilities/FBXPath.m @@ -14,6 +14,7 @@ #import "FBLogger.h" #import "FBMacros.h" #import "FBXMLGenerationOptions.h" +#import "FBXPathExtensions.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "NSString+FBXMLSafeString.h" #import "XCUIApplication.h" @@ -152,7 +153,17 @@ @implementation FBXPath + (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery { - NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery]; + return [self throwException:name forQuery:xpathQuery detail:nil]; +} + ++ (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery detail:(nullable NSString *)detail +{ + NSString *reason; + if (nil != detail) { + reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\": %@", xpathQuery, detail]; + } else { + reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery]; + } @throw [NSException exceptionWithName:name reason:reason userInfo:@{}]; return nil; } @@ -284,16 +295,18 @@ + (nullable NSString *)xmlStringWithRootElement:(id)root contextNode = nodeSet->nodeTab[0]; } } + NSString *evaluationError = nil; xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery document:doc - contextNode:contextNode]; + contextNode:contextNode + errorMessage:&evaluationError]; if (NULL != contextNodeQueryResult) { xmlXPathFreeObject(contextNodeQueryResult); } if (NULL == queryResult) { xmlFreeTextWriter(writer); xmlFreeDoc(doc); - return [self throwException:FBInvalidXPathException forQuery:xpathQuery]; + return [self throwException:FBInvalidXPathException forQuery:xpathQuery detail:evaluationError]; } NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval @@ -435,6 +448,14 @@ + (int)xmlRepresentationWithRootElement:(id)root + (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc contextNode:(nullable xmlNodePtr)contextNode +{ + return [self evaluate:xpathQuery document:doc contextNode:contextNode errorMessage:nil]; +} + ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode + errorMessage:(NSString * _Nullable * _Nullable)errorMessage { xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); if (NULL == xpathCtx) { @@ -443,10 +464,21 @@ + (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery } xpathCtx->node = NULL == contextNode ? doc->children : contextNode; + FBXPathExtensions *extensions = [FBXPathExtensions new]; + [extensions registerFunctionsWithContext:xpathCtx]; + xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx); if (NULL == xpathObj) { + NSString *detail = extensions.lastEvaluationError; + if (NULL != errorMessage) { + *errorMessage = detail; + } + if (nil != detail) { + [FBLogger logFmt:@"Failed to evaluate XPath query \"%@\": %@", xpathQuery, detail]; + } else { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery]; + } xmlXPathFreeContext(xpathCtx); - [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery]; return NULL; } xmlXPathFreeContext(xpathCtx); diff --git a/WebDriverAgentLib/Utilities/FBXPathExtensions.h b/WebDriverAgentLib/Utilities/FBXPathExtensions.h new file mode 100644 index 000000000..11f95bbda --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXPathExtensions.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" +#endif + +#import + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface FBXPathExtensions : NSObject + +/** + Registers XPath 2-compatible extension functions on the given libxml2 context. + */ +- (void)registerFunctionsWithContext:(xmlXPathContextPtr)xpathCtx; + +/** + Human-readable message for the most recent XPath extension evaluation failure on this instance, + for example an invalid regular expression pattern or flags. Nil when no extension error has occurred. + Scoped to the libxml2 context this instance is registered with; each evaluation should use its own instance. + */ +@property (nonatomic, nullable, readonly, copy) NSString *lastEvaluationError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXPathExtensions.m b/WebDriverAgentLib/Utilities/FBXPathExtensions.m new file mode 100644 index 000000000..46cc42413 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXPathExtensions.m @@ -0,0 +1,418 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBXPathExtensions.h" + +#import "FBLogger.h" + +#import + +static void FBRegisterXPathExtensions(xmlXPathContextPtr xpathCtx); + +static NSString *const FBXPathTokenSequenceSeparator = @"\x1E"; +static const NSRegularExpressionOptions FBXPathNoRegexOptions = (NSRegularExpressionOptions)0; +static const NSMatchingOptions FBXPathNoMatchingOptions = (NSMatchingOptions)0; + +@interface FBXPathExtensions () +@property (nonatomic, nullable, readwrite, copy) NSString *lastEvaluationError; +@end + +static FBXPathExtensions *FBXPathExtensionsFromParserContext(xmlXPathParserContextPtr ctxt) +{ + if (NULL == ctxt || NULL == ctxt->context || NULL == ctxt->context->userData) { + return nil; + } + return (__bridge FBXPathExtensions *)ctxt->context->userData; +} + +static void FBXPathSetEvaluationError(xmlXPathParserContextPtr ctxt, int xpathErrorCode, NSString *message) +{ + FBXPathExtensions *extensions = FBXPathExtensionsFromParserContext(ctxt); + extensions.lastEvaluationError = message; + [FBLogger logFmt:@"XPath extension evaluation error: %@", message]; + if (NULL == ctxt) { + return; + } + xmlXPatherror(ctxt, __FILE__, __LINE__, xpathErrorCode); + ctxt->error = xpathErrorCode; +} + +static void FBXPathSetInvalidArityError(xmlXPathParserContextPtr ctxt) +{ + if (NULL == ctxt) { + return; + } + xmlXPatherror(ctxt, __FILE__, __LINE__, XPATH_INVALID_ARITY); + ctxt->error = XPATH_INVALID_ARITY; +} + +static BOOL FBXPathFlagsAreValid(NSString *flags, BOOL allowsQFlag) +{ + if (nil == flags || 0 == flags.length) { + return YES; + } + + NSString *validFlags = allowsQFlag ? @"imsxq" : @"imsx"; + for (NSUInteger index = 0; index < flags.length; index++) { + unichar flag = [flags characterAtIndex:index]; + if ([validFlags rangeOfString:[NSString stringWithCharacters:&flag length:1]].location == NSNotFound) { + return NO; + } + } + return YES; +} + +static NSString *FBXPathStringFromUTF8Bytes(const xmlChar *bytes) +{ + if (NULL == bytes) { + return nil; + } + return [NSString stringWithUTF8String:(const char *)bytes]; +} + +@implementation FBXPathExtensions + +- (void)registerFunctionsWithContext:(xmlXPathContextPtr)xpathCtx +{ + xpathCtx->userData = (__bridge void *)self; + FBRegisterXPathExtensions(xpathCtx); +} + +@end + +static NSString *FBXPathPopNSString(xmlXPathParserContextPtr ctxt) +{ + xmlChar *value = xmlXPathPopString(ctxt); + if (NULL == value || xmlXPathCheckError(ctxt)) { + return nil; + } + NSString *result = [NSString stringWithUTF8String:(const char *)value]; + xmlFree(value); + return result; +} + +static NSRegularExpressionOptions FBXPathRegexOptionsFromFlags(NSString *flags) +{ + NSRegularExpressionOptions options = FBXPathNoRegexOptions; + if (nil != flags && [flags rangeOfString:@"i"].location != NSNotFound) { + options |= NSRegularExpressionCaseInsensitive; + } + return options; +} + +static NSRegularExpression *FBXPathRegexWithPattern(NSString *pattern, + NSString *flags, + BOOL allowsQFlag, + xmlXPathParserContextPtr ctxt) +{ + if (!FBXPathFlagsAreValid(flags, allowsQFlag)) { + FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, @"Invalid regular expression flags"); + return nil; + } + + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern + options:FBXPathRegexOptionsFromFlags(flags) + error:&error]; + if (nil == regex) { + NSString *message = error.localizedDescription ?: @"Invalid regular expression"; + FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, message); + return nil; + } + return regex; +} + +static BOOL FBXPathTokenizeString(NSString *input, + NSString *pattern, + xmlXPathParserContextPtr ctxt, + NSArray **outTokens) +{ + if (0 == input.length) { + *outTokens = @[]; + return YES; + } + + if (nil == pattern) { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\S+" + options:FBXPathNoRegexOptions + error:nil]; + if (nil == regex) { + FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, @"Invalid regular expression"); + return NO; + } + NSMutableArray *tokens = [NSMutableArray array]; + [regex enumerateMatchesInString:input + options:FBXPathNoMatchingOptions + range:NSMakeRange(0, input.length) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + if (nil != result) { + [tokens addObject:[input substringWithRange:result.range]]; + } + }]; + *outTokens = tokens.copy; + return YES; + } + + if (0 == pattern.length) { + NSMutableArray *tokens = [NSMutableArray array]; + [input enumerateSubstringsInRange:NSMakeRange(0, input.length) + options:NSStringEnumerationByComposedCharacterSequences + usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { + if (substring.length > 0) { + [tokens addObject:substring]; + } + }]; + *outTokens = tokens.copy; + return YES; + } + + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern + options:FBXPathNoRegexOptions + error:&error]; + if (nil == regex) { + NSString *message = error.localizedDescription ?: @"Invalid regular expression"; + FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, message); + return NO; + } + + NSMutableArray *tokens = [NSMutableArray array]; + __block NSUInteger lastIndex = 0; + [regex enumerateMatchesInString:input + options:FBXPathNoMatchingOptions + range:NSMakeRange(0, input.length) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + if (nil == result) { + return; + } + if (result.range.location > lastIndex) { + NSString *token = [input substringWithRange:NSMakeRange(lastIndex, result.range.location - lastIndex)]; + if (token.length > 0) { + [tokens addObject:token]; + } + } + lastIndex = NSMaxRange(result.range); + }]; + if (lastIndex < input.length) { + NSString *token = [input substringFromIndex:lastIndex]; + if (token.length > 0) { + [tokens addObject:token]; + } + } + *outTokens = tokens.copy; + return YES; +} + +static void FBXPathReturnNSString(xmlXPathParserContextPtr ctxt, NSString *value) +{ + if (nil == value) { + xmlXPathReturnEmptyString(ctxt); + return; + } + xmlChar *copiedValue = xmlStrdup((const xmlChar *)[value UTF8String]); + if (NULL == copiedValue) { + xmlXPathReturnEmptyString(ctxt); + return; + } + // xmlXPathWrapString takes ownership of the buffer passed to xmlXPathReturnString. + xmlXPathReturnString(ctxt, copiedValue); +} + +static NSArray *FBXPathPartsFromXPathObject(xmlXPathObjectPtr sequence) +{ + if (sequence->type == XPATH_NODESET && NULL != sequence->nodesetval) { + NSMutableArray *parts = [NSMutableArray array]; + for (int index = 0; index < sequence->nodesetval->nodeNr; index++) { + xmlChar *content = xmlNodeGetContent(sequence->nodesetval->nodeTab[index]); + if (NULL != content) { + NSString *part = FBXPathStringFromUTF8Bytes(content); + xmlFree(content); + if (nil != part) { + [parts addObject:part]; + } + } + } + return parts.copy; + } + + xmlChar *asString = xmlXPathCastToString(sequence); + if (NULL == asString) { + return @[]; + } + NSString *value = FBXPathStringFromUTF8Bytes(asString); + xmlFree(asString); + if (nil == value || 0 == value.length) { + return @[]; + } + if ([value rangeOfString:FBXPathTokenSequenceSeparator].location != NSNotFound) { + return [value componentsSeparatedByString:FBXPathTokenSequenceSeparator]; + } + return @[value]; +} + +static void FBXPathMatchesFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs < 2 || nargs > 3) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *flags = nargs == 3 ? FBXPathPopNSString(ctxt) : nil; + NSString *pattern = FBXPathPopNSString(ctxt); + NSString *input = FBXPathPopNSString(ctxt); + if (nil == pattern || nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + NSRegularExpression *regex = FBXPathRegexWithPattern(pattern, flags, NO, ctxt); + if (nil == regex) { + return; + } + + NSRange range = NSMakeRange(0, input.length); + NSTextCheckingResult *match = [regex firstMatchInString:input options:FBXPathNoMatchingOptions range:range]; + xmlXPathReturnBoolean(ctxt, nil != match); +} + +static void FBXPathEndsWithFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs != 2) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *suffix = FBXPathPopNSString(ctxt); + NSString *input = FBXPathPopNSString(ctxt); + if (nil == suffix || nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + xmlXPathReturnBoolean(ctxt, [input hasSuffix:suffix]); +} + +static void FBXPathLowerCaseFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs != 1) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *input = FBXPathPopNSString(ctxt); + if (nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + FBXPathReturnNSString(ctxt, input.lowercaseString); +} + +static void FBXPathUpperCaseFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs != 1) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *input = FBXPathPopNSString(ctxt); + if (nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + FBXPathReturnNSString(ctxt, input.uppercaseString); +} + +static void FBXPathReplaceFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs < 3 || nargs > 4) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *flags = nargs == 4 ? FBXPathPopNSString(ctxt) : nil; + NSString *replacement = FBXPathPopNSString(ctxt); + NSString *pattern = FBXPathPopNSString(ctxt); + NSString *input = FBXPathPopNSString(ctxt); + if (nil == replacement || nil == pattern || nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + NSRegularExpression *regex = FBXPathRegexWithPattern(pattern, flags, YES, ctxt); + if (nil == regex) { + return; + } + + NSRange range = NSMakeRange(0, input.length); + NSString *result = [regex stringByReplacingMatchesInString:input + options:FBXPathNoMatchingOptions + range:range + withTemplate:replacement]; + FBXPathReturnNSString(ctxt, result); +} + +static void FBXPathTokenizeFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs < 1 || nargs > 2) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *pattern = nargs == 2 ? FBXPathPopNSString(ctxt) : nil; + NSString *input = FBXPathPopNSString(ctxt); + if (nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + NSArray *tokens = nil; + if (!FBXPathTokenizeString(input, pattern, ctxt, &tokens)) { + return; + } + + FBXPathReturnNSString(ctxt, [tokens componentsJoinedByString:FBXPathTokenSequenceSeparator]); +} + +static void FBXPathStringJoinFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs != 2) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + xmlChar *separatorChars = xmlXPathPopString(ctxt); + xmlXPathObjectPtr sequence = valuePop(ctxt); + if (xmlXPathCheckError(ctxt) || NULL == sequence || NULL == separatorChars) { + if (NULL != separatorChars) { + xmlFree(separatorChars); + } + if (NULL != sequence) { + xmlXPathFreeObject(sequence); + } + return; + } + + NSString *separator = FBXPathStringFromUTF8Bytes(separatorChars); + xmlFree(separatorChars); + if (nil == separator) { + xmlXPathFreeObject(sequence); + return; + } + + NSArray *parts = FBXPathPartsFromXPathObject(sequence); + xmlXPathFreeObject(sequence); + + FBXPathReturnNSString(ctxt, [parts componentsJoinedByString:separator]); +} + +static void FBRegisterXPathExtensions(xmlXPathContextPtr xpathCtx) +{ + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "matches", FBXPathMatchesFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "ends-with", FBXPathEndsWithFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "lower-case", FBXPathLowerCaseFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "upper-case", FBXPathUpperCaseFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "replace", FBXPathReplaceFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "tokenize", FBXPathTokenizeFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "string-join", FBXPathStringJoinFunction); +} diff --git a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m index c001f28ba..60ac241a7 100644 --- a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m +++ b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m @@ -35,11 +35,16 @@ @interface FBXCTestSymbolsLoader : NSObject @implementation FBXCTestSymbolsLoader +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" + + (void)load { FBLoadXCTestSymbols(); } +#pragma clang diagnostic pop + @end void FBLoadXCTestSymbols(void) diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m index 3309cf164..a11552008 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m @@ -22,9 +22,7 @@ - (UInt64) offset { return 0; } -- (void)setOffset:(UInt64)offset { - ; -} +- (void)setOffset:(UInt64)offset {} - (NSData*) readDataOfLength:(NSUInteger)length { return nil; diff --git a/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m index 7082224ef..4d9d5874a 100644 --- a/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m @@ -9,6 +9,7 @@ #import #import "FBIntegrationTestCase.h" +#import "FBExceptions.h" #import "FBMacros.h" #import "FBTestMacros.h" #import "FBXPath.h" @@ -51,6 +52,31 @@ - (void)setUp return snapshot; } +- (NSSet *)labelsForMatchingSnapshots:(NSArray> *)matchingSnapshots +{ + NSMutableSet *labels = [NSMutableSet set]; + for (id snapshot in matchingSnapshots) { + NSString *label = [FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdLabel; + if (nil != label) { + [labels addObject:label]; + } + } + return labels.copy; +} + +- (void)assertXPathQuery:(NSString *)query findsButtonLabels:(NSArray *)expectedLabels +{ + NSArray> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication + forQuery:query]; + NSSet *foundLabels = [self labelsForMatchingSnapshots:matchingSnapshots]; + NSSet *expectedLabelSet = [NSSet setWithArray:expectedLabels]; + XCTAssertEqual(foundLabels.count, expectedLabelSet.count); + XCTAssertEqualObjects(foundLabels, expectedLabelSet); + for (id snapshot in matchingSnapshots) { + XCTAssertEqualObjects([FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdType, @"XCUIElementTypeButton"); + } +} + - (void)testApplicationNodeXMLRepresentation { id snapshot = [self.testedApplication fb_customSnapshot]; @@ -154,4 +180,81 @@ - (void)testFindMatchesInElementWithDotNotation } } +- (void)testFindMatchesWithMatchesFunction +{ + [self assertXPathQuery:@"//XCUIElementTypeButton[matches(@label, '^Alerts$')]" + findsButtonLabels:@[@"Alerts"]]; +} + +- (void)testFindMatchesWithMatchesFunctionCaseInsensitive +{ + [self assertXPathQuery:@"//XCUIElementTypeButton[matches(@label, '^alerts$', 'i')]" + findsButtonLabels:@[@"Alerts"]]; +} + +- (void)testFindMatchesWithEndsWithFunction +{ + [self assertXPathQuery:@"//XCUIElementTypeButton[ends-with(@label, 'ing')]" + findsButtonLabels:@[@"Scrolling"]]; +} + +- (void)testFindMatchesWithLowerCaseFunction +{ + [self assertXPathQuery:@"//XCUIElementTypeButton[lower-case(@label)='alerts']" + findsButtonLabels:@[@"Alerts"]]; +} + +- (void)testFindMatchesWithUpperCaseFunction +{ + [self assertXPathQuery:@"//XCUIElementTypeButton[upper-case(@label)='TOUCH']" + findsButtonLabels:@[@"Touch"]]; +} + +- (void)testFindMatchesWithReplaceFunction +{ + [self assertXPathQuery:@"//XCUIElementTypeButton[replace(@label, ' ', '')='Deadlockapp']" + findsButtonLabels:@[@"Deadlock app"]]; +} + +- (void)testFindMatchesWithTokenizeAndStringJoinFunctions +{ + [self assertXPathQuery:@"//XCUIElementTypeButton[string-join(tokenize(@label, ' '), '-')='Deadlock-app']" + findsButtonLabels:@[@"Deadlock app"]]; +} + +- (void)testFindMatchesWithExtensionFunctionsNoMatches +{ + NSArray> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication + forQuery:@"//XCUIElementTypeButton[matches(@label, '^NoSuchButton$')]"]; + XCTAssertEqual(matchingSnapshots.count, 0); +} + +- (void)testFindMultipleMatchesWithMatchesFunction +{ + [self assertXPathQuery:@"//XCUIElementTypeButton[matches(@label, '.*')]" + findsButtonLabels:@[@"Alerts", @"Deadlock app", @"Attributes", @"Scrolling", @"Touch"]]; +} + +- (void)testInvalidXPathExtensionFunctionViaElementLookup +{ + XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[matches(@label)]" + shouldReturnAfterFirstMatch:NO], + NSException, + FBInvalidXPathException); +} + +- (void)testInvalidXPathExtensionRegexpViaElementLookup +{ + NSException *exception = nil; + @try { + [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[matches(@label, '[')]" + shouldReturnAfterFirstMatch:NO]; + } @catch (NSException *caughtException) { + exception = caughtException; + } + XCTAssertEqualObjects(exception.name, FBInvalidXPathException); + XCTAssertTrue([exception.reason containsString:@"Cannot evaluate results for XPath expression"]); + XCTAssertTrue([exception.reason rangeOfString:@"invalid" options:NSCaseInsensitiveSearch].location != NSNotFound); +} + @end diff --git a/WebDriverAgentTests/UnitTests/FBXPathTests.m b/WebDriverAgentTests/UnitTests/FBXPathTests.m index fef61b3ef..c7d545747 100644 --- a/WebDriverAgentTests/UnitTests/FBXPathTests.m +++ b/WebDriverAgentTests/UnitTests/FBXPathTests.m @@ -152,4 +152,129 @@ - (void)testSnapshotXPathResultsMatching XCTAssertEqual(1, [matchingSnapshots count]); } +- (NSString *)xpathStringResultForQuery:(NSString *)query document:(xmlDocPtr)doc +{ + xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL]; + if (NULL == queryResult) { + return nil; + } + xmlChar *stringValue = xmlXPathCastToString(queryResult); + xmlXPathFreeObject(queryResult); + if (NULL == stringValue) { + return nil; + } + NSString *result = [NSString stringWithUTF8String:(const char *)stringValue]; + xmlFree(stringValue); + return result; +} + +- (BOOL)xpathBooleanResultForQuery:(NSString *)query document:(xmlDocPtr)doc +{ + xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL]; + if (NULL == queryResult) { + return NO; + } + BOOL result = queryResult->boolval; + xmlXPathFreeObject(queryResult); + return result; +} + +- (xmlDocPtr)documentForSnapshot:(XCElementSnapshotDouble *)snapshot query:(NSString *)query +{ + xmlDocPtr doc; + xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); + NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; + id root = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; + int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL); + if (rc >= 0) { + rc = [FBXPath xmlRepresentationWithRootElement:(id)root + writer:writer + elementStore:elementStore + query:query + excludingAttributes:nil]; + if (rc >= 0) { + rc = xmlTextWriterEndDocument(writer); + } + } + xmlFreeTextWriter(writer); + XCTAssertTrue(rc >= 0); + return doc; +} + +- (void)testXPathExtensionFunctions +{ + XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; + snapshot.label = @"Hello World"; + snapshot.value = @"One-Two-Three"; + + xmlDocPtr doc = [self documentForSnapshot:snapshot query:@"//*[@label and @name and @value]"]; + + @try { + XCTAssertTrue([self xpathBooleanResultForQuery:@"matches(//XCUIElementTypeOther/@label, 'Hello.*')" document:doc]); + XCTAssertFalse([self xpathBooleanResultForQuery:@"matches(//XCUIElementTypeOther/@label, 'hello.*')" document:doc]); + XCTAssertTrue([self xpathBooleanResultForQuery:@"matches(//XCUIElementTypeOther/@label, 'hello.*', 'i')" document:doc]); + XCTAssertTrue([self xpathBooleanResultForQuery:@"ends-with(//XCUIElementTypeOther/@name, 'Name')" document:doc]); + XCTAssertFalse([self xpathBooleanResultForQuery:@"ends-with(//XCUIElementTypeOther/@name, 'Foo')" document:doc]); + XCTAssertEqualObjects([self xpathStringResultForQuery:@"lower-case(//XCUIElementTypeOther/@label)" document:doc], @"hello world"); + XCTAssertEqualObjects([self xpathStringResultForQuery:@"upper-case(//XCUIElementTypeOther/@name)" document:doc], @"TESTNAME"); + XCTAssertEqualObjects([self xpathStringResultForQuery:@"replace(//XCUIElementTypeOther/@value, '-', '_')" document:doc], @"One_Two_Three"); + XCTAssertEqualObjects([self xpathStringResultForQuery:@"string-join(tokenize(//XCUIElementTypeOther/@value, '-'), '|')" document:doc], @"One|Two|Three"); + } @finally { + xmlFreeDoc(doc); + } +} + +- (void)testInvalidXPathExtensionRegexp +{ + XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; + snapshot.label = @"Hello World"; + snapshot.value = @"One-Two-Three"; + + xmlDocPtr doc = [self documentForSnapshot:snapshot query:@"//*[@label and @name and @value]"]; + + @try { + [self assertXPathEvaluationFailsForQuery:@"matches(//XCUIElementTypeOther/@label, '[')" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"replace(//XCUIElementTypeOther/@label, '[', '')" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"tokenize(//XCUIElementTypeOther/@value, '[')" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"matches(//XCUIElementTypeOther/@label, 'a', 'z')" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[matches(@label, '[')]" document:doc]; + } @finally { + xmlFreeDoc(doc); + } +} + +- (void)assertXPathEvaluationFailsForQuery:(NSString *)query document:(xmlDocPtr)doc +{ + xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL]; + @try { + XCTAssertEqual(NULL, queryResult); + } @finally { + if (NULL != queryResult) { + xmlXPathFreeObject(queryResult); + } + } +} + +- (void)testInvalidXPathExtensionFunctionArity +{ + XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; + snapshot.label = @"Hello World"; + snapshot.value = @"One-Two-Three"; + + xmlDocPtr doc = [self documentForSnapshot:snapshot query:@"//*[@label and @name and @value]"]; + + @try { + [self assertXPathEvaluationFailsForQuery:@"matches(//XCUIElementTypeOther/@label)" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"lower-case()" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"string-join(//XCUIElementTypeOther/@label)" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"replace(//XCUIElementTypeOther/@label, '-')" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[matches(@label)]" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[lower-case()]" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[string-join(@label)]" document:doc]; + [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[replace(@label, '-')]" document:doc]; + } @finally { + xmlFreeDoc(doc); + } +} + @end From db1ef4a05cdd984ca39e4d0d7484f08e83a32d42 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 26 May 2026 05:18:41 +0000 Subject: [PATCH 32/55] chore(release): 13.2.0 [skip ci] ## [13.2.0](https://github.com/appium/WebDriverAgent/compare/v13.1.3...v13.2.0) (2026-05-26) ### Features * Add XPath extensions ([#1144](https://github.com/appium/WebDriverAgent/issues/1144)) ([a975b89](https://github.com/appium/WebDriverAgent/commit/a975b89ac998d31a72bf3723b843d85af8867cf0)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd689392..abb4f7859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.2.0](https://github.com/appium/WebDriverAgent/compare/v13.1.3...v13.2.0) (2026-05-26) + +### Features + +* Add XPath extensions ([#1144](https://github.com/appium/WebDriverAgent/issues/1144)) ([a975b89](https://github.com/appium/WebDriverAgent/commit/a975b89ac998d31a72bf3723b843d85af8867cf0)) + ## [13.1.3](https://github.com/appium/WebDriverAgent/compare/v13.1.2...v13.1.3) (2026-05-24) ### Bug Fixes diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 89875d0d0..b0fd318ab 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.1.3 + 13.2.0 CFBundleSignature ???? CFBundleVersion - 13.1.3 + 13.2.0 NSPrincipalClass diff --git a/package.json b/package.json index ad7f12f2c..1acc569e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.1.3", + "version": "13.2.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From ff7ac368debb22659509169a0eca530bae3dc879 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 6 Jun 2026 07:48:21 +0200 Subject: [PATCH 33/55] chore: Refactor settings handling API (#1148) --- Gemfile | 1 + WebDriverAgent.xcodeproj/project.pbxproj | 12 + .../Commands/FBSessionCommands.m | 187 +-------- .../Utilities/FBSettingsHandler.h | 35 ++ .../Utilities/FBSettingsHandler.m | 356 ++++++++++++++++++ 5 files changed, 415 insertions(+), 176 deletions(-) create mode 100644 WebDriverAgentLib/Utilities/FBSettingsHandler.h create mode 100644 WebDriverAgentLib/Utilities/FBSettingsHandler.m diff --git a/Gemfile b/Gemfile index ed0a22fa4..355ae9339 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,4 @@ source "https://rubygems.org" gem "fastlane", '~> 2.229' +gem "multi_json" diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index 904461564..4a254ddaa 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -522,6 +522,10 @@ 71F3E7D525417FF400E0C22B /* FBSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F3E7D225417FF400E0C22B /* FBSettings.h */; }; 71F3E7D625417FF400E0C22B /* FBSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F3E7D325417FF400E0C22B /* FBSettings.m */; }; 71F3E7D725417FF400E0C22B /* FBSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F3E7D325417FF400E0C22B /* FBSettings.m */; }; + 71F3E7D825417FF400E0C22C /* FBSettingsHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F3E7D625417FF400E0C22C /* FBSettingsHandler.h */; }; + 71F3E7D925417FF400E0C22C /* FBSettingsHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F3E7D625417FF400E0C22C /* FBSettingsHandler.h */; }; + 71F3E7DA25417FF400E0C22C /* FBSettingsHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F3E7D725417FF400E0C22C /* FBSettingsHandler.m */; }; + 71F3E7DB25417FF400E0C22C /* FBSettingsHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F3E7D725417FF400E0C22C /* FBSettingsHandler.m */; }; 71F5BE23252E576C00EE9EBA /* XCUIElement+FBSwiping.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE21252E576C00EE9EBA /* XCUIElement+FBSwiping.h */; }; 71F5BE24252E576C00EE9EBA /* XCUIElement+FBSwiping.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE21252E576C00EE9EBA /* XCUIElement+FBSwiping.h */; }; 71F5BE25252E576C00EE9EBA /* XCUIElement+FBSwiping.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE22252E576C00EE9EBA /* XCUIElement+FBSwiping.m */; }; @@ -1114,6 +1118,8 @@ 71E75E6C254824230099FC87 /* XCUIElementQuery+FBHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElementQuery+FBHelpers.m"; sourceTree = ""; }; 71F3E7D225417FF400E0C22B /* FBSettings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBSettings.h; sourceTree = ""; }; 71F3E7D325417FF400E0C22B /* FBSettings.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSettings.m; sourceTree = ""; }; + 71F3E7D625417FF400E0C22C /* FBSettingsHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBSettingsHandler.h; sourceTree = ""; }; + 71F3E7D725417FF400E0C22C /* FBSettingsHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSettingsHandler.m; sourceTree = ""; }; 71F5BE21252E576C00EE9EBA /* XCUIElement+FBSwiping.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBSwiping.h"; sourceTree = ""; }; 71F5BE22252E576C00EE9EBA /* XCUIElement+FBSwiping.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBSwiping.m"; sourceTree = ""; }; 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementSwipingTests.m; sourceTree = ""; }; @@ -1982,6 +1988,8 @@ EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */, 71F3E7D225417FF400E0C22B /* FBSettings.h */, 71F3E7D325417FF400E0C22B /* FBSettings.m */, + 71F3E7D625417FF400E0C22C /* FBSettingsHandler.h */, + 71F3E7D725417FF400E0C22C /* FBSettingsHandler.m */, 715AFABF1FFA29180053896D /* FBScreen.h */, 715AFAC01FFA29180053896D /* FBScreen.m */, 71C9EAAA25E8415A00470CD8 /* FBScreenshot.h */, @@ -2333,6 +2341,7 @@ 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */, 71F3E7D525417FF400E0C22B /* FBSettings.h in Headers */, + 71F3E7D825417FF400E0C22C /* FBSettingsHandler.h in Headers */, 641EE63A2240C5CA00173FCB /* XCTest.h in Headers */, 641EE63B2240C5CA00173FCB /* FBAlertsMonitor.h in Headers */, 641EE63D2240C5CA00173FCB /* FBSession.h in Headers */, @@ -2746,6 +2755,7 @@ 719DCF152601EAFB000E765F /* FBNotificationsHelper.h in Headers */, EE35AD4A1E3B77D600A02D78 /* XCTestExpectationDelegate-Protocol.h in Headers */, 71F3E7D425417FF400E0C22B /* FBSettings.h in Headers */, + 71F3E7D925417FF400E0C22C /* FBSettingsHandler.h in Headers */, EE35AD641E3B77D600A02D78 /* XCTUIApplicationMonitor-Protocol.h in Headers */, EE35AD591E3B77D600A02D78 /* XCTKVOExpectation.h in Headers */, 13DE7A43287C2A8D003243C6 /* FBXCAccessibilityElement.h in Headers */, @@ -3159,6 +3169,7 @@ E444DCD624917A5E0060D7EB /* DDRange.m in Sources */, 641EE5D72240C5CA00173FCB /* FBScreenshotCommands.m in Sources */, 71F3E7D725417FF400E0C22B /* FBSettings.m in Sources */, + 71F3E7DA25417FF400E0C22C /* FBSettingsHandler.m in Sources */, 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */, 641EE5DA2240C5CA00173FCB /* XCUIApplicationProcessDelay.m in Sources */, 641EE5DB2240C5CA00173FCB /* FBXPath.m in Sources */, @@ -3374,6 +3385,7 @@ EE158AB91CBD456F00A3E3F0 /* FBAlertViewCommands.m in Sources */, 71BB58F12B96511800CB9BFE /* FBVideoCommands.m in Sources */, 71F3E7D625417FF400E0C22B /* FBSettings.m in Sources */, + 71F3E7DB25417FF400E0C22C /* FBSettingsHandler.m in Sources */, 13DE7A57287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */, 71BB58F82B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */, EE158AB31CBD456F00A3E3F0 /* XCUIElement+FBScrolling.m in Sources */, diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m index 0522633fd..9ee382e1d 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.m +++ b/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -9,7 +9,6 @@ #import "FBSessionCommands.h" #import "FBCapabilities.h" -#import "FBClassChainQueryParser.h" #import "FBConfiguration.h" #import "FBExceptions.h" #import "FBLogger.h" @@ -17,8 +16,8 @@ #import "FBRouteRequest.h" #import "FBSession.h" #import "FBSettings.h" +#import "FBSettingsHandler.h" #import "FBRuntimeUtils.h" -#import "FBActiveAppDetectionPoint.h" #import "FBXCodeCompatibility.h" #import "XCUIApplication+FBHelpers.h" #import "XCUIApplication+FBQuiescence.h" @@ -324,192 +323,28 @@ + (NSArray *)routes + (id)handleGetSettings:(FBRouteRequest *)request { - return FBResponseWithObject( - @{ - FB_SETTING_USE_COMPACT_RESPONSES: @([FBConfiguration shouldUseCompactResponses]), - FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES: [FBConfiguration elementResponseAttributes], - FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY: @([FBConfiguration mjpegServerScreenshotQuality]), - FB_SETTING_MJPEG_SERVER_FRAMERATE: @([FBConfiguration mjpegServerFramerate]), - FB_SETTING_MJPEG_SCALING_FACTOR: @([FBConfiguration mjpegScalingFactor]), - FB_SETTING_MJPEG_FIX_ORIENTATION: @([FBConfiguration mjpegShouldFixOrientation]), - FB_SETTING_SCREENSHOT_QUALITY: @([FBConfiguration screenshotQuality]), - FB_SETTING_KEYBOARD_AUTOCORRECTION: @([FBConfiguration keyboardAutocorrection]), - FB_SETTING_KEYBOARD_PREDICTION: @([FBConfiguration keyboardPrediction]), - FB_SETTING_SNAPSHOT_MAX_DEPTH: @([FBConfiguration snapshotMaxDepth]), - FB_SETTING_SNAPSHOT_MAX_CHILDREN: @([FBConfiguration snapshotMaxChildren]), - FB_SETTING_USE_FIRST_MATCH: @([FBConfiguration useFirstMatch]), - FB_SETTING_WAIT_FOR_IDLE_TIMEOUT: @([FBConfiguration waitForIdleTimeout]), - FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT: @([FBConfiguration animationCoolOffTimeout]), - FB_SETTING_BOUND_ELEMENTS_BY_INDEX: @([FBConfiguration boundElementsByIndex]), - FB_SETTING_REDUCE_MOTION: @([FBConfiguration reduceMotionEnabled]), - FB_SETTING_DEFAULT_ACTIVE_APPLICATION: request.session.defaultActiveApplication, - FB_SETTING_ACTIVE_APP_DETECTION_POINT: FBActiveAppDetectionPoint.sharedInstance.stringCoordinates, - FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR: FBConfiguration.acceptAlertButtonSelector, - FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR: FBConfiguration.dismissAlertButtonSelector, - FB_SETTING_AUTO_CLICK_ALERT_SELECTOR: FBConfiguration.autoClickAlertSelector, - FB_SETTING_DEFAULT_ALERT_ACTION: request.session.defaultAlertAction ?: @"", - FB_SETTING_MAX_TYPING_FREQUENCY: @([FBConfiguration maxTypingFrequency]), - FB_SETTING_RESPECT_SYSTEM_ALERTS: @([FBConfiguration shouldRespectSystemAlerts]), - FB_SETTING_USE_CLEAR_TEXT_SHORTCUT: @([FBConfiguration useClearTextShortcut]), - FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE: @([FBConfiguration includeHittableInPageSource]), - FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE: @([FBConfiguration includeNativeFrameInPageSource]), - FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE: @([FBConfiguration includeMinMaxValueInPageSource]), - FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE: @([FBConfiguration includeCustomActionsInPageSource]), - FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS: @([FBConfiguration enforceCustomSnapshots]), - FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE: @([FBConfiguration limitXpathContextScope]), -#if !TARGET_OS_TV - FB_SETTING_SCREENSHOT_ORIENTATION: [FBConfiguration humanReadableScreenshotOrientation], -#endif - } - ); + return FBResponseWithObject([FBSettingsHandler currentSettingsForSession:request.session]); } -// TODO if we get lots more settings, handling them with a series of if-statements will be unwieldy -// and this should be refactored + (id)handleSetSettings:(FBRouteRequest *)request { - NSDictionary* settings = request.arguments[@"settings"]; - - if (nil != [settings objectForKey:FB_SETTING_USE_COMPACT_RESPONSES]) { - [FBConfiguration setShouldUseCompactResponses:[[settings objectForKey:FB_SETTING_USE_COMPACT_RESPONSES] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]) { - [FBConfiguration setElementResponseAttributes:(NSString *)[settings objectForKey:FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]]; - } - if (nil != [settings objectForKey:FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY]) { - [FBConfiguration setMjpegServerScreenshotQuality:[[settings objectForKey:FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY] unsignedIntegerValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_MJPEG_SERVER_FRAMERATE]) { - [FBConfiguration setMjpegServerFramerate:[[settings objectForKey:FB_SETTING_MJPEG_SERVER_FRAMERATE] unsignedIntegerValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY]) { - [FBConfiguration setScreenshotQuality:[[settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY] unsignedIntegerValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR]) { - [FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] floatValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION]) { - [FBConfiguration setMjpegShouldFixOrientation:[[settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION]) { - [FBConfiguration setKeyboardAutocorrection:[[settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION] boolValue]]; + id settingsArgument = request.arguments[@"settings"]; + if (nil != settingsArgument && ![settingsArgument isKindOfClass:NSDictionary.class]) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"settings must be a dictionary" + traceback:nil]); } - if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION]) { - [FBConfiguration setKeyboardPrediction:[[settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION] boolValue]]; + NSDictionary *settings = settingsArgument ?: @{}; + FBCommandStatus *status = [FBSettingsHandler applySettings:settings + toSession:request.session]; + if (status.hasError) { + return FBResponseWithStatus(status); } - if (nil != [settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS]) { - [FBConfiguration setShouldRespectSystemAlerts:[[settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH]) { - [FBConfiguration setSnapshotMaxDepth:[[settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH] intValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_MAX_CHILDREN]) { - [FBConfiguration setSnapshotMaxChildren:[[settings objectForKey:FB_SETTING_SNAPSHOT_MAX_CHILDREN] intValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_USE_FIRST_MATCH]) { - [FBConfiguration setUseFirstMatch:[[settings objectForKey:FB_SETTING_USE_FIRST_MATCH] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_BOUND_ELEMENTS_BY_INDEX]) { - [FBConfiguration setBoundElementsByIndex:[[settings objectForKey:FB_SETTING_BOUND_ELEMENTS_BY_INDEX] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_REDUCE_MOTION]) { - [FBConfiguration setReduceMotionEnabled:[[settings objectForKey:FB_SETTING_REDUCE_MOTION] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_DEFAULT_ACTIVE_APPLICATION]) { - request.session.defaultActiveApplication = (NSString *)[settings objectForKey:FB_SETTING_DEFAULT_ACTIVE_APPLICATION]; - } - if (nil != [settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT]) { - NSError *error; - if (![FBActiveAppDetectionPoint.sharedInstance setCoordinatesWithString:(NSString *)[settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT] - error:&error]) { - return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription - traceback:nil]); - } - } - if (nil != [settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]) { - [FBConfiguration setAcceptAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]]; - } - if (nil != [settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]) { - [FBConfiguration setDismissAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]]; - } - if (nil != [settings objectForKey:FB_SETTING_AUTO_CLICK_ALERT_SELECTOR]) { - FBCommandStatus *status = [self.class configureAutoClickAlertWithSelector:settings[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR] - forSession:request.session]; - if (status.hasError) { - return FBResponseWithStatus(status); - } - } - if (nil != [settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) { - [FBConfiguration setWaitForIdleTimeout:[[settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT]) { - [FBConfiguration setAnimationCoolOffTimeout:[[settings objectForKey:FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] doubleValue]]; - } - if ([[settings objectForKey:FB_SETTING_DEFAULT_ALERT_ACTION] isKindOfClass:NSString.class]) { - request.session.defaultAlertAction = [settings[FB_SETTING_DEFAULT_ALERT_ACTION] lowercaseString]; - } - if (nil != [settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY]) { - [FBConfiguration setMaxTypingFrequency:[[settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY] unsignedIntegerValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT]) { - [FBConfiguration setUseClearTextShortcut:[[settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE]) { - [FBConfiguration setIncludeHittableInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE]) { - [FBConfiguration setIncludeNativeFrameInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE]) { - [FBConfiguration setIncludeMinMaxValueInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE]) { - [FBConfiguration setIncludeCustomActionsInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS]) { - [FBConfiguration setEnforceCustomSnapshots:[[settings objectForKey:FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS] boolValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE]) { - [FBConfiguration setLimitXpathContextScope:[[settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] boolValue]]; - } - -#if !TARGET_OS_TV - if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]) { - NSError *error; - if (![FBConfiguration setScreenshotOrientation:(NSString *)[settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION] - error:&error]) { - return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription - traceback:nil]); - } - } -#endif - return [self handleGetSettings:request]; } #pragma mark - Helpers -+ (FBCommandStatus *)configureAutoClickAlertWithSelector:(NSString *)selector - forSession:(FBSession *)session -{ - if (0 == [selector length]) { - [FBConfiguration setAutoClickAlertSelector:selector]; - [session disableAlertsMonitor]; - return [FBCommandStatus ok]; - } - - NSError *error; - FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:selector error:&error]; - if (nil == parsedChain) { - return [FBCommandStatus invalidSelectorErrorWithMessage:error.localizedDescription - traceback:nil]; - } - [FBConfiguration setAutoClickAlertSelector:selector]; - [session enableAlertsMonitor]; - return [FBCommandStatus ok]; -} - + (NSString *)buildTimestamp { return [NSString stringWithFormat:@"%@ %@", diff --git a/WebDriverAgentLib/Utilities/FBSettingsHandler.h b/WebDriverAgentLib/Utilities/FBSettingsHandler.h new file mode 100644 index 000000000..bb30384d7 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBSettingsHandler.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class FBCommandStatus; +@class FBSession; + +NS_ASSUME_NONNULL_BEGIN + +@interface FBSettingsHandler : NSObject + +/** + * Applies the given settings dictionary to FBConfiguration and the active session. + * JSON null values are normalized to nil. Nil is applied only for settings that + * support clearing (e.g. alert action and selectors); other keys are skipped so + * null does not get coerced to NO/0. Unknown keys are skipped. + * + * @return nil on success, or an FBCommandStatus describing the validation error. + */ ++ (nullable FBCommandStatus *)applySettings:(NSDictionary *)settings toSession:(FBSession *)session; + +/** + * Returns the current values for all known settings. + */ ++ (NSDictionary *)currentSettingsForSession:(FBSession *)session; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBSettingsHandler.m b/WebDriverAgentLib/Utilities/FBSettingsHandler.m new file mode 100644 index 000000000..c1e17cf3b --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBSettingsHandler.m @@ -0,0 +1,356 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBSettingsHandler.h" + +#import "FBActiveAppDetectionPoint.h" +#import "FBClassChainQueryParser.h" +#import "FBCommandStatus.h" +#import "FBConfiguration.h" +#import "FBSession.h" +#import "FBSettings.h" + +typedef FBCommandStatus * _Nullable (^FBSettingApplyBlock)(FBSession *session, id value); +typedef id _Nonnull (^FBSettingGetBlock)(FBSession *session); + +static id FBNormalizedSettingValue(id value) +{ + return value == NSNull.null ? nil : value; +} + +static NSSet *FBNilClearableSettingKeys(void) +{ + static NSSet *keys; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keys = [NSSet setWithObjects: + FB_SETTING_DEFAULT_ALERT_ACTION, + FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR, + FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR, + FB_SETTING_AUTO_CLICK_ALERT_SELECTOR, + nil]; + }); + return keys; +} + +@implementation FBSettingsHandler + ++ (NSDictionary *)settersMap +{ + static NSDictionary *settersMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *map = [NSMutableDictionary dictionary]; + map[FB_SETTING_USE_COMPACT_RESPONSES] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setShouldUseCompactResponses:[value boolValue]]; + return nil; + }; + map[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setElementResponseAttributes:(NSString *)value]; + return nil; + }; + map[FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setMjpegServerScreenshotQuality:[value unsignedIntegerValue]]; + return nil; + }; + map[FB_SETTING_MJPEG_SERVER_FRAMERATE] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setMjpegServerFramerate:[value unsignedIntegerValue]]; + return nil; + }; + map[FB_SETTING_SCREENSHOT_QUALITY] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setScreenshotQuality:[value unsignedIntegerValue]]; + return nil; + }; + map[FB_SETTING_MJPEG_SCALING_FACTOR] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setMjpegScalingFactor:[value floatValue]]; + return nil; + }; + map[FB_SETTING_MJPEG_FIX_ORIENTATION] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setMjpegShouldFixOrientation:[value boolValue]]; + return nil; + }; + map[FB_SETTING_KEYBOARD_AUTOCORRECTION] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setKeyboardAutocorrection:[value boolValue]]; + return nil; + }; + map[FB_SETTING_KEYBOARD_PREDICTION] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setKeyboardPrediction:[value boolValue]]; + return nil; + }; + map[FB_SETTING_RESPECT_SYSTEM_ALERTS] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setShouldRespectSystemAlerts:[value boolValue]]; + return nil; + }; + map[FB_SETTING_SNAPSHOT_MAX_DEPTH] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setSnapshotMaxDepth:[value intValue]]; + return nil; + }; + map[FB_SETTING_SNAPSHOT_MAX_CHILDREN] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setSnapshotMaxChildren:[value intValue]]; + return nil; + }; + map[FB_SETTING_USE_FIRST_MATCH] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setUseFirstMatch:[value boolValue]]; + return nil; + }; + map[FB_SETTING_BOUND_ELEMENTS_BY_INDEX] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setBoundElementsByIndex:[value boolValue]]; + return nil; + }; + map[FB_SETTING_REDUCE_MOTION] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setReduceMotionEnabled:[value boolValue]]; + return nil; + }; + map[FB_SETTING_DEFAULT_ACTIVE_APPLICATION] = ^FBCommandStatus *(FBSession *session, id value) { + session.defaultActiveApplication = (NSString *)value; + return nil; + }; + map[FB_SETTING_ACTIVE_APP_DETECTION_POINT] = ^FBCommandStatus *(FBSession *session, id value) { + NSError *error; + if (![FBActiveAppDetectionPoint.sharedInstance setCoordinatesWithString:(NSString *)value + error:&error]) { + return [FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]; + } + return nil; + }; + map[FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setAcceptAlertButtonSelector:(NSString *)value]; + return nil; + }; + map[FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setDismissAlertButtonSelector:(NSString *)value]; + return nil; + }; + map[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR] = ^FBCommandStatus *(FBSession *session, id value) { + return [self configureAutoClickAlertWithSelector:(NSString *)value forSession:session]; + }; + map[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setWaitForIdleTimeout:[value doubleValue]]; + return nil; + }; + map[FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setAnimationCoolOffTimeout:[value doubleValue]]; + return nil; + }; + map[FB_SETTING_DEFAULT_ALERT_ACTION] = ^FBCommandStatus *(FBSession *session, id value) { + if (nil == value) { + session.defaultAlertAction = nil; + } else if ([value isKindOfClass:NSString.class]) { + session.defaultAlertAction = [(NSString *)value lowercaseString]; + } + return nil; + }; + map[FB_SETTING_MAX_TYPING_FREQUENCY] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setMaxTypingFrequency:[value unsignedIntegerValue]]; + return nil; + }; + map[FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setUseClearTextShortcut:[value boolValue]]; + return nil; + }; + map[FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setIncludeHittableInPageSource:[value boolValue]]; + return nil; + }; + map[FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setIncludeNativeFrameInPageSource:[value boolValue]]; + return nil; + }; + map[FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setIncludeMinMaxValueInPageSource:[value boolValue]]; + return nil; + }; + map[FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setIncludeCustomActionsInPageSource:[value boolValue]]; + return nil; + }; + map[FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setEnforceCustomSnapshots:[value boolValue]]; + return nil; + }; + map[FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setLimitXpathContextScope:[value boolValue]]; + return nil; + }; +#if !TARGET_OS_TV + map[FB_SETTING_SCREENSHOT_ORIENTATION] = ^FBCommandStatus *(FBSession *session, id value) { + NSError *error; + if (![FBConfiguration setScreenshotOrientation:(NSString *)value error:&error]) { + return [FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]; + } + return nil; + }; +#endif + settersMap = map.copy; + }); + return settersMap; +} + ++ (NSDictionary *)gettersMap +{ + static NSDictionary *gettersMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *map = [NSMutableDictionary dictionary]; + map[FB_SETTING_USE_COMPACT_RESPONSES] = ^id(FBSession *session) { + return @([FBConfiguration shouldUseCompactResponses]); + }; + map[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES] = ^id(FBSession *session) { + return [FBConfiguration elementResponseAttributes]; + }; + map[FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY] = ^id(FBSession *session) { + return @([FBConfiguration mjpegServerScreenshotQuality]); + }; + map[FB_SETTING_MJPEG_SERVER_FRAMERATE] = ^id(FBSession *session) { + return @([FBConfiguration mjpegServerFramerate]); + }; + map[FB_SETTING_MJPEG_SCALING_FACTOR] = ^id(FBSession *session) { + return @([FBConfiguration mjpegScalingFactor]); + }; + map[FB_SETTING_MJPEG_FIX_ORIENTATION] = ^id(FBSession *session) { + return @([FBConfiguration mjpegShouldFixOrientation]); + }; + map[FB_SETTING_SCREENSHOT_QUALITY] = ^id(FBSession *session) { + return @([FBConfiguration screenshotQuality]); + }; + map[FB_SETTING_KEYBOARD_AUTOCORRECTION] = ^id(FBSession *session) { + return @([FBConfiguration keyboardAutocorrection]); + }; + map[FB_SETTING_KEYBOARD_PREDICTION] = ^id(FBSession *session) { + return @([FBConfiguration keyboardPrediction]); + }; + map[FB_SETTING_SNAPSHOT_MAX_DEPTH] = ^id(FBSession *session) { + return @([FBConfiguration snapshotMaxDepth]); + }; + map[FB_SETTING_SNAPSHOT_MAX_CHILDREN] = ^id(FBSession *session) { + return @([FBConfiguration snapshotMaxChildren]); + }; + map[FB_SETTING_USE_FIRST_MATCH] = ^id(FBSession *session) { + return @([FBConfiguration useFirstMatch]); + }; + map[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] = ^id(FBSession *session) { + return @([FBConfiguration waitForIdleTimeout]); + }; + map[FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] = ^id(FBSession *session) { + return @([FBConfiguration animationCoolOffTimeout]); + }; + map[FB_SETTING_BOUND_ELEMENTS_BY_INDEX] = ^id(FBSession *session) { + return @([FBConfiguration boundElementsByIndex]); + }; + map[FB_SETTING_REDUCE_MOTION] = ^id(FBSession *session) { + return @([FBConfiguration reduceMotionEnabled]); + }; + map[FB_SETTING_DEFAULT_ACTIVE_APPLICATION] = ^id(FBSession *session) { + return session.defaultActiveApplication; + }; + map[FB_SETTING_ACTIVE_APP_DETECTION_POINT] = ^id(FBSession *session) { + return FBActiveAppDetectionPoint.sharedInstance.stringCoordinates; + }; + map[FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR] = ^id(FBSession *session) { + return FBConfiguration.acceptAlertButtonSelector; + }; + map[FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR] = ^id(FBSession *session) { + return FBConfiguration.dismissAlertButtonSelector; + }; + map[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR] = ^id(FBSession *session) { + return FBConfiguration.autoClickAlertSelector; + }; + map[FB_SETTING_DEFAULT_ALERT_ACTION] = ^id(FBSession *session) { + return session.defaultAlertAction ?: @""; + }; + map[FB_SETTING_MAX_TYPING_FREQUENCY] = ^id(FBSession *session) { + return @([FBConfiguration maxTypingFrequency]); + }; + map[FB_SETTING_RESPECT_SYSTEM_ALERTS] = ^id(FBSession *session) { + return @([FBConfiguration shouldRespectSystemAlerts]); + }; + map[FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] = ^id(FBSession *session) { + return @([FBConfiguration useClearTextShortcut]); + }; + map[FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] = ^id(FBSession *session) { + return @([FBConfiguration includeHittableInPageSource]); + }; + map[FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] = ^id(FBSession *session) { + return @([FBConfiguration includeNativeFrameInPageSource]); + }; + map[FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] = ^id(FBSession *session) { + return @([FBConfiguration includeMinMaxValueInPageSource]); + }; + map[FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE] = ^id(FBSession *session) { + return @([FBConfiguration includeCustomActionsInPageSource]); + }; + map[FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS] = ^id(FBSession *session) { + return @([FBConfiguration enforceCustomSnapshots]); + }; + map[FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] = ^id(FBSession *session) { + return @([FBConfiguration limitXpathContextScope]); + }; +#if !TARGET_OS_TV + map[FB_SETTING_SCREENSHOT_ORIENTATION] = ^id(FBSession *session) { + return [FBConfiguration humanReadableScreenshotOrientation]; + }; +#endif + gettersMap = map.copy; + }); + return gettersMap; +} + ++ (NSDictionary *)currentSettingsForSession:(FBSession *)session +{ + NSDictionary *gettersMap = [self gettersMap]; + NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:gettersMap.count]; + for (NSString *key in gettersMap) { + settings[key] = gettersMap[key](session); + } + return settings.copy; +} + ++ (nullable FBCommandStatus *)applySettings:(NSDictionary *)settings toSession:(FBSession *)session +{ + NSDictionary *settersMap = [self settersMap]; + NSSet *nilClearableKeys = FBNilClearableSettingKeys(); + for (NSString *key in settings) { + FBSettingApplyBlock handler = settersMap[key]; + if (nil == handler) { + continue; + } + id value = FBNormalizedSettingValue(settings[key]); + if (nil == value && ![nilClearableKeys containsObject:key]) { + continue; + } + FBCommandStatus *status = handler(session, value); + if (status.hasError) { + return status; + } + } + return nil; +} + ++ (FBCommandStatus *)configureAutoClickAlertWithSelector:(NSString *)selector + forSession:(FBSession *)session +{ + if (0 == [selector length]) { + [FBConfiguration setAutoClickAlertSelector:selector]; + [session disableAlertsMonitor]; + return [FBCommandStatus ok]; + } + + NSError *error; + FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:selector error:&error]; + if (nil == parsedChain) { + return [FBCommandStatus invalidSelectorErrorWithMessage:error.localizedDescription + traceback:nil]; + } + [FBConfiguration setAutoClickAlertSelector:selector]; + [session enableAlertsMonitor]; + return [FBCommandStatus ok]; +} + +@end From f2e5c253ee2592b9e936b97c518f669151e32921 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 6 Jun 2026 05:51:55 +0000 Subject: [PATCH 34/55] chore(release): 13.2.1 [skip ci] ## [13.2.1](https://github.com/appium/WebDriverAgent/compare/v13.2.0...v13.2.1) (2026-06-06) ### Miscellaneous Chores * Refactor settings handling API ([#1148](https://github.com/appium/WebDriverAgent/issues/1148)) ([ff7ac36](https://github.com/appium/WebDriverAgent/commit/ff7ac368debb22659509169a0eca530bae3dc879)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abb4f7859..29696f69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.2.1](https://github.com/appium/WebDriverAgent/compare/v13.2.0...v13.2.1) (2026-06-06) + +### Miscellaneous Chores + +* Refactor settings handling API ([#1148](https://github.com/appium/WebDriverAgent/issues/1148)) ([ff7ac36](https://github.com/appium/WebDriverAgent/commit/ff7ac368debb22659509169a0eca530bae3dc879)) + ## [13.2.0](https://github.com/appium/WebDriverAgent/compare/v13.1.3...v13.2.0) (2026-05-26) ### Features diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index b0fd318ab..473f2dae0 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.2.0 + 13.2.1 CFBundleSignature ???? CFBundleVersion - 13.2.0 + 13.2.1 NSPrincipalClass diff --git a/package.json b/package.json index 1acc569e3..ac55abffd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.2.0", + "version": "13.2.1", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 923b523b55f880b921de2c95a82786ce0699cb9d Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 6 Jun 2026 19:18:03 +0200 Subject: [PATCH 35/55] chore: Refactor session creation handler (#1149) --- .../Commands/FBSessionCommands.m | 293 +++++++++++------- 1 file changed, 179 insertions(+), 114 deletions(-) diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m index 9ee382e1d..c567381ab 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.m +++ b/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -90,134 +90,35 @@ + (NSArray *)routes } NSDictionary *capabilities; - NSError *error; - if (![request.arguments[@"capabilities"] isKindOfClass:NSDictionary.class]) { - return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:@"'capabilities' is mandatory to create a new session" - traceback:nil]); - } - if (nil == (capabilities = FBParseCapabilities((NSDictionary *)request.arguments[@"capabilities"], &error))) { - return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.localizedDescription traceback:nil]); - } - - [FBConfiguration resetSessionSettings]; - if (capabilities[FB_SETTING_USE_COMPACT_RESPONSES]) { - [FBConfiguration setShouldUseCompactResponses:[capabilities[FB_SETTING_USE_COMPACT_RESPONSES] boolValue]]; - } - NSString *elementResponseAttributes = capabilities[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]; - if (elementResponseAttributes) { - [FBConfiguration setElementResponseAttributes:elementResponseAttributes]; - } - if (capabilities[FB_CAP_MAX_TYPING_FREQUENCY]) { - [FBConfiguration setMaxTypingFrequency:[capabilities[FB_CAP_MAX_TYPING_FREQUENCY] unsignedIntegerValue]]; - } - if (capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER]) { - [FBConfiguration setShouldUseSingletonTestManager:[capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER] boolValue]]; - } - if (capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS]) { - if ([capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS] boolValue]) { - [FBConfiguration disableScreenshots]; - } else { - [FBConfiguration enableScreenshots]; - } - } - if (capabilities[FB_CAP_SHOULD_TERMINATE_APP]) { - [FBConfiguration setShouldTerminateApp:[capabilities[FB_CAP_SHOULD_TERMINATE_APP] boolValue]]; - } - NSNumber *delay = capabilities[FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC]; - if ([delay doubleValue] > 0.0) { - [XCUIApplicationProcessDelay setEventLoopHasIdledDelay:[delay doubleValue]]; - } else { - [XCUIApplicationProcessDelay disableEventLoopDelay]; + id errorResponse = [self capabilitiesFromCreateSessionRequest:request + capabilitiesOut:&capabilities]; + if (nil != errorResponse) { + return errorResponse; } - if (nil != capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) { - FBConfiguration.waitForIdleTimeout = [capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]; - } - - if (nil == capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] || - [capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] boolValue]) { - [FBConfiguration forceSimulatorSoftwareKeyboardPresence]; - } + [self applyConfigurationFromCapabilities:capabilities]; NSString *bundleID = capabilities[FB_CAP_BUNDLE_ID]; NSString *initialUrl = capabilities[FB_CAP_INITIAL_URL]; XCUIApplication *app = nil; - if (bundleID != nil) { - app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleID]; - BOOL forceAppLaunch = YES; - if (nil != capabilities[FB_CAP_FORCE_APP_LAUNCH]) { - forceAppLaunch = [capabilities[FB_CAP_FORCE_APP_LAUNCH] boolValue]; - } - XCUIApplicationState appState = app.state; - BOOL isAppRunning = appState >= XCUIApplicationStateRunningBackground; - if (!isAppRunning || (isAppRunning && forceAppLaunch)) { - app.fb_shouldWaitForQuiescence = nil == capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] - || [capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] boolValue]; - app.launchArguments = (NSArray *)capabilities[FB_CAP_ARGUMENTS] ?: @[]; - app.launchEnvironment = (NSDictionary *)capabilities[FB_CAP_ENVIRNOMENT] ?: @{}; - if (nil != initialUrl) { - if (app.running) { - [app terminate]; - } - id errorResponse = [self openDeepLink:initialUrl - withApplication:bundleID - timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]]; - if (nil != errorResponse) { - return errorResponse; - } - } else { - NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout(); - if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) { - _XCTSetApplicationStateTimeout([capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC] doubleValue]); - } - @try { - [app launch]; - } @catch (NSException *e) { - return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:e.reason traceback:nil]); - } @finally { - if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) { - _XCTSetApplicationStateTimeout(defaultTimeout); - } - } - } - if (!app.running) { - NSString *errorMsg = [NSString stringWithFormat:@"Cannot launch %@ application. Make sure the correct bundle identifier has been provided in capabilities and check the device log for possible crash report occurrences", bundleID]; - return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg - traceback:nil]); - } - } else if (appState == XCUIApplicationStateRunningBackground && !forceAppLaunch) { - if (nil != initialUrl) { - id errorResponse = [self openDeepLink:initialUrl - withApplication:bundleID - timeout:nil]; - if (nil != errorResponse) { - return errorResponse; - } - } else { - [app activate]; - } - } + errorResponse = [self prepareApplicationForSessionWithBundleID:bundleID + initialUrl:initialUrl + capabilities:capabilities + application:&app]; + if (nil != errorResponse) { + return errorResponse; } if (nil != initialUrl && nil == bundleID) { - id errorResponse = [self openDeepLink:initialUrl - withApplication:nil - timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]]; + errorResponse = [self openDeepLink:initialUrl + withApplication:nil + timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]]; if (nil != errorResponse) { return errorResponse; } } - if (capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]) { - [FBSession initWithApplication:app - defaultAlertAction:(id)capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]]; - } else { - [FBSession initWithApplication:app]; - } - - if (nil != capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY]) { - FBSession.activeSession.useNativeCachingStrategy = [capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY] boolValue]; - } + [self initializeSessionWithApplication:app capabilities:capabilities]; return FBResponseWithObject(FBSessionCommands.sessionInformation); } @@ -343,6 +244,170 @@ + (NSArray *)routes } +#pragma mark - Session Creation Helpers + ++ (nullable id)capabilitiesFromCreateSessionRequest:(FBRouteRequest *)request + capabilitiesOut:(NSDictionary *_Nonnull *_Nonnull)capabilitiesOut +{ + if (![request.arguments[@"capabilities"] isKindOfClass:NSDictionary.class]) { + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:@"'capabilities' is mandatory to create a new session" + traceback:nil]); + } + NSError *error; + NSDictionary *capabilities = FBParseCapabilities((NSDictionary *)request.arguments[@"capabilities"], &error); + if (nil == capabilities) { + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.localizedDescription traceback:nil]); + } + *capabilitiesOut = capabilities; + return nil; +} + ++ (void)applyConfigurationFromCapabilities:(NSDictionary *)capabilities +{ + [FBConfiguration resetSessionSettings]; + if (capabilities[FB_SETTING_USE_COMPACT_RESPONSES]) { + [FBConfiguration setShouldUseCompactResponses:[capabilities[FB_SETTING_USE_COMPACT_RESPONSES] boolValue]]; + } + NSString *elementResponseAttributes = capabilities[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]; + if (elementResponseAttributes) { + [FBConfiguration setElementResponseAttributes:elementResponseAttributes]; + } + if (capabilities[FB_CAP_MAX_TYPING_FREQUENCY]) { + [FBConfiguration setMaxTypingFrequency:[capabilities[FB_CAP_MAX_TYPING_FREQUENCY] unsignedIntegerValue]]; + } + if (capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER]) { + [FBConfiguration setShouldUseSingletonTestManager:[capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER] boolValue]]; + } + if (capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS]) { + if ([capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS] boolValue]) { + [FBConfiguration disableScreenshots]; + } else { + [FBConfiguration enableScreenshots]; + } + } + if (capabilities[FB_CAP_SHOULD_TERMINATE_APP]) { + [FBConfiguration setShouldTerminateApp:[capabilities[FB_CAP_SHOULD_TERMINATE_APP] boolValue]]; + } + NSNumber *delay = capabilities[FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC]; + if ([delay doubleValue] > 0.0) { + [XCUIApplicationProcessDelay setEventLoopHasIdledDelay:[delay doubleValue]]; + } else { + [XCUIApplicationProcessDelay disableEventLoopDelay]; + } + if (nil != capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) { + FBConfiguration.waitForIdleTimeout = [capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]; + } + if (nil == capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] || + [capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] boolValue]) { + [FBConfiguration forceSimulatorSoftwareKeyboardPresence]; + } +} + ++ (nullable id)prepareApplicationForSessionWithBundleID:(nullable NSString *)bundleID + initialUrl:(nullable NSString *)initialUrl + capabilities:(NSDictionary *)capabilities + application:(XCUIApplication *_Nullable *_Nonnull)applicationOut +{ + if (nil == bundleID) { + *applicationOut = nil; + return nil; + } + + XCUIApplication *app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleID]; + BOOL forceAppLaunch = nil == capabilities[FB_CAP_FORCE_APP_LAUNCH] + || [capabilities[FB_CAP_FORCE_APP_LAUNCH] boolValue]; + XCUIApplicationState appState = app.state; + BOOL isAppRunning = appState >= XCUIApplicationStateRunningBackground; + + if (!isAppRunning || (isAppRunning && forceAppLaunch)) { + id errorResponse = [self launchApplication:app + bundleID:bundleID + initialUrl:initialUrl + capabilities:capabilities]; + if (nil != errorResponse) { + return errorResponse; + } + } else if (appState == XCUIApplicationStateRunningBackground && !forceAppLaunch) { + id errorResponse = [self activateBackgroundApplication:app + bundleID:bundleID + initialUrl:initialUrl]; + if (nil != errorResponse) { + return errorResponse; + } + } + + *applicationOut = app; + return nil; +} + ++ (nullable id)launchApplication:(XCUIApplication *)app + bundleID:(NSString *)bundleID + initialUrl:(nullable NSString *)initialUrl + capabilities:(NSDictionary *)capabilities +{ + app.fb_shouldWaitForQuiescence = nil == capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] + || [capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] boolValue]; + app.launchArguments = (NSArray *)capabilities[FB_CAP_ARGUMENTS] ?: @[]; + app.launchEnvironment = (NSDictionary *)capabilities[FB_CAP_ENVIRNOMENT] ?: @{}; + + if (nil != initialUrl) { + if (app.running) { + [app terminate]; + } + id errorResponse = [self openDeepLink:initialUrl + withApplication:bundleID + timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]]; + if (nil != errorResponse) { + return errorResponse; + } + } else { + NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout(); + if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) { + _XCTSetApplicationStateTimeout([capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC] doubleValue]); + } + @try { + [app launch]; + } @catch (NSException *e) { + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:e.reason traceback:nil]); + } @finally { + if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) { + _XCTSetApplicationStateTimeout(defaultTimeout); + } + } + } + + if (!app.running) { + NSString *errorMsg = [NSString stringWithFormat:@"Cannot launch %@ application. Make sure the correct bundle identifier has been provided in capabilities and check the device log for possible crash report occurrences", bundleID]; + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg traceback:nil]); + } + return nil; +} + ++ (nullable id)activateBackgroundApplication:(XCUIApplication *)app + bundleID:(NSString *)bundleID + initialUrl:(nullable NSString *)initialUrl +{ + if (nil != initialUrl) { + return [self openDeepLink:initialUrl withApplication:bundleID timeout:nil]; + } + [app activate]; + return nil; +} + ++ (void)initializeSessionWithApplication:(nullable XCUIApplication *)app + capabilities:(NSDictionary *)capabilities +{ + if (capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]) { + [FBSession initWithApplication:app + defaultAlertAction:(id)capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]]; + } else { + [FBSession initWithApplication:app]; + } + if (nil != capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY]) { + FBSession.activeSession.useNativeCachingStrategy = [capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY] boolValue]; + } +} + #pragma mark - Helpers + (NSString *)buildTimestamp From 2eb1125a940cd17e49ea3d271fee11f52795392b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 6 Jun 2026 17:22:02 +0000 Subject: [PATCH 36/55] chore(release): 13.2.2 [skip ci] ## [13.2.2](https://github.com/appium/WebDriverAgent/compare/v13.2.1...v13.2.2) (2026-06-06) ### Miscellaneous Chores * Refactor session creation handler ([#1149](https://github.com/appium/WebDriverAgent/issues/1149)) ([923b523](https://github.com/appium/WebDriverAgent/commit/923b523b55f880b921de2c95a82786ce0699cb9d)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29696f69f..dda25b279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.2.2](https://github.com/appium/WebDriverAgent/compare/v13.2.1...v13.2.2) (2026-06-06) + +### Miscellaneous Chores + +* Refactor session creation handler ([#1149](https://github.com/appium/WebDriverAgent/issues/1149)) ([923b523](https://github.com/appium/WebDriverAgent/commit/923b523b55f880b921de2c95a82786ce0699cb9d)) + ## [13.2.1](https://github.com/appium/WebDriverAgent/compare/v13.2.0...v13.2.1) (2026-06-06) ### Miscellaneous Chores diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 473f2dae0..e877592bb 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.2.1 + 13.2.2 CFBundleSignature ???? CFBundleVersion - 13.2.1 + 13.2.2 NSPrincipalClass diff --git a/package.json b/package.json index ac55abffd..0ea2e362e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.2.1", + "version": "13.2.2", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 98d79e7c3875424cb4b5fdee55bb079286a14b05 Mon Sep 17 00:00:00 2001 From: Abhinav Pandey Date: Sun, 7 Jun 2026 13:31:30 +0530 Subject: [PATCH 37/55] fix: auto-handle iOS 18+ limited access permission prompt (#1150) --- .../Categories/XCUIApplication+FBAlert.h | 14 +++++++++++++- .../Categories/XCUIApplication+FBAlert.m | 13 +++++++++++++ WebDriverAgentLib/FBAlert.m | 3 +++ WebDriverAgentLib/Utilities/FBAlertsMonitor.m | 16 +++++++++++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h index 01bea467d..d9c81ef8c 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h @@ -8,6 +8,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface XCUIApplication (FBAlert) /* The accessiblity label used for Safari app */ @@ -18,6 +20,16 @@ extern NSString *const FB_SAFARI_APP_NAME; @return Alert element instance */ -- (XCUIElement *)fb_alertElement; +- (nullable XCUIElement *)fb_alertElement; + +/** + Retrieve an alert element hosted by the iOS 18+ limited access permission prompt + process. See https://github.com/appium/appium/issues/20591 + + @return Alert element instance if the prompt is present, otherwise nil + */ ++ (nullable XCUIElement *)fb_limitedAccessPromptAlertElement; @end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m index 36628bc61..ba86f4bc1 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m @@ -17,9 +17,22 @@ NSString *const FB_SAFARI_APP_NAME = @"Safari"; +// The iOS 18+ limited access permission prompt (e.g. the "Select Contacts" view) +// runs in a dedicated process that is not reported by fb_activeApplications. +static NSString *const FB_LIMITED_ACCESS_PROMPT_BUNDLE_ID = @"com.apple.ContactsUI.LimitedAccessPromptView"; + @implementation XCUIApplication (FBAlert) ++ (nullable XCUIElement *)fb_limitedAccessPromptAlertElement +{ + XCUIApplication *promptApp = [[XCUIApplication alloc] initWithBundleIdentifier:FB_LIMITED_ACCESS_PROMPT_BUNDLE_ID]; + if (promptApp.state < XCUIApplicationStateRunningForeground) { + return nil; + } + return promptApp.fb_alertElement; +} + - (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement *)scrollView viewSnapshot:(id)viewSnapshot { diff --git a/WebDriverAgentLib/FBAlert.m b/WebDriverAgentLib/FBAlert.m index 2e2de763d..fc620da05 100644 --- a/WebDriverAgentLib/FBAlert.m +++ b/WebDriverAgentLib/FBAlert.m @@ -267,6 +267,9 @@ - (XCUIElement *)alertElement } else { self.element = systemApp.fb_alertElement ?: self.application.fb_alertElement; } + if (nil == self.element) { + self.element = [XCUIApplication fb_limitedAccessPromptAlertElement]; + } } return self.element; } diff --git a/WebDriverAgentLib/Utilities/FBAlertsMonitor.m b/WebDriverAgentLib/Utilities/FBAlertsMonitor.m index 3dd221198..176d4f29f 100644 --- a/WebDriverAgentLib/Utilities/FBAlertsMonitor.m +++ b/WebDriverAgentLib/Utilities/FBAlertsMonitor.m @@ -48,22 +48,36 @@ - (void)scheduleNextTick } dispatch_async(dispatch_get_main_queue(), ^{ + id delegate = self.delegate; NSArray *activeApps = XCUIApplication.fb_activeApplications; + BOOL didDetectAlert = NO; for (XCUIApplication *activeApp in activeApps) { XCUIElement *alertElement = nil; @try { alertElement = activeApp.fb_alertElement; if (nil != alertElement) { - [self.delegate didDetectAlert:[FBAlert alertWithElement:alertElement]]; + [delegate didDetectAlert:[FBAlert alertWithElement:alertElement]]; } } @catch (NSException *e) { [FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols]; } if (nil != alertElement) { + didDetectAlert = YES; break; } } + if (!didDetectAlert) { + @try { + XCUIElement *alertElement = [XCUIApplication fb_limitedAccessPromptAlertElement]; + if (nil != alertElement) { + [delegate didDetectAlert:[FBAlert alertWithElement:alertElement]]; + } + } @catch (NSException *e) { + [FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols]; + } + } + if (self.isMonitoring) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delta), dispatch_get_main_queue(), ^{ [self scheduleNextTick]; From 23a6181c037be682c43d07d7aa721805f00a1ef9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 7 Jun 2026 08:05:11 +0000 Subject: [PATCH 38/55] chore(release): 13.2.3 [skip ci] ## [13.2.3](https://github.com/appium/WebDriverAgent/compare/v13.2.2...v13.2.3) (2026-06-07) ### Bug Fixes * auto-handle iOS 18+ limited access permission prompt ([#1150](https://github.com/appium/WebDriverAgent/issues/1150)) ([98d79e7](https://github.com/appium/WebDriverAgent/commit/98d79e7c3875424cb4b5fdee55bb079286a14b05)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dda25b279..d3a8bfd8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.2.3](https://github.com/appium/WebDriverAgent/compare/v13.2.2...v13.2.3) (2026-06-07) + +### Bug Fixes + +* auto-handle iOS 18+ limited access permission prompt ([#1150](https://github.com/appium/WebDriverAgent/issues/1150)) ([98d79e7](https://github.com/appium/WebDriverAgent/commit/98d79e7c3875424cb4b5fdee55bb079286a14b05)) + ## [13.2.2](https://github.com/appium/WebDriverAgent/compare/v13.2.1...v13.2.2) (2026-06-06) ### Miscellaneous Chores diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index e877592bb..72ca7c236 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.2.2 + 13.2.3 CFBundleSignature ???? CFBundleVersion - 13.2.2 + 13.2.3 NSPrincipalClass diff --git a/package.json b/package.json index 0ea2e362e..39ad7007d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.2.2", + "version": "13.2.3", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From eea2229f8d2e8bd2dd936fe3ddb69a9458789f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Egl=C4=ABtis?= <37242620+eglitise@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:53:50 +0300 Subject: [PATCH 39/55] fix: update WebDriverAgentRunner app icon (#1151) --- .../AppIcon.appiconset/icon-1024.png | Bin 63450 -> 58884 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/icon-1024.png index 8f359b2bebf0237192445a6d5877179c69025228..373e26bd1a197a83e7b79573b131e63b0667b251 100644 GIT binary patch literal 58884 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 literal 63450 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 z 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 SJs<7Ay{oLLRH*p$#s2}mrgG~5 From 5fe4d83f9a83e09844b30dfc5a812fcf7a57f53d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 8 Jun 2026 17:13:36 +0000 Subject: [PATCH 40/55] chore(release): 13.2.4 [skip ci] ## [13.2.4](https://github.com/appium/WebDriverAgent/compare/v13.2.3...v13.2.4) (2026-06-08) ### Bug Fixes * update WebDriverAgentRunner app icon ([#1151](https://github.com/appium/WebDriverAgent/issues/1151)) ([eea2229](https://github.com/appium/WebDriverAgent/commit/eea2229f8d2e8bd2dd936fe3ddb69a9458789f49)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3a8bfd8c..2754a2351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.2.4](https://github.com/appium/WebDriverAgent/compare/v13.2.3...v13.2.4) (2026-06-08) + +### Bug Fixes + +* update WebDriverAgentRunner app icon ([#1151](https://github.com/appium/WebDriverAgent/issues/1151)) ([eea2229](https://github.com/appium/WebDriverAgent/commit/eea2229f8d2e8bd2dd936fe3ddb69a9458789f49)) + ## [13.2.3](https://github.com/appium/WebDriverAgent/compare/v13.2.2...v13.2.3) (2026-06-07) ### Bug Fixes diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 72ca7c236..8e9dfa5bf 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.2.3 + 13.2.4 CFBundleSignature ???? CFBundleVersion - 13.2.3 + 13.2.4 NSPrincipalClass diff --git a/package.json b/package.json index 39ad7007d..dbb85c4c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.2.3", + "version": "13.2.4", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From e6156212e6fba6af98a69a400f5fa18b67f1e3e3 Mon Sep 17 00:00:00 2001 From: Anton Yereshchenko Date: Tue, 9 Jun 2026 05:23:02 +0200 Subject: [PATCH 41/55] feat: Expose native isAccessibilityElement (#1146) * feat: expose native isAccessibilityElement * refactor: rename nativeAccessible to nativeAccessibilityElement Address review feedback to use the full word for clarity: - protocol property wdNativeAccessible -> wdNativeAccessibilityElement - JSON key isNativeAccessible -> isNativeAccessibilityElement - XML attribute nativeAccessible -> nativeAccessibilityElement - setting includeNativeAccessibleInPageSource -> includeNativeAccessibilityElementInPageSource Co-authored-by: Cursor * Unify JSON attribute key prefixing * Remove empty-key guard from FBJsonPrefixedAttributeKey --------- Co-authored-by: Cursor --- .../Categories/XCUIApplication+FBHelpers.m | 13 ++++++++++- .../XCUIElement+FBWebDriverAttributes.m | 5 +++++ WebDriverAgentLib/Routing/FBElement.h | 3 +++ WebDriverAgentLib/Utilities/FBConfiguration.h | 18 +++++++++++++++ WebDriverAgentLib/Utilities/FBConfiguration.m | 11 ++++++++++ WebDriverAgentLib/Utilities/FBSettings.h | 1 + WebDriverAgentLib/Utilities/FBSettings.m | 1 + .../Utilities/FBSettingsHandler.m | 7 ++++++ WebDriverAgentLib/Utilities/FBXPath.m | 22 +++++++++++++++++++ .../FBElementAttributeTests.m | 9 ++++++++ .../UnitTests/Doubles/XCUIElementDouble.h | 1 + .../UnitTests/Doubles/XCUIElementDouble.m | 1 + WebDriverAgentTests/UnitTests/FBXPathTests.m | 4 ++-- .../Doubles/XCUIElementDouble.h | 1 + .../Doubles/XCUIElementDouble.m | 1 + 15 files changed, 95 insertions(+), 3 deletions(-) diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m index bda8a8c5f..454288831 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m @@ -44,6 +44,7 @@ static NSString* const FBExclusionAttributeEnabled = @"enabled"; static NSString* const FBExclusionAttributeVisible = @"visible"; static NSString* const FBExclusionAttributeAccessible = @"accessible"; +static NSString* const FBExclusionAttributeNativeAccessibilityElement = @"nativeAccessibilityElement"; static NSString* const FBExclusionAttributeFocused = @"focused"; static NSString* const FBExclusionAttributePlaceholderValue = @"placeholderValue"; static NSString* const FBExclusionAttributeNativeFrame = @"nativeFrame"; @@ -51,6 +52,13 @@ static NSString* const FBExclusionAttributeMinValue = @"minValue"; static NSString* const FBExclusionAttributeMaxValue = @"maxValue"; +static NSString *FBJsonPrefixedAttributeKey(NSString *key) +{ + return [NSString stringWithFormat:@"is%@%@", + [[key substringToIndex:1] uppercaseString], + [key substringFromIndex:1]]; +} + _Nullable id extractIssueProperty(id issue, NSString *propertyName) { SEL selector = NSSelectorFromString(propertyName); NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector]; @@ -223,7 +231,7 @@ + (NSDictionary *)dictionaryForElement:(id)snapshot if ([nonPrefixedKeys containsObject:key]) { info[key] = value; } else { - info[[NSString stringWithFormat:@"is%@", [key capitalizedString]]] = value; + info[FBJsonPrefixedAttributeKey(key)] = value; } } } @@ -268,6 +276,9 @@ + (NSDictionary *)dictionaryForElement:(id)snapshot }, FBExclusionAttributeAccessible: ^{ return [@([wrappedSnapshot isWDAccessible]) stringValue]; + }, + FBExclusionAttributeNativeAccessibilityElement: ^{ + return [@([wrappedSnapshot isWDNativeAccessibilityElement]) stringValue]; }, FBExclusionAttributeFocused: ^{ return [@([wrappedSnapshot isWDFocused]) stringValue]; diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m index a80db0218..361929261 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m @@ -202,6 +202,11 @@ - (BOOL)isWDFocused return self.hasFocus; } +- (BOOL)isWDNativeAccessibilityElement +{ + return self.fb_isAccessibilityElement; +} + - (BOOL)isWDAccessible { XCUIElementType elementType = self.elementType; diff --git a/WebDriverAgentLib/Routing/FBElement.h b/WebDriverAgentLib/Routing/FBElement.h index 2df9c6644..a1fdb30c7 100644 --- a/WebDriverAgentLib/Routing/FBElement.h +++ b/WebDriverAgentLib/Routing/FBElement.h @@ -55,6 +55,9 @@ NS_ASSUME_NONNULL_BEGIN /*! Whether element is accessible */ @property (nonatomic, readonly, getter = isWDAccessible) BOOL wdAccessible; +/*! The raw, native `isAccessibilityElement` value reported by the accessibility framework, without WebDriverAgent's custom computation applied by `wdAccessible` */ +@property (nonatomic, readonly, getter = isWDNativeAccessibilityElement) BOOL wdNativeAccessibilityElement; + /*! Whether element is an accessibility container (contains children of any depth that are accessible) */ @property (nonatomic, readonly, getter = isWDAccessibilityContainer) BOOL wdAccessibilityContainer; diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgentLib/Utilities/FBConfiguration.h index e8c7754bf..2e5fecccd 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.h +++ b/WebDriverAgentLib/Utilities/FBConfiguration.h @@ -366,6 +366,24 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + (void)setIncludeNativeFrameInPageSource:(BOOL)enabled; + (BOOL)includeNativeFrameInPageSource; +/** + * Whether to include the `nativeAccessibilityElement` attribute in the XML page source. + * + * When enabled, the XML representation will contain the raw, native + * `isAccessibilityElement` value as reported by the accessibility framework, + * without the custom computation that WebDriverAgent applies to the + * `accessible` attribute (cell/text field special cases and parent absorption). + * + * This is useful for consumers that need to reason about the unmodified + * accessibility flag alongside the computed `accessible` value. + * + * The value is disabled by default to keep the default page source stable. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeNativeAccessibilityElementInPageSource:(BOOL)enabled; ++ (BOOL)includeNativeAccessibilityElementInPageSource; + /** * Whether to include `minValue`/`maxValue` attributes in the page source. * These attributes are retrieved from native element snapshots and represent diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgentLib/Utilities/FBConfiguration.m index dcd1a62e3..9b76fc3c3 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.m +++ b/WebDriverAgentLib/Utilities/FBConfiguration.m @@ -61,6 +61,7 @@ #endif static BOOL FBShouldIncludeHittableInPageSource = NO; static BOOL FBShouldIncludeNativeFrameInPageSource = NO; +static BOOL FBShouldIncludeNativeAccessibilityElementInPageSource = NO; static BOOL FBShouldIncludeMinMaxValueInPageSource = NO; static BOOL FBShouldIncludeCustomActionsInPageSource = NO; static BOOL FBShouldEnforceCustomSnapshots = NO; @@ -665,6 +666,16 @@ + (BOOL)includeNativeFrameInPageSource return FBShouldIncludeNativeFrameInPageSource; } ++ (void)setIncludeNativeAccessibilityElementInPageSource:(BOOL)enabled +{ + FBShouldIncludeNativeAccessibilityElementInPageSource = enabled; +} + ++ (BOOL)includeNativeAccessibilityElementInPageSource +{ + return FBShouldIncludeNativeAccessibilityElementInPageSource; +} + + (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled { FBShouldIncludeMinMaxValueInPageSource = enabled; diff --git a/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgentLib/Utilities/FBSettings.h index c1fc6e346..c3f1523e2 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.h +++ b/WebDriverAgentLib/Utilities/FBSettings.h @@ -41,6 +41,7 @@ extern NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE; extern NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR; extern NSString *const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE; extern NSString *const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_INCLUDE_NATIVE_ACCESSIBILITY_ELEMENT_IN_PAGE_SOURCE; extern NSString *const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE; extern NSString *const FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE; extern NSString *const FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS; diff --git a/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgentLib/Utilities/FBSettings.m index d333c58f1..b2b219d85 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.m +++ b/WebDriverAgentLib/Utilities/FBSettings.m @@ -37,6 +37,7 @@ NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR = @"autoClickAlertSelector"; NSString* const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE = @"includeHittableInPageSource"; NSString* const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE = @"includeNativeFrameInPageSource"; +NSString* const FB_SETTING_INCLUDE_NATIVE_ACCESSIBILITY_ELEMENT_IN_PAGE_SOURCE = @"includeNativeAccessibilityElementInPageSource"; NSString* const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE = @"includeMinMaxValueInPageSource"; NSString* const FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE = @"includeCustomActionsInPageSource"; NSString* const FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS = @"enforceCustomSnapshots"; diff --git a/WebDriverAgentLib/Utilities/FBSettingsHandler.m b/WebDriverAgentLib/Utilities/FBSettingsHandler.m index c1e17cf3b..641687dae 100644 --- a/WebDriverAgentLib/Utilities/FBSettingsHandler.m +++ b/WebDriverAgentLib/Utilities/FBSettingsHandler.m @@ -162,6 +162,10 @@ @implementation FBSettingsHandler [FBConfiguration setIncludeNativeFrameInPageSource:[value boolValue]]; return nil; }; + map[FB_SETTING_INCLUDE_NATIVE_ACCESSIBILITY_ELEMENT_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setIncludeNativeAccessibilityElementInPageSource:[value boolValue]]; + return nil; + }; map[FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) { [FBConfiguration setIncludeMinMaxValueInPageSource:[value boolValue]]; return nil; @@ -280,6 +284,9 @@ @implementation FBSettingsHandler map[FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] = ^id(FBSession *session) { return @([FBConfiguration includeNativeFrameInPageSource]); }; + map[FB_SETTING_INCLUDE_NATIVE_ACCESSIBILITY_ELEMENT_IN_PAGE_SOURCE] = ^id(FBSession *session) { + return @([FBConfiguration includeNativeAccessibilityElementInPageSource]); + }; map[FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] = ^id(FBSession *session) { return @([FBConfiguration includeMinMaxValueInPageSource]); }; diff --git a/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgentLib/Utilities/FBXPath.m index 18f01a3fc..6e00daf7f 100644 --- a/WebDriverAgentLib/Utilities/FBXPath.m +++ b/WebDriverAgentLib/Utilities/FBXPath.m @@ -120,6 +120,10 @@ @interface FBNativeFrameAttribute : FBElementAttribute @end +@interface FBNativeAccessibilityElementAttribute : FBElementAttribute + +@end + @interface FBTraitsAttribute : FBElementAttribute @end @@ -409,6 +413,10 @@ + (int)xmlRepresentationWithRootElement:(id)root // Include nativeFrame only when requested [includedAttributes removeObject:FBNativeFrameAttribute.class]; } + if (!FBConfiguration.includeNativeAccessibilityElementInPageSource) { + // Include the raw native accessibility flag only when requested + [includedAttributes removeObject:FBNativeAccessibilityElementAttribute.class]; + } if (!FBConfiguration.includeMinMaxValueInPageSource) { // minValue/maxValue are retrieved from private APIs and may be slow on deep trees [includedAttributes removeObject:FBMinValueAttribute.class]; @@ -686,6 +694,7 @@ + (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)v FBEnabledAttribute.class, FBVisibleAttribute.class, FBAccessibleAttribute.class, + FBNativeAccessibilityElementAttribute.class, #if TARGET_OS_TV FBFocusedAttribute.class, #endif @@ -954,6 +963,19 @@ + (NSString *)valueForElement:(id)element } @end +@implementation FBNativeAccessibilityElementAttribute + ++ (NSString *)name +{ + return @"nativeAccessibilityElement"; +} + ++ (NSString *)valueForElement:(id)element +{ + return FBBoolToString(element.wdNativeAccessibilityElement); +} +@end + @implementation FBTraitsAttribute + (NSString *)name diff --git a/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m b/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m index ddc7a2805..1012b6722 100644 --- a/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m @@ -41,6 +41,15 @@ - (void)testElementAccessibilityAttributes XCTAssertFalse(buttonElement.isWDAccessibilityContainer); } +- (void)testNativeAccessibilityElementAttribute +{ + // wdNativeAccessibilityElement must expose the raw, native isAccessibilityElement flag + // without WebDriverAgent's custom computation applied by wdAccessible + XCUIElement *buttonElement = self.testedApplication.buttons[@"Button"]; + XCTAssertTrue(buttonElement.exists); + XCTAssertEqual(buttonElement.wdNativeAccessibilityElement, buttonElement.fb_isAccessibilityElement); +} + - (void)testContainerAccessibilityAttributes { // "not_accessible" isn't accessibility element, but contains accessibility elements, so it is accessibility container diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h index cc57b86e8..1e3797e37 100644 --- a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h +++ b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h @@ -31,6 +31,7 @@ @property (nonatomic, readwrite) NSUInteger wdIndex; @property (nonatomic, readwrite, getter=isWDVisible) BOOL wdVisible; @property (nonatomic, readwrite, getter=isWDAccessible) BOOL wdAccessible; +@property (nonatomic, readwrite, getter=isWDNativeAccessibilityElement) BOOL wdNativeAccessibilityElement; @property (nonatomic, readwrite, getter = isWDFocused) BOOL wdFocused; @property (nonatomic, readwrite, getter = isWDHittable) BOOL wdHittable; @property (nonatomic, copy, readwrite, nullable) NSString *wdPlaceholderValue; diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m index 18f7b1202..e51dfdbb2 100644 --- a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m +++ b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m @@ -28,6 +28,7 @@ - (id)init self.wdCustomActions = nil; self.wdVisible = YES; self.wdAccessible = YES; + self.wdNativeAccessibilityElement = YES; self.wdEnabled = YES; self.wdSelected = YES; self.wdFocused = YES; diff --git a/WebDriverAgentTests/UnitTests/FBXPathTests.m b/WebDriverAgentTests/UnitTests/FBXPathTests.m index c7d545747..2255436aa 100644 --- a/WebDriverAgentTests/UnitTests/FBXPathTests.m +++ b/WebDriverAgentTests/UnitTests/FBXPathTests.m @@ -92,8 +92,8 @@ - (void)testXPathPresentationBasedOnQueryMatchingAllAttributes NSString *resultXml = [self xmlStringWithElement:(id)element xpathQuery:[NSString stringWithFormat:@"//%@[@*]", element.wdType] excludingAttributes:@[@"visible"]]; - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" hittable=\"%@\" traits=\"%@\" nativeFrame=\"%@\" customActions=\"%@\" private_indexPath=\"top\"/>\n", - element.wdType, element.wdType, @"йоло<>&"", element.wdName, @"a b", FBBoolToString(element.wdEnabled), FBBoolToString(element.wdVisible), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex, FBBoolToString(element.wdHittable), element.wdTraits, NSStringFromCGRect(element.wdNativeFrame), element.wdCustomActions]; + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" nativeAccessibilityElement=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" hittable=\"%@\" traits=\"%@\" nativeFrame=\"%@\" customActions=\"%@\" private_indexPath=\"top\"/>\n", + element.wdType, element.wdType, @"йоло<>&"", element.wdName, @"a b", FBBoolToString(element.wdEnabled), FBBoolToString(element.wdVisible), FBBoolToString(element.wdAccessible), FBBoolToString(element.wdNativeAccessibilityElement), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex, FBBoolToString(element.wdHittable), element.wdTraits, NSStringFromCGRect(element.wdNativeFrame), element.wdCustomActions]; XCTAssertEqualObjects(expectedXml, resultXml); } diff --git a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h index 690a6b729..26ab59baf 100644 --- a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h +++ b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h @@ -29,6 +29,7 @@ @property (nonatomic, readwrite) NSUInteger wdIndex; @property (nonatomic, readwrite, getter=isWDVisible) BOOL wdVisible; @property (nonatomic, readwrite, getter=isWDAccessible) BOOL wdAccessible; +@property (nonatomic, readwrite, getter=isWDNativeAccessibilityElement) BOOL wdNativeAccessibilityElement; @property (nonatomic, readwrite, getter=isWDFocused) BOOL wdFocused; @property (nonatomic, readwrite, getter = isWDHittable) BOOL wdHittable; @property (copy, nonnull) NSArray *children; diff --git a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m index cde9629e2..771913adc 100644 --- a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m +++ b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m @@ -24,6 +24,7 @@ - (id)init self.wdValue = @"magicValue"; self.wdVisible = YES; self.wdAccessible = YES; + self.wdNativeAccessibilityElement = YES; self.wdEnabled = YES; self.wdSelected = YES; self.wdHittable = YES; From c82f61ec369ba7c1b6f8b184aee18f77e755d4c9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 9 Jun 2026 03:28:20 +0000 Subject: [PATCH 42/55] chore(release): 13.3.0 [skip ci] ## [13.3.0](https://github.com/appium/WebDriverAgent/compare/v13.2.4...v13.3.0) (2026-06-09) ### Features * Expose native isAccessibilityElement ([#1146](https://github.com/appium/WebDriverAgent/issues/1146)) ([e615621](https://github.com/appium/WebDriverAgent/commit/e6156212e6fba6af98a69a400f5fa18b67f1e3e3)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2754a2351..61c771cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [13.3.0](https://github.com/appium/WebDriverAgent/compare/v13.2.4...v13.3.0) (2026-06-09) + +### Features + +* Expose native isAccessibilityElement ([#1146](https://github.com/appium/WebDriverAgent/issues/1146)) ([e615621](https://github.com/appium/WebDriverAgent/commit/e6156212e6fba6af98a69a400f5fa18b67f1e3e3)) + ## [13.2.4](https://github.com/appium/WebDriverAgent/compare/v13.2.3...v13.2.4) (2026-06-08) ### Bug Fixes diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 8e9dfa5bf..ce66db4e1 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.2.4 + 13.3.0 CFBundleSignature ???? CFBundleVersion - 13.2.4 + 13.3.0 NSPrincipalClass diff --git a/package.json b/package.json index dbb85c4c6..440b31165 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.2.4", + "version": "13.3.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 74498f79c9f00678f2bc37b9afb50f70e30d0f88 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 8 Jun 2026 22:38:38 -0700 Subject: [PATCH 43/55] feat: bump the deployment target to 15 (#1152) BREAKING CHANGE: bump the deployment target to 15 --- WebDriverAgent.xcodeproj/project.pbxproj | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index 4a254ddaa..8ab6829ed 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -52,7 +52,6 @@ 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */; }; 641EE5DA2240C5CA00173FCB /* XCUIApplicationProcessDelay.m in Sources */ = {isa = PBXBuildFile; fileRef = 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */; }; 641EE5DB2240C5CA00173FCB /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; - 71B2E0042733FB970074B004 /* FBXPathExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B2E0022733FB970074B002 /* FBXPathExtensions.m */; }; 641EE5DC2240C5CA00173FCB /* XCUIApplication+FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8FB2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m */; }; 641EE5DE2240C5CA00173FCB /* XCUIApplication+FBTouchAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */; }; 641EE5DF2240C5CA00173FCB /* FBWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB78D1CAEDF0C008C271F /* FBWebServer.m */; }; @@ -317,7 +316,6 @@ 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */; }; 711084441DA3AA7500F913D6 /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 711084421DA3AA7500F913D6 /* FBXPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; - 71B2E0062733FB970074B006 /* FBXPathExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B2E0022733FB970074B002 /* FBXPathExtensions.m */; }; 7119097C2152580600BA3C7E /* XCUIScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 7119097B2152580600BA3C7E /* XCUIScreen.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7119E1EC1E891F8600D0B125 /* FBPickerWheelSelectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */; }; 711CD03425ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */; }; @@ -469,6 +467,8 @@ 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DB230711E900646AFB /* FBCommandStatus.m */; }; 71B155DF23080CA600646AFB /* FBProtocolHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */; }; 71B155E123080CA600646AFB /* FBProtocolHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */; }; + 71B2E0042733FB970074B004 /* FBXPathExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B2E0022733FB970074B002 /* FBXPathExtensions.m */; }; + 71B2E0062733FB970074B006 /* FBXPathExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B2E0022733FB970074B002 /* FBXPathExtensions.m */; }; 71B49EC71ED1A58100D51AD6 /* XCUIElement+FBUID.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */; }; 71B49EC81ED1A58100D51AD6 /* XCUIElement+FBUID.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */; }; 71BB58DE2B9631B700CB9BFE /* FBVideoRecordingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */; }; @@ -535,6 +535,7 @@ 71F5BE50252F14EB00EE9EBA /* FBExceptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */; }; 71F5BE51252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; 71F5BE52252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; + A1B2C3D4E5F600000000001B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F600000000001A /* Assets.xcassets */; }; AABBCCDDEEFF001122334457 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AABBCCDDEEFF001122334456 /* SceneDelegate.m */; }; AD35D06C1CF1C35500870A75 /* WebDriverAgentLib.framework in Copy frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; AD6C26941CF2379700F8B5FF /* FBAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26921CF2379700F8B5FF /* FBAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -832,7 +833,6 @@ F59CD6D52EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */ = {isa = PBXBuildFile; fileRef = F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */; }; F59CD6D62EF16E5E00F91287 /* XCUIElement+FBCustomActions.h in Headers */ = {isa = PBXBuildFile; fileRef = F59CD6D22EF16E5E00F91287 /* XCUIElement+FBCustomActions.h */; }; F59CD6D72EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */ = {isa = PBXBuildFile; fileRef = F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */; }; - A1B2C3D4E5F600000000001B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F600000000001A /* Assets.xcassets */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -982,8 +982,6 @@ 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FBTVNavigationTracker-Private.h"; sourceTree = ""; }; 711084421DA3AA7500F913D6 /* FBXPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBXPath.h; sourceTree = ""; }; 711084431DA3AA7500F913D6 /* FBXPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPath.m; sourceTree = ""; }; - 71B2E0012733FB970074B001 /* FBXPathExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXPathExtensions.h; sourceTree = ""; }; - 71B2E0022733FB970074B002 /* FBXPathExtensions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXPathExtensions.m; sourceTree = ""; }; 7119097B2152580600BA3C7E /* XCUIScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIScreen.h; sourceTree = ""; }; 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBPickerWheelSelectTests.m; sourceTree = ""; }; 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIScreenDataSource-Protocol.h"; sourceTree = ""; }; @@ -1090,6 +1088,8 @@ 71B155DB230711E900646AFB /* FBCommandStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBCommandStatus.m; sourceTree = ""; }; 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBProtocolHelpers.h; sourceTree = ""; }; 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBProtocolHelpers.m; sourceTree = ""; }; + 71B2E0012733FB970074B001 /* FBXPathExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXPathExtensions.h; sourceTree = ""; }; + 71B2E0022733FB970074B002 /* FBXPathExtensions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXPathExtensions.m; sourceTree = ""; }; 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBUID.h"; sourceTree = ""; }; 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBUID.m"; sourceTree = ""; }; 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBVideoRecordingTests.m; sourceTree = ""; }; @@ -1125,6 +1125,7 @@ 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementSwipingTests.m; sourceTree = ""; }; 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBExceptions.h; sourceTree = ""; }; 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBExceptions.m; sourceTree = ""; }; + A1B2C3D4E5F600000000001A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = WebDriverAgentRunner/Assets.xcassets; sourceTree = SOURCE_ROOT; }; AABBCCDDEEFF001122334455 /* SceneDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; AABBCCDDEEFF001122334456 /* SceneDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; AD42DD2A1CF121E600806E5D /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; @@ -1379,7 +1380,6 @@ EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRuntimeUtils.m; sourceTree = ""; }; EE9AB7FC1CAEE048008C271F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = WebDriverAgentRunner/Info.plist; sourceTree = SOURCE_ROOT; }; EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UITestingUITests.m; path = WebDriverAgentRunner/UITestingUITests.m; sourceTree = SOURCE_ROOT; }; - A1B2C3D4E5F600000000001A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = WebDriverAgentRunner/Assets.xcassets; sourceTree = SOURCE_ROOT; }; EE9AB8031CAEE182008C271F /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; EE9B75D41CF7956C00275851 /* IntegrationApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; EE9B75EC1CF7956C00275851 /* IntegrationTests_1.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_1.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2012,11 +2012,11 @@ 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */, 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */, 712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */, - 711084421DA3AA7500F913D6 /* FBXPath.h */, - 711084431DA3AA7500F913D6 /* FBXPath.m */, - 71B2E0012733FB970074B001 /* FBXPathExtensions.h */, - 71B2E0022733FB970074B002 /* FBXPathExtensions.m */, - EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */, + 711084421DA3AA7500F913D6 /* FBXPath.h */, + 711084431DA3AA7500F913D6 /* FBXPath.m */, + 71B2E0012733FB970074B001 /* FBXPathExtensions.h */, + 71B2E0022733FB970074B002 /* FBXPathExtensions.m */, + EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */, EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */, 633E904A220DEE7F007CADF9 /* XCUIApplicationProcessDelay.h */, 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */, @@ -3976,13 +3976,13 @@ "$(SDKROOT)/usr/include/libxml2", "$(SRCROOT)/Modules", ); - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 15.0; VALIDATE_WORKSPACE = NO; }; name = Debug; @@ -4039,12 +4039,12 @@ "$(SDKROOT)/usr/include/libxml2", "$(SRCROOT)/Modules", ); - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 15.0; VALIDATE_PRODUCT = YES; VALIDATE_WORKSPACE = NO; }; From c7fac44dc4bb10901f1d3e2a7d9f1a85a28a8d86 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 9 Jun 2026 05:42:37 +0000 Subject: [PATCH 44/55] chore(release): 14.0.0 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [14.0.0](https://github.com/appium/WebDriverAgent/compare/v13.3.0...v14.0.0) (2026-06-09) ### ⚠ BREAKING CHANGES * bump the deployment target to 15 ### Features * bump the deployment target to 15 ([#1152](https://github.com/appium/WebDriverAgent/issues/1152)) ([74498f7](https://github.com/appium/WebDriverAgent/commit/74498f79c9f00678f2bc37b9afb50f70e30d0f88)) --- CHANGELOG.md | 10 ++++++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c771cf2..293623bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [14.0.0](https://github.com/appium/WebDriverAgent/compare/v13.3.0...v14.0.0) (2026-06-09) + +### ⚠ BREAKING CHANGES + +* bump the deployment target to 15 + +### Features + +* bump the deployment target to 15 ([#1152](https://github.com/appium/WebDriverAgent/issues/1152)) ([74498f7](https://github.com/appium/WebDriverAgent/commit/74498f79c9f00678f2bc37b9afb50f70e30d0f88)) + ## [13.3.0](https://github.com/appium/WebDriverAgent/compare/v13.2.4...v13.3.0) (2026-06-09) ### Features diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index ce66db4e1..290fe665a 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 13.3.0 + 14.0.0 CFBundleSignature ???? CFBundleVersion - 13.3.0 + 14.0.0 NSPrincipalClass diff --git a/package.json b/package.json index 440b31165..d1b8a8a48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "13.3.0", + "version": "14.0.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 6b3631aed7a95439b01a9a3bb87189df384dcf06 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 10 Jun 2026 08:27:47 +0200 Subject: [PATCH 45/55] feat: Add VoiceOver automation support (#1153) --- WebDriverAgent.xcodeproj/project.pbxproj | 20 +- .../Categories/XCUIDevice+FBVoiceOver.h | 66 ++++++ .../Categories/XCUIDevice+FBVoiceOver.m | 216 ++++++++++++++++++ WebDriverAgentLib/Commands/FBCustomCommands.m | 80 +++++++ WebDriverAgentLib/WebDriverAgentLib.h | 1 + .../IntegrationTests/FBVoiceOverTests.m | 103 +++++++++ 6 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.h create mode 100644 WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.m create mode 100644 WebDriverAgentTests/IntegrationTests/FBVoiceOverTests.m diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index 8ab6829ed..3c0a06d69 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -535,6 +535,11 @@ 71F5BE50252F14EB00EE9EBA /* FBExceptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */; }; 71F5BE51252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; 71F5BE52252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; + A1B2C3D41F001A00A1B0004 /* XCUIDevice+FBVoiceOver.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A1B2C3D41F001A00A1B0005 /* XCUIDevice+FBVoiceOver.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0002 /* XCUIDevice+FBVoiceOver.m */; }; + A1B2C3D41F001A00A1B0006 /* XCUIDevice+FBVoiceOver.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A1B2C3D41F001A00A1B0007 /* XCUIDevice+FBVoiceOver.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0002 /* XCUIDevice+FBVoiceOver.m */; }; + A1B2C3D41F001A00A1B0008 /* FBVoiceOverTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0003 /* FBVoiceOverTests.m */; }; A1B2C3D4E5F600000000001B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F600000000001A /* Assets.xcassets */; }; AABBCCDDEEFF001122334457 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AABBCCDDEEFF001122334456 /* SceneDelegate.m */; }; AD35D06C1CF1C35500870A75 /* WebDriverAgentLib.framework in Copy frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1125,6 +1130,9 @@ 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementSwipingTests.m; sourceTree = ""; }; 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBExceptions.h; sourceTree = ""; }; 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBExceptions.m; sourceTree = ""; }; + A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIDevice+FBVoiceOver.h"; sourceTree = ""; }; + A1B2C3D41F001A00A1B0002 /* XCUIDevice+FBVoiceOver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIDevice+FBVoiceOver.m"; sourceTree = ""; }; + A1B2C3D41F001A00A1B0003 /* FBVoiceOverTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBVoiceOverTests.m; sourceTree = ""; }; A1B2C3D4E5F600000000001A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = WebDriverAgentRunner/Assets.xcassets; sourceTree = SOURCE_ROOT; }; AABBCCDDEEFF001122334455 /* SceneDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; AABBCCDDEEFF001122334456 /* SceneDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; @@ -1798,6 +1806,8 @@ EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */, AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */, AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */, + A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */, + A1B2C3D41F001A00A1B0002 /* XCUIDevice+FBVoiceOver.m */, EEE3763D1D59F81400ED88DD /* XCUIDevice+FBRotation.h */, EEE3763E1D59F81400ED88DD /* XCUIDevice+FBRotation.m */, EE9AB7451CAEDF0C008C271F /* XCUIElement+FBAccessibility.h */, @@ -2070,6 +2080,7 @@ AD76723F1D6B826F00610457 /* FBTypingTest.m */, 714CA3C61DC23186000F12C9 /* FBXPathIntegrationTests.m */, 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */, + A1B2C3D41F001A00A1B0003 /* FBVoiceOverTests.m */, 71241D7D1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m */, 71241D7F1FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m */, 7136C0F8243A182400921C76 /* FBW3CTypeActionsTests.m */, @@ -2513,6 +2524,7 @@ 71822777258744CE00661B83 /* DDNumber.h in Headers */, 641EE6C82240C5CA00173FCB /* XCTKVOExpectation.h in Headers */, 641EE6C92240C5CA00173FCB /* XCUIDevice+FBRotation.h in Headers */, + A1B2C3D41F001A00A1B0004 /* XCUIDevice+FBVoiceOver.h in Headers */, 641EE6CA2240C5CA00173FCB /* XCEventGenerator.h in Headers */, 719DCF162601EAFB000E765F /* FBNotificationsHelper.h in Headers */, 71414ED52670A1EE003A8C5D /* LRUCache.h in Headers */, @@ -2760,6 +2772,7 @@ EE35AD591E3B77D600A02D78 /* XCTKVOExpectation.h in Headers */, 13DE7A43287C2A8D003243C6 /* FBXCAccessibilityElement.h in Headers */, EEE376431D59F81400ED88DD /* XCUIDevice+FBRotation.h in Headers */, + A1B2C3D41F001A00A1B0006 /* XCUIDevice+FBVoiceOver.h in Headers */, EE35AD2E1E3B77D600A02D78 /* XCEventGenerator.h in Headers */, EE9B76A61CF7A43900275851 /* FBConfiguration.h in Headers */, EE35AD571E3B77D600A02D78 /* XCTestSuiteRun.h in Headers */, @@ -3206,6 +3219,7 @@ 641EE5F22240C5CA00173FCB /* NSPredicate+FBFormat.m in Sources */, 718F49CA23087AD30045FE8B /* FBProtocolHelpers.m in Sources */, 641EE5F42240C5CA00173FCB /* XCUIDevice+FBRotation.m in Sources */, + A1B2C3D41F001A00A1B0005 /* XCUIDevice+FBVoiceOver.m in Sources */, 13815F722328D20400CDAB61 /* FBActiveAppDetectionPoint.m in Sources */, 71D475C52538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m in Sources */, 641EE5F52240C5CA00173FCB /* XCUIElement+FBUID.m in Sources */, @@ -3333,6 +3347,7 @@ 71A224E61DE2F56600844D55 /* NSPredicate+FBFormat.m in Sources */, E444DC85249131B10060D7EB /* DDNumber.m in Sources */, EEE376441D59F81400ED88DD /* XCUIDevice+FBRotation.m in Sources */, + A1B2C3D41F001A00A1B0007 /* XCUIDevice+FBVoiceOver.m in Sources */, 13815F712328D20400CDAB61 /* FBActiveAppDetectionPoint.m in Sources */, 71B49EC81ED1A58100D51AD6 /* XCUIElement+FBUID.m in Sources */, EE158AE21CBD456F00A3E3F0 /* FBRouteRequest.m in Sources */, @@ -3422,6 +3437,7 @@ files = ( 71241D801FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m in Sources */, 71BB58DE2B9631B700CB9BFE /* FBVideoRecordingTests.m in Sources */, + A1B2C3D41F001A00A1B0008 /* FBVoiceOverTests.m in Sources */, 71241D7E1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m in Sources */, 63FD950221F9D06100A3E356 /* FBImageProcessorTests.m in Sources */, 719CD8FF2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m in Sources */, @@ -4300,7 +4316,7 @@ CLANG_ANALYZER_NONNULL = YES; DEBUG_INFORMATION_FORMAT = dwarf; INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4317,7 +4333,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.h b/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.h new file mode 100644 index 000000000..a44b041b7 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.h @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIDevice (FBVoiceOver) + +/** + Whether VoiceOver control APIs are available in the current Xcode SDK. + + @return YES if the VoiceOver service is exposed by XCUIDevice + */ +- (BOOL)fb_isVoiceOverServiceAvailable; + +/** + Enable VoiceOver. Only works since Xcode 27/iOS 27. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if VoiceOver has been successfully enabled + */ +- (BOOL)fb_enableVoiceOver:(NSError **)error; + +/** + Disable VoiceOver. Only works since Xcode 27/iOS 27. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if VoiceOver has been successfully disabled + */ +- (BOOL)fb_disableVoiceOver:(NSError **)error; + +/** + Whether VoiceOver is currently enabled. Only works since Xcode 27/iOS 27. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if VoiceOver is enabled + */ +- (BOOL)fb_isVoiceOverEnabled:(NSError **)error; + +/** + Move VoiceOver focus and return speech for the newly focused element. + Only works since Xcode 27/iOS 27. + + @param direction One of: forward, backward, in (iOS only), out (iOS only) + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return The spoken utterance or nil in case of failure + */ +- (nullable NSString *)fb_voiceOverMove:(NSString *)direction error:(NSError **)error; + +/** + Return the speech for the currently focused element. Only works since Xcode 27/iOS 27. + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return The spoken utterance or nil in case of failure + */ +- (nullable NSString *)fb_voiceOverCurrentSpeech:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.m b/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.m new file mode 100644 index 000000000..4d1d51718 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.m @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "XCUIDevice+FBVoiceOver.h" + +#import "FBErrorBuilder.h" + +static NSString *const FBVoiceOverSDKUnsupportedError = +@"The current Xcode SDK does not support VoiceOver control. Consider upgrading to Xcode 27+/iOS 27+"; + +static BOOL FBVoiceOverBuildSDKUnsupportedError(NSError **error) +{ + return [[[FBErrorBuilder builder] + withDescription:FBVoiceOverSDKUnsupportedError] + buildError:error]; +} + +static BOOL FBIsVoiceOverServiceAvailable(void) +{ + static BOOL isAvailable = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + isAvailable = [XCUIDevice.sharedDevice respondsToSelector:NSSelectorFromString(@"voiceOverService")]; + }); + return isAvailable; +} + +static id FBVoiceOverService(NSError **error) +{ + if (!FBIsVoiceOverServiceAvailable()) { + FBVoiceOverBuildSDKUnsupportedError(error); + return nil; + } + return [XCUIDevice.sharedDevice valueForKey:@"voiceOverService"]; +} + +static BOOL FBInvokeVoiceOverBoolMethod(id voiceOverService, + SEL selector, + NSError **error) +{ + if (![voiceOverService respondsToSelector:selector]) { + return FBVoiceOverBuildSDKUnsupportedError(error); + } + + NSMethodSignature *signature = [voiceOverService methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation setTarget:voiceOverService]; + NSError *invokeError = nil; + [invocation setArgument:&invokeError atIndex:2]; + [invocation invoke]; + if (nil != invokeError) { + if (error) { + *error = invokeError; + } + return NO; + } + + BOOL result = NO; + [invocation getReturnValue:&result]; + return result; +} + +static id FBInvokeVoiceOverOutputMethod(id voiceOverService, + SEL selector, + NSError **error) +{ + if (![voiceOverService respondsToSelector:selector]) { + FBVoiceOverBuildSDKUnsupportedError(error); + return nil; + } + + NSMethodSignature *signature = [voiceOverService methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation setTarget:voiceOverService]; + NSError *invokeError = nil; + [invocation setArgument:&invokeError atIndex:2]; + [invocation invoke]; + if (nil != invokeError) { + if (error) { + *error = invokeError; + } + return nil; + } + + id __unsafe_unretained output = nil; + [invocation getReturnValue:&output]; + return output; +} + +static NSString *FBUtteranceFromVoiceOverOutput(id output, NSError **error) +{ + if (nil == output) { + return nil; + } + + if (![output respondsToSelector:NSSelectorFromString(@"utterance")]) { + [[[FBErrorBuilder builder] + withDescription:@"VoiceOver output does not provide an utterance"] + buildError:error]; + return nil; + } + + id utterance = [output valueForKey:@"utterance"]; + return [utterance isKindOfClass:NSString.class] ? utterance : nil; +} + +static NSString *FBVoiceOverSpeechFromSelector(SEL selector, NSError **error) +{ + id service = FBVoiceOverService(error); + if (nil == service) { + return nil; + } + + id output = FBInvokeVoiceOverOutputMethod(service, selector, error); + if (nil != error && nil != *error) { + return nil; + } + return FBUtteranceFromVoiceOverOutput(output, error); +} + +static NSDictionary *FBVoiceOverMoveSelectors(void) +{ + static NSDictionary *selectors = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *mapping = [@{ + @"forward": @"moveForwardAndReturnError:", + @"backward": @"moveBackwardAndReturnError:", + } mutableCopy]; +#if TARGET_OS_IOS + mapping[@"in"] = @"moveInAndReturnError:"; + mapping[@"out"] = @"moveOutAndReturnError:"; +#endif + selectors = mapping.copy; + }); + return selectors; +} + +@implementation XCUIDevice (FBVoiceOver) + +- (BOOL)fb_isVoiceOverServiceAvailable +{ + return FBIsVoiceOverServiceAvailable(); +} + +- (BOOL)fb_enableVoiceOver:(NSError **)error +{ + id service = FBVoiceOverService(error); + if (nil == service) { + return NO; + } + return FBInvokeVoiceOverBoolMethod(service, + NSSelectorFromString(@"enableAndReturnError:"), + error); +} + +- (BOOL)fb_disableVoiceOver:(NSError **)error +{ + id service = FBVoiceOverService(error); + if (nil == service) { + return NO; + } + return FBInvokeVoiceOverBoolMethod(service, + NSSelectorFromString(@"disableAndReturnError:"), + error); +} + +- (BOOL)fb_isVoiceOverEnabled:(NSError **)error +{ + id service = FBVoiceOverService(error); + if (nil == service) { + return NO; + } + + if (![service respondsToSelector:NSSelectorFromString(@"isEnabled")] && + ![service respondsToSelector:NSSelectorFromString(@"enabled")]) { + return FBVoiceOverBuildSDKUnsupportedError(error); + } + + return [[service valueForKey:@"enabled"] boolValue]; +} + +- (nullable NSString *)fb_voiceOverMove:(NSString *)direction error:(NSError **)error +{ + if (![direction isKindOfClass:NSString.class] || 0 == direction.length) { + return [[[FBErrorBuilder builder] + withDescription:@"VoiceOver move direction must be a non-empty string"] + buildError:error], nil; + } + + NSString *normalizedDirection = direction.lowercaseString; + NSString *selectorName = FBVoiceOverMoveSelectors()[normalizedDirection]; + if (nil == selectorName) { + NSArray *supportedDirections = [FBVoiceOverMoveSelectors().allKeys sortedArrayUsingSelector:@selector(compare:)]; + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Unsupported VoiceOver move direction '%@'. Supported directions: %@", + direction, supportedDirections] + buildError:error], nil; + } + + return FBVoiceOverSpeechFromSelector(NSSelectorFromString(selectorName), error); +} + +- (nullable NSString *)fb_voiceOverCurrentSpeech:(NSError **)error +{ + return FBVoiceOverSpeechFromSelector(NSSelectorFromString(@"currentSpeechAndReturnError:"), error); +} + +@end diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgentLib/Commands/FBCustomCommands.m index b1490d921..4c4803ff3 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.m +++ b/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -26,6 +26,7 @@ #import "XCUIApplication.h" #import "XCUIApplication+FBHelpers.h" #import "XCUIDevice+FBHelpers.h" +#import "XCUIDevice+FBVoiceOver.h" #import "XCUIElement.h" #import "XCUIElement+FBIsVisible.h" #import "XCUIElementQuery.h" @@ -81,6 +82,16 @@ + (NSArray *)routes [[FBRoute DELETE:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleClearSimulatedLocation:)], [[FBRoute DELETE:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleClearSimulatedLocation:)], #endif + [[FBRoute POST:@"/wda/voiceOver/enable"] respondWithTarget:self action:@selector(handleVoiceOverEnable:)], + [[FBRoute POST:@"/wda/voiceOver/enable"].withoutSession respondWithTarget:self action:@selector(handleVoiceOverEnable:)], + [[FBRoute POST:@"/wda/voiceOver/disable"] respondWithTarget:self action:@selector(handleVoiceOverDisable:)], + [[FBRoute POST:@"/wda/voiceOver/disable"].withoutSession respondWithTarget:self action:@selector(handleVoiceOverDisable:)], + [[FBRoute GET:@"/wda/voiceOver/enabled"] respondWithTarget:self action:@selector(handleVoiceOverEnabled:)], + [[FBRoute GET:@"/wda/voiceOver/enabled"].withoutSession respondWithTarget:self action:@selector(handleVoiceOverEnabled:)], + [[FBRoute POST:@"/wda/voiceOver/move"] respondWithTarget:self action:@selector(handleVoiceOverMove:)], + [[FBRoute POST:@"/wda/voiceOver/move"].withoutSession respondWithTarget:self action:@selector(handleVoiceOverMove:)], + [[FBRoute GET:@"/wda/voiceOver/currentSpeech"] respondWithTarget:self action:@selector(handleVoiceOverCurrentSpeech:)], + [[FBRoute GET:@"/wda/voiceOver/currentSpeech"].withoutSession respondWithTarget:self action:@selector(handleVoiceOverCurrentSpeech:)], [[FBRoute OPTIONS:@"/*"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)], ]; } @@ -611,6 +622,75 @@ + (NSString *)timeZone #endif #endif ++ (id)fb_handleVoiceOverSpeechResponse:(nullable NSString *)utterance + error:(NSError *)error +{ + if (nil != error) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithObject(@{ + @"utterance": utterance ?: NSNull.null, + }); +} + ++ (id)handleVoiceOverEnable:(FBRouteRequest *)request +{ + NSError *error; + if (![XCUIDevice.sharedDevice fb_enableVoiceOver:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleVoiceOverDisable:(FBRouteRequest *)request +{ + NSError *error; + if (![XCUIDevice.sharedDevice fb_disableVoiceOver:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleVoiceOverEnabled:(FBRouteRequest *)request +{ + NSError *error; + BOOL isEnabled = [XCUIDevice.sharedDevice fb_isVoiceOverEnabled:&error]; + if (nil != error) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithObject(@{@"enabled": @(isEnabled)}); +} + ++ (id)handleVoiceOverMove:(FBRouteRequest *)request +{ + NSString *direction = request.arguments[@"direction"]; + if (nil == direction) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"The 'direction' argument must be provided" + traceback:nil]); + } + + NSError *error; + NSString *utterance = [XCUIDevice.sharedDevice fb_voiceOverMove:direction error:&error]; + if (nil != error) { + FBCommandStatus *status = [error.localizedDescription containsString:@"Unsupported VoiceOver move direction"] + ? [FBCommandStatus invalidArgumentErrorWithMessage:error.description traceback:nil] + : [FBCommandStatus unknownErrorWithMessage:error.description traceback:nil]; + return FBResponseWithStatus(status); + } + return [self fb_handleVoiceOverSpeechResponse:utterance error:nil]; +} + ++ (id)handleVoiceOverCurrentSpeech:(FBRouteRequest *)request +{ + NSError *error; + NSString *utterance = [XCUIDevice.sharedDevice fb_voiceOverCurrentSpeech:&error]; + return [self fb_handleVoiceOverSpeechResponse:utterance error:error]; +} + + (id)handlePerformAccessibilityAudit:(FBRouteRequest *)request { NSError *error; diff --git a/WebDriverAgentLib/WebDriverAgentLib.h b/WebDriverAgentLib/WebDriverAgentLib.h index d0e3f7391..de916ee2e 100644 --- a/WebDriverAgentLib/WebDriverAgentLib.h +++ b/WebDriverAgentLib/WebDriverAgentLib.h @@ -48,6 +48,7 @@ FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; #import #import #import +#import #import #import #import diff --git a/WebDriverAgentTests/IntegrationTests/FBVoiceOverTests.m b/WebDriverAgentTests/IntegrationTests/FBVoiceOverTests.m new file mode 100644 index 000000000..ce747b501 --- /dev/null +++ b/WebDriverAgentTests/IntegrationTests/FBVoiceOverTests.m @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBIntegrationTestCase.h" +#import "FBMacros.h" +#import "FBTestMacros.h" +#import "XCUIDevice+FBVoiceOver.h" + +@interface FBVoiceOverTests : FBIntegrationTestCase +@end + +@implementation FBVoiceOverTests + +- (void)tearDown +{ + if ([XCUIDevice.sharedDevice fb_isVoiceOverServiceAvailable]) { + NSError *error = nil; + if ([XCUIDevice.sharedDevice fb_isVoiceOverEnabled:&error] && nil == error) { + [XCUIDevice.sharedDevice fb_disableVoiceOver:&error]; + } + } + [super tearDown]; +} + +- (void)testVoiceOverUnavailableOnOlderSDK +{ + if ([XCUIDevice.sharedDevice fb_isVoiceOverServiceAvailable]) { + return; + } + + NSError *error = nil; + XCTAssertFalse([XCUIDevice.sharedDevice fb_enableVoiceOver:&error]); + XCTAssertNotNil(error); + XCTAssertTrue([error.localizedDescription containsString:@"Xcode 27"]); +} + +- (void)testVoiceOverEnableDisableAndNavigation +{ + if (SYSTEM_VERSION_LESS_THAN(@"27.0")) { + return; + } + if (![XCUIDevice.sharedDevice fb_isVoiceOverServiceAvailable]) { + return; + } + + [self launchApplication]; + + NSError *error = nil; + XCTAssertTrue([XCUIDevice.sharedDevice fb_enableVoiceOver:&error]); + XCTAssertNil(error); + XCTAssertTrue([XCUIDevice.sharedDevice fb_isVoiceOverEnabled:&error]); + XCTAssertNil(error); + + NSString *utterance = [XCUIDevice.sharedDevice fb_voiceOverMove:@"forward" error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(utterance); + XCTAssertTrue(utterance.length > 0); + + NSString *currentSpeech = [XCUIDevice.sharedDevice fb_voiceOverCurrentSpeech:&error]; + XCTAssertNil(error); + XCTAssertNotNil(currentSpeech); + XCTAssertEqualObjects(currentSpeech, utterance); + + XCTAssertTrue([XCUIDevice.sharedDevice fb_disableVoiceOver:&error]); + XCTAssertNil(error); + XCTAssertFalse([XCUIDevice.sharedDevice fb_isVoiceOverEnabled:&error]); + XCTAssertNil(error); +} + +#if TARGET_OS_IOS +- (void)testVoiceOverMoveBackward +{ + if (SYSTEM_VERSION_LESS_THAN(@"27.0")) { + return; + } + if (![XCUIDevice.sharedDevice fb_isVoiceOverServiceAvailable]) { + return; + } + + [self launchApplication]; + + NSError *error = nil; + XCTAssertTrue([XCUIDevice.sharedDevice fb_enableVoiceOver:&error]); + XCTAssertNil(error); + + XCTAssertNotNil([XCUIDevice.sharedDevice fb_voiceOverMove:@"forward" error:&error]); + XCTAssertNil(error); + + NSString *utterance = [XCUIDevice.sharedDevice fb_voiceOverMove:@"backward" error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(utterance); + XCTAssertTrue(utterance.length > 0); +} +#endif + +@end From 3474dca334afbd16c44b3b49cb89334c7c69a1cf Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 10 Jun 2026 07:16:42 +0000 Subject: [PATCH 46/55] chore(release): 14.1.0 [skip ci] ## [14.1.0](https://github.com/appium/WebDriverAgent/compare/v14.0.0...v14.1.0) (2026-06-10) ### Features * Add VoiceOver automation support ([#1153](https://github.com/appium/WebDriverAgent/issues/1153)) ([6b3631a](https://github.com/appium/WebDriverAgent/commit/6b3631aed7a95439b01a9a3bb87189df384dcf06)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 293623bf0..2d45ae1f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [14.1.0](https://github.com/appium/WebDriverAgent/compare/v14.0.0...v14.1.0) (2026-06-10) + +### Features + +* Add VoiceOver automation support ([#1153](https://github.com/appium/WebDriverAgent/issues/1153)) ([6b3631a](https://github.com/appium/WebDriverAgent/commit/6b3631aed7a95439b01a9a3bb87189df384dcf06)) + ## [14.0.0](https://github.com/appium/WebDriverAgent/compare/v13.3.0...v14.0.0) (2026-06-09) ### ⚠ BREAKING CHANGES diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 290fe665a..684281c32 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 14.0.0 + 14.1.0 CFBundleSignature ???? CFBundleVersion - 14.0.0 + 14.1.0 NSPrincipalClass diff --git a/package.json b/package.json index d1b8a8a48..cbea980b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "14.0.0", + "version": "14.1.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 6618b0b6ccd06c69fc9e4a0947ef0c88c89b1e48 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 10 Jun 2026 08:30:40 -0700 Subject: [PATCH 47/55] chore: address runtime version rather than Xcode for selector based methods (#1154) * chore: address runtime version rather than Xcode for selector based methods * revert unnecessary change --- .../Categories/XCUIDevice+FBHelpers.h | 6 +++--- .../Categories/XCUIDevice+FBHelpers.m | 2 +- .../Categories/XCUIDevice+FBVoiceOver.h | 12 ++++++------ .../Categories/XCUIDevice+FBVoiceOver.m | 2 +- WebDriverAgentLib/Utilities/FBCapabilities.h | 2 +- WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m | 14 +++++++------- .../IntegrationTests/FBVoiceOverTests.m | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h index db03b7a1f..cbe308543 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h @@ -169,7 +169,7 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { #if !TARGET_OS_TV /** Allows to set a simulated geolocation coordinates. - Only works since Xcode 14.3/iOS 16.4 + Only works since iOS 16.4 runtime @param location The simlated location coordinates to set @param error If there is an error, upon return contains an NSError object that describes the problem. @@ -179,7 +179,7 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { /** Allows to get a simulated geolocation coordinates. - Only works since Xcode 14.3/iOS 16.4 + Only works since iOS 16.4 runtime @param error If there is an error, upon return contains an NSError object that describes the problem. @return The current simulated location or nil in case of failure or if no location has previously been seet @@ -189,7 +189,7 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { /** Allows to clear a previosuly set simulated geolocation coordinates. - Only works since Xcode 14.3/iOS 16.4 + Only works since iOS 16.4 runtime @param error If there is an error, upon return contains an NSError object that describes the problem. @return YES if the simulated location has been successfully cleared diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index bcde01972..0b5d0f0b1 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -229,7 +229,7 @@ - (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error return [self fb_activateSiriVoiceRecognitionWithText:[NSString stringWithFormat:@"Open {%@}", url] error:error]; } - NSString *description = [NSString stringWithFormat:@"Cannot open '%@' with the default application assigned for it. Consider upgrading to Xcode 14.3+/iOS 16.4+", url]; + NSString *description = [NSString stringWithFormat:@"Cannot open '%@' with the default application assigned for it. This API requires an iOS 16.4+ runtime", url]; return [[[FBErrorBuilder builder] withDescriptionFormat:@"%@", description] buildError:error]; diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.h b/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.h index a44b041b7..91ad3e688 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.h +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.h @@ -13,14 +13,14 @@ NS_ASSUME_NONNULL_BEGIN @interface XCUIDevice (FBVoiceOver) /** - Whether VoiceOver control APIs are available in the current Xcode SDK. + Whether VoiceOver control APIs are available in the current OS runtime. @return YES if the VoiceOver service is exposed by XCUIDevice */ - (BOOL)fb_isVoiceOverServiceAvailable; /** - Enable VoiceOver. Only works since Xcode 27/iOS 27. + Enable VoiceOver. Only works since iOS 27 runtime. @param error If there is an error, upon return contains an NSError object that describes the problem. @return YES if VoiceOver has been successfully enabled @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)fb_enableVoiceOver:(NSError **)error; /** - Disable VoiceOver. Only works since Xcode 27/iOS 27. + Disable VoiceOver. Only works since iOS 27 runtime. @param error If there is an error, upon return contains an NSError object that describes the problem. @return YES if VoiceOver has been successfully disabled @@ -36,7 +36,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)fb_disableVoiceOver:(NSError **)error; /** - Whether VoiceOver is currently enabled. Only works since Xcode 27/iOS 27. + Whether VoiceOver is currently enabled. Only works since iOS 27 runtime. @param error If there is an error, upon return contains an NSError object that describes the problem. @return YES if VoiceOver is enabled @@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN /** Move VoiceOver focus and return speech for the newly focused element. - Only works since Xcode 27/iOS 27. + Only works since iOS 27 runtime. @param direction One of: forward, backward, in (iOS only), out (iOS only) @param error If there is an error, upon return contains an NSError object that describes the problem. @@ -54,7 +54,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSString *)fb_voiceOverMove:(NSString *)direction error:(NSError **)error; /** - Return the speech for the currently focused element. Only works since Xcode 27/iOS 27. + Return the speech for the currently focused element. Only works since iOS 27 runtime. @param error If there is an error, upon return contains an NSError object that describes the problem. @return The spoken utterance or nil in case of failure diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.m b/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.m index 4d1d51718..6e054b1f7 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBVoiceOver.m @@ -11,7 +11,7 @@ #import "FBErrorBuilder.h" static NSString *const FBVoiceOverSDKUnsupportedError = -@"The current Xcode SDK does not support VoiceOver control. Consider upgrading to Xcode 27+/iOS 27+"; +@"The current OS runtime does not support VoiceOver control. This API requires an iOS 27+ runtime"; static BOOL FBVoiceOverBuildSDKUnsupportedError(NSError **error) { diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.h b/WebDriverAgentLib/Utilities/FBCapabilities.h index 649a227ce..6e7d7e35c 100644 --- a/WebDriverAgentLib/Utilities/FBCapabilities.h +++ b/WebDriverAgentLib/Utilities/FBCapabilities.h @@ -24,7 +24,7 @@ extern NSString* const FB_CAP_BUNDLE_ID; Usually an URL used as initial link to run Mobile Safari, but could be any other deep link. This might also work together with `FB_CAP_BUNLDE_ID`, which tells XCTest to open the given deep link in the particular app. - Only works since iOS 16.4 + Only works since iOS 16.4 runtime */ extern NSString* const FB_CAP_INITIAL_URL; /** Whether to enforrce (re)start of the application under test on session startup */ diff --git a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m index e29b94e99..e1ced2be1 100644 --- a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +++ b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m @@ -134,7 +134,7 @@ + (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSErro XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) { return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"] + withDescriptionFormat:@"The current OS runtime does not support opening URLs with a given application"] buildError:error]; } @@ -161,7 +161,7 @@ + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasin XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) { return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + withDescriptionFormat:@"The current OS runtime does not support opening URLs. This API requires an iOS 16.4+ runtime"] buildError:error]; } @@ -189,7 +189,7 @@ + (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleas XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) { return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + withDescriptionFormat:@"The current OS runtime does not support location simulation. This API requires an iOS 16.4+ runtime"] buildError:error]; } if (![session supportsLocationSimulation]) { @@ -221,7 +221,7 @@ + (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error; XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) { [[[FBErrorBuilder builder] - withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + withDescriptionFormat:@"The current OS runtime does not support location simulation. This API requires an iOS 16.4+ runtime"] buildError:error]; return nil; } @@ -255,7 +255,7 @@ + (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) { return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + withDescriptionFormat:@"The current OS runtime does not support location simulation. This API requires an iOS 16.4+ runtime"] buildError:error]; } if (![session supportsLocationSimulation]) { @@ -289,7 +289,7 @@ + (FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecording XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(startScreenRecordingWithRequest:withReply:)]) { [[[FBErrorBuilder builder] - withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"] + withDescriptionFormat:@"The current OS runtime does not support screen recording. This API requires an iOS 17+ runtime"] buildError:error]; return nil; } @@ -331,7 +331,7 @@ + (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid error:(NSError *__autoreleasi XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(stopScreenRecordingWithUUID:withReply:)]) { return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"] + withDescriptionFormat:@"The current OS runtime does not support screen recording. This API requires an iOS 17+ runtime"] buildError:error]; } diff --git a/WebDriverAgentTests/IntegrationTests/FBVoiceOverTests.m b/WebDriverAgentTests/IntegrationTests/FBVoiceOverTests.m index ce747b501..ba9d10dfd 100644 --- a/WebDriverAgentTests/IntegrationTests/FBVoiceOverTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBVoiceOverTests.m @@ -38,7 +38,7 @@ - (void)testVoiceOverUnavailableOnOlderSDK NSError *error = nil; XCTAssertFalse([XCUIDevice.sharedDevice fb_enableVoiceOver:&error]); XCTAssertNotNil(error); - XCTAssertTrue([error.localizedDescription containsString:@"Xcode 27"]); + XCTAssertTrue([error.localizedDescription containsString:@"iOS 27"]); } - (void)testVoiceOverEnableDisableAndNavigation From 417db80eea2a6bfcd962c565d027d770892210e9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 10 Jun 2026 15:35:42 +0000 Subject: [PATCH 48/55] chore(release): 14.1.1 [skip ci] ## [14.1.1](https://github.com/appium/WebDriverAgent/compare/v14.1.0...v14.1.1) (2026-06-10) ### Miscellaneous Chores * address runtime version rather than Xcode for selector based methods ([#1154](https://github.com/appium/WebDriverAgent/issues/1154)) ([6618b0b](https://github.com/appium/WebDriverAgent/commit/6618b0b6ccd06c69fc9e4a0947ef0c88c89b1e48)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d45ae1f3..bea9ef4ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [14.1.1](https://github.com/appium/WebDriverAgent/compare/v14.1.0...v14.1.1) (2026-06-10) + +### Miscellaneous Chores + +* address runtime version rather than Xcode for selector based methods ([#1154](https://github.com/appium/WebDriverAgent/issues/1154)) ([6618b0b](https://github.com/appium/WebDriverAgent/commit/6618b0b6ccd06c69fc9e4a0947ef0c88c89b1e48)) + ## [14.1.0](https://github.com/appium/WebDriverAgent/compare/v14.0.0...v14.1.0) (2026-06-10) ### Features diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 684281c32..f9736e48d 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 14.1.0 + 14.1.1 CFBundleSignature ???? CFBundleVersion - 14.1.0 + 14.1.1 NSPrincipalClass diff --git a/package.json b/package.json index cbea980b8..12786b8c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "14.1.0", + "version": "14.1.1", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 9ea244b29e3c2b160079a620fdf4ef445a3c1e38 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Thu, 18 Jun 2026 07:36:17 +0200 Subject: [PATCH 49/55] feat: Limit the maximum request body size (#1158) --- WebDriverAgentLib/Routing/FBWebServer.m | 5 +++ WebDriverAgentLib/Utilities/FBConfiguration.h | 6 +++ WebDriverAgentLib/Utilities/FBConfiguration.m | 14 ++++++ .../Vendor/CocoaHTTPServer/HTTPConnection.h | 2 + .../Vendor/CocoaHTTPServer/HTTPConnection.m | 43 +++++++++++++++++++ .../UnitTests/FBConfigurationTests.m | 12 ++++++ lib/types.ts | 2 + lib/utils.ts | 16 ++++++- lib/webdriveragent.ts | 6 +++ lib/xcodebuild.ts | 7 +++ test/unit/utils-specs.ts | 7 +++ 11 files changed, 119 insertions(+), 1 deletion(-) diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index 29a2e16e6..5d4d83535 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -39,6 +39,11 @@ - (void)handleResourceNotFound [super handleResourceNotFound]; } +- (UInt64)maxRequestBodySize +{ + return FBConfiguration.httpRequestBodySizeLimit; +} + @end diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgentLib/Utilities/FBConfiguration.h index 2e5fecccd..94ff68ee0 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.h +++ b/WebDriverAgentLib/Utilities/FBConfiguration.h @@ -132,6 +132,12 @@ extern NSString *const FBSnapshotMaxDepthKey; */ + (NSInteger)mjpegServerPort; +/** + The maximum allowed HTTP request body size in bytes. + Defaults to 1GB and can be overridden with the MAX_HTTP_REQUEST_BODY_SIZE environment variable. + */ ++ (UInt64)httpRequestBodySizeLimit; + /** The scaling factor for frames of the mjpeg stream. The default (and maximum) value is 100, which does not perform any scaling. The minimum value must be greater than zero. diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgentLib/Utilities/FBConfiguration.m index 9b76fc3c3..fa856d967 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.m +++ b/WebDriverAgentLib/Utilities/FBConfiguration.m @@ -26,6 +26,7 @@ static NSUInteger const DefaultStartingPort = 8100; static NSUInteger const DefaultMjpegServerPort = 9100; static NSUInteger const DefaultPortRange = 100; +static UInt64 const DefaultHttpRequestBodySizeLimit = 1024ull * 1024ull * 1024ull; static char const *const controllerPrefBundlePath = "/System/Library/PrivateFrameworks/TextInput.framework/TextInput"; static NSString *const controllerClassName = @"TIPreferencesController"; @@ -164,6 +165,19 @@ + (NSInteger)mjpegServerPort return DefaultMjpegServerPort; } ++ (UInt64)httpRequestBodySizeLimit +{ + NSString *limit = NSProcessInfo.processInfo.environment[@"MAX_HTTP_REQUEST_BODY_SIZE"]; + if (limit.length > 0) { + long long parsedLimit = [limit longLongValue]; + if (parsedLimit > 0) { + return (UInt64)parsedLimit; + } + } + + return DefaultHttpRequestBodySizeLimit; +} + + (CGFloat)mjpegScalingFactor { return FBMjpegScalingFactor; diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h index e1868532f..8d409bf71 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h @@ -85,8 +85,10 @@ - (void)prepareForBodyWithSize:(UInt64)contentLength; - (void)processBodyData:(NSData *)postDataChunk; - (void)finishBody; +- (UInt64)maxRequestBodySize; - (void)handleVersionNotSupported:(NSString *)version; +- (void)handleRequestBodyTooLarge; - (void)handleResourceNotFound; - (void)handleInvalidRequest:(NSData *)data; - (void)handleUnknownMethod:(NSString *)method; diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m index 064f653b8..3e2a11893 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m @@ -1301,6 +1301,14 @@ - (void)finishBody // the hook to flush any pending data to disk and maybe close the file. } +/** + * Returns the maximum request body size this connection accepts. + **/ +- (UInt64)maxRequestBodySize +{ + return (UInt64)-1; +} + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Errors //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1324,6 +1332,21 @@ - (void)handleVersionNotSupported:(NSString *)version } +/** + * Called if the HTTP request body is larger than the configured limit. + **/ +- (void)handleRequestBodyTooLarge +{ + HTTPLogWarn(@"HTTP Server: Error 413 - Request Entity Too Large (%@)", [self requestURI]); + + HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:413 description:nil version:HTTPVersion1_1]; + [response setHeaderField:@"Content-Length" value:@"0"]; + [response setHeaderField:@"Connection" value:@"close"]; + + NSData *responseData = [self preprocessErrorResponse:response]; + [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_FINAL_RESPONSE]; +} + /** * Called if we receive some sort of malformed HTTP request. * The data parameter is the invalid HTTP header line, including CRLF, as read from GCDAsyncSocket. @@ -1617,6 +1640,15 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData*)data withTag:(long)ta [self handleInvalidRequest:nil]; return; } + + if (requestContentLength > [self maxRequestBodySize]) + { + HTTPLogWarn(@"%@[%p]: Request body size %llu exceeds the configured limit %llu", + THIS_FILE, self, requestContentLength, [self maxRequestBodySize]); + + [self handleRequestBodyTooLarge]; + return; + } } } else @@ -1749,6 +1781,17 @@ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData*)data withTag:(long)ta if (requestChunkSize > 0) { + UInt64 maxRequestBodySize = [self maxRequestBodySize]; + if (requestChunkSize > maxRequestBodySize || + requestContentLengthReceived > maxRequestBodySize - requestChunkSize) + { + HTTPLogWarn(@"%@[%p]: Chunked request body exceeds the configured limit %llu", + THIS_FILE, self, maxRequestBodySize); + + [self handleRequestBodyTooLarge]; + return; + } + NSUInteger bytesToRead; bytesToRead = (requestChunkSize < POST_CHUNKSIZE) ? (NSUInteger)requestChunkSize : POST_CHUNKSIZE; diff --git a/WebDriverAgentTests/UnitTests/FBConfigurationTests.m b/WebDriverAgentTests/UnitTests/FBConfigurationTests.m index 6d25ea6b0..3f37228fb 100644 --- a/WebDriverAgentTests/UnitTests/FBConfigurationTests.m +++ b/WebDriverAgentTests/UnitTests/FBConfigurationTests.m @@ -22,6 +22,7 @@ - (void)setUp unsetenv("USE_PORT"); unsetenv("USE_IP"); unsetenv("VERBOSE_LOGGING"); + unsetenv("MAX_HTTP_REQUEST_BODY_SIZE"); } - (void)testBindingPortDefault @@ -57,4 +58,15 @@ - (void)testBindingIPEnvironmentOverwrite XCTAssertEqualObjects([FBConfiguration bindingIPAddress], @"192.168.1.100"); } +- (void)testHttpRequestBodySizeLimitDefault +{ + XCTAssertEqual([FBConfiguration httpRequestBodySizeLimit], 1024ull * 1024ull * 1024ull); +} + +- (void)testHttpRequestBodySizeLimitEnvironmentOverwrite +{ + setenv("MAX_HTTP_REQUEST_BODY_SIZE", "1024", 1); + XCTAssertEqual([FBConfiguration httpRequestBodySizeLimit], 1024ull); +} + @end diff --git a/lib/types.ts b/lib/types.ts index f3604718f..3ec482459 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -80,6 +80,7 @@ export interface WebDriverAgentArgs { usePrebuiltWDA?: boolean; derivedDataPath?: string; mjpegServerPort?: number; + maxHttpRequestBodySize?: number; updatedWDABundleId?: string; wdaLaunchTimeout?: number; usePreinstalledWDA?: boolean; @@ -167,6 +168,7 @@ export interface XcodeBuildArgs { updatedWDABundleId?: string; derivedDataPath?: string; mjpegServerPort?: number; + maxHttpRequestBodySize?: number; prebuildDelay?: number; allowProvisioningDeviceRegistration?: boolean; resultBundlePath?: string; diff --git a/lib/utils.ts b/lib/utils.ts index a4782c069..adadcaa92 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -60,6 +60,7 @@ export interface XctestrunFileArgs { bootstrapPath: string; wdaRemotePort: number | string; wdaBindingIP?: string; + maxHttpRequestBodySize?: number | string; } /** @@ -146,13 +147,21 @@ export async function setRealDeviceSecurity( * then it will throw a file not found exception */ export async function setXctestrunFile(args: XctestrunFileArgs): Promise { - const {deviceInfo, sdkVersion, bootstrapPath, wdaRemotePort, wdaBindingIP} = args; + const { + deviceInfo, + sdkVersion, + bootstrapPath, + wdaRemotePort, + wdaBindingIP, + maxHttpRequestBodySize, + } = args; const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); const updateWDAPort = getAdditionalRunContent( deviceInfo.platformName, wdaRemotePort, wdaBindingIP, + maxHttpRequestBodySize, ); const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort); await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); @@ -165,12 +174,14 @@ export async function setXctestrunFile(args: XctestrunFileArgs): Promise * @param platformName - The name of the platform * @param wdaRemotePort - The remote port number * @param wdaBindingIP - The IP address to bind to. If not given, it binds to all interfaces. + * @param maxHttpRequestBodySize - The maximum HTTP request body size in bytes. * @return returns a runner object which has USE_PORT and optionally USE_IP */ export function getAdditionalRunContent( platformName: string, wdaRemotePort: number | string, wdaBindingIP?: string, + maxHttpRequestBodySize?: number | string, ): Record { const runner = `WebDriverAgentRunner${isTvOS(platformName) ? '_tvOS' : ''}`; return { @@ -179,6 +190,9 @@ export function getAdditionalRunContent( // USE_PORT must be 'string' USE_PORT: `${wdaRemotePort}`, ...(wdaBindingIP ? {USE_IP: wdaBindingIP} : {}), + ...(maxHttpRequestBodySize + ? {MAX_HTTP_REQUEST_BODY_SIZE: `${maxHttpRequestBodySize}`} + : {}), }, }, }; diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 95216023d..fed35274b 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -62,6 +62,7 @@ export class WebDriverAgent { private readonly useXctestrunFile?: boolean; private readonly usePrebuiltWDA?: boolean; private readonly mjpegServerPort?: number; + private readonly maxHttpRequestBodySize?: number; private readonly wdaLaunchTimeout: number; private readonly usePreinstalledWDA?: boolean; private readonly updatedWDABundleIdSuffix: string; @@ -105,6 +106,7 @@ export class WebDriverAgent { this.useXctestrunFile = args.useXctestrunFile; this.usePrebuiltWDA = args.usePrebuiltWDA; this.mjpegServerPort = args.mjpegServerPort; + this.maxHttpRequestBodySize = args.maxHttpRequestBodySize; this.updatedWDABundleId = args.updatedWDABundleId; @@ -138,6 +140,7 @@ export class WebDriverAgent { useXctestrunFile: this.useXctestrunFile, derivedDataPath: args.derivedDataPath, mjpegServerPort: this.mjpegServerPort, + maxHttpRequestBodySize: this.maxHttpRequestBodySize, allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration, resultBundlePath: args.resultBundlePath, resultBundleVersion: args.resultBundleVersion, @@ -703,6 +706,9 @@ export class WebDriverAgent { if (this.wdaBindingIP) { xctestEnv.USE_IP = this.wdaBindingIP; } + if (this.maxHttpRequestBodySize) { + xctestEnv.MAX_HTTP_REQUEST_BODY_SIZE = this.maxHttpRequestBodySize; + } this.log.info('Launching WebDriverAgent on the device without xcodebuild'); if (this.isRealDevice) { await this._launchViaDevicectl({env: xctestEnv}); diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index c7fb0cb6a..68e94d444 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -72,6 +72,7 @@ export class XcodeBuild { private readonly wdaBindingIP?: string; private readonly updatedWDABundleId?: string; private readonly mjpegServerPort?: number; + private readonly maxHttpRequestBodySize?: number; private readonly prebuildDelay: number; private readonly allowProvisioningDeviceRegistration?: boolean; private readonly resultBundlePath?: string; @@ -126,6 +127,7 @@ export class XcodeBuild { this.derivedDataPath = args.derivedDataPath; this.mjpegServerPort = args.mjpegServerPort; + this.maxHttpRequestBodySize = args.maxHttpRequestBodySize; this.prebuildDelay = typeof args.prebuildDelay === 'number' ? args.prebuildDelay : PREBUILD_DELAY; @@ -160,6 +162,7 @@ export class XcodeBuild { bootstrapPath: this.bootstrapPath, wdaRemotePort: this.wdaRemotePort || 8100, wdaBindingIP: this.wdaBindingIP, + maxHttpRequestBodySize: this.maxHttpRequestBodySize, }); return; } @@ -445,6 +448,10 @@ export class XcodeBuild { USE_PORT: this.wdaRemotePort, WDA_PRODUCT_BUNDLE_IDENTIFIER: this.updatedWDABundleId || WDA_RUNNER_BUNDLE_ID, }); + delete env.MAX_HTTP_REQUEST_BODY_SIZE; + if (this.maxHttpRequestBodySize) { + env.MAX_HTTP_REQUEST_BODY_SIZE = this.maxHttpRequestBodySize; + } if (this.mjpegServerPort) { // https://github.com/appium/WebDriverAgent/pull/105 env.MJPEG_SERVER_PORT = this.mjpegServerPort; diff --git a/test/unit/utils-specs.ts b/test/unit/utils-specs.ts index d29863200..9d4a90804 100644 --- a/test/unit/utils-specs.ts +++ b/test/unit/utils-specs.ts @@ -181,6 +181,13 @@ describe('utils', function () { const wdaPort = getAdditionalRunContent(PLATFORM_NAME_TVOS, '9000'); expect(wdaPort.WebDriverAgentRunner_tvOS.EnvironmentVariables.USE_PORT).to.equal('9000'); }); + + it('should include max HTTP request body size if provided', function () { + const runContent = getAdditionalRunContent(PLATFORM_NAME_IOS, 8000, undefined, 1024); + expect( + runContent.WebDriverAgentRunner.EnvironmentVariables.MAX_HTTP_REQUEST_BODY_SIZE, + ).to.equal('1024'); + }); }); describe('#getXctestrunFileName', function () { From 4082bce5485fa6a52d0bde112e2fc32ad282386d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Jun 2026 05:39:42 +0000 Subject: [PATCH 50/55] chore(release): 14.2.0 [skip ci] ## [14.2.0](https://github.com/appium/WebDriverAgent/compare/v14.1.1...v14.2.0) (2026-06-18) ### Features * Limit the maximum request body size ([#1158](https://github.com/appium/WebDriverAgent/issues/1158)) ([9ea244b](https://github.com/appium/WebDriverAgent/commit/9ea244b29e3c2b160079a620fdf4ef445a3c1e38)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bea9ef4ff..c60857eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [14.2.0](https://github.com/appium/WebDriverAgent/compare/v14.1.1...v14.2.0) (2026-06-18) + +### Features + +* Limit the maximum request body size ([#1158](https://github.com/appium/WebDriverAgent/issues/1158)) ([9ea244b](https://github.com/appium/WebDriverAgent/commit/9ea244b29e3c2b160079a620fdf4ef445a3c1e38)) + ## [14.1.1](https://github.com/appium/WebDriverAgent/compare/v14.1.0...v14.1.1) (2026-06-10) ### Miscellaneous Chores diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index f9736e48d..594e55cb2 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 14.1.1 + 14.2.0 CFBundleSignature ???? CFBundleVersion - 14.1.1 + 14.2.0 NSPrincipalClass diff --git a/package.json b/package.json index 12786b8c9..c39c6caa9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "14.1.1", + "version": "14.2.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 93d704317483eb9c29a2d46070a6a2c2943ae014 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:42:27 +0200 Subject: [PATCH 51/55] chore(deps-dev): bump @types/node from 25.9.4 to 26.0.0 (#1159) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.4 to 26.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 26.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c39c6caa9..55dfea73b 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@types/chai": "^5.2.3", "@types/chai-as-promised": "^8.0.2", "@types/mocha": "^10.0.1", - "@types/node": "^25.0.0", + "@types/node": "^26.0.0", "@types/sinon": "^21.0.1", "appium-xcode": "^6.0.0", "chai": "^6.0.0", From 4d03d6c08a1d04da3d1341ddf59c4ccb58987fdb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 19 Jun 2026 17:46:52 +0000 Subject: [PATCH 52/55] chore(release): 14.2.1 [skip ci] ## [14.2.1](https://github.com/appium/WebDriverAgent/compare/v14.2.0...v14.2.1) (2026-06-19) ### Miscellaneous Chores * **deps-dev:** bump @types/node from 25.9.4 to 26.0.0 ([#1159](https://github.com/appium/WebDriverAgent/issues/1159)) ([93d7043](https://github.com/appium/WebDriverAgent/commit/93d704317483eb9c29a2d46070a6a2c2943ae014)) --- CHANGELOG.md | 6 ++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c60857eef..c16972a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [14.2.1](https://github.com/appium/WebDriverAgent/compare/v14.2.0...v14.2.1) (2026-06-19) + +### Miscellaneous Chores + +* **deps-dev:** bump @types/node from 25.9.4 to 26.0.0 ([#1159](https://github.com/appium/WebDriverAgent/issues/1159)) ([93d7043](https://github.com/appium/WebDriverAgent/commit/93d704317483eb9c29a2d46070a6a2c2943ae014)) + ## [14.2.0](https://github.com/appium/WebDriverAgent/compare/v14.1.1...v14.2.0) (2026-06-18) ### Features diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 594e55cb2..7161ba5a9 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 14.2.0 + 14.2.1 CFBundleSignature ???? CFBundleVersion - 14.2.0 + 14.2.1 NSPrincipalClass diff --git a/package.json b/package.json index 55dfea73b..4e2664aa8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "14.2.0", + "version": "14.2.1", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From 890d32b4ac3fa881784dacc012650d58274941c8 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 22 Jun 2026 12:22:51 +0200 Subject: [PATCH 53/55] feat: Abstract out platform-specific actions (#1160) BREAKING CHANGE: AppleDevice is now abstract and only contains udid; it no longer exposes simctl or devicectl. BREAKING CHANGE: Preinstalled WDA launch/terminate no longer falls back to package-owned simctl or devicectl behavior. Callers must provide hostOps.simulator or hostOps.realDevicePreinstalled for those flows. --- lib/types.ts | 62 ++- lib/utils.ts | 442 -------------------- lib/utils/index.ts | 20 + lib/utils/module.ts | 29 ++ lib/utils/platform.ts | 10 + lib/utils/processes.ts | 135 ++++++ lib/utils/security.ts | 15 + lib/utils/xctestrun.ts | 160 +++++++ lib/wda-strategies.ts | 340 +++++++++++++++ lib/webdriveragent.ts | 252 ++++------- lib/xcodebuild.ts | 51 ++- package.json | 3 +- test/functional/helpers/simulator.ts | 6 +- test/functional/webdriveragent-e2e-specs.ts | 8 +- test/unit/webdriveragent-specs.ts | 105 ++++- 15 files changed, 1001 insertions(+), 637 deletions(-) delete mode 100644 lib/utils.ts create mode 100644 lib/utils/index.ts create mode 100644 lib/utils/module.ts create mode 100644 lib/utils/platform.ts create mode 100644 lib/utils/processes.ts create mode 100644 lib/utils/security.ts create mode 100644 lib/utils/xctestrun.ts create mode 100644 lib/wda-strategies.ts diff --git a/lib/types.ts b/lib/types.ts index 3ec482459..207f3193b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,4 @@ import {type HTTPHeaders} from '@appium/types'; -import type {Simctl} from 'node-simctl'; -import type {Devicectl} from 'node-devicectl'; // WebDriverAgentLib/Utilities/FBSettings.h export interface WDASettings { @@ -98,12 +96,68 @@ export interface WebDriverAgentArgs { reqBasePath?: string; launchTimeout?: number; extraRequestHeaders?: HTTPHeaders; + hostOps?: WdaHostOps; } export interface AppleDevice { udid: string; - simctl?: Simctl; - devicectl?: Devicectl; +} + +export type WdaStartupStrategyName = + | 'existing-url' + | 'simulator' + | 'real-device-xcodebuild' + | 'real-device-preinstalled'; + +export type WdaLaunchEnvironment = Record; + +export interface WdaLaunchOptions { + udid: string; + bundleId: string; + env: WdaLaunchEnvironment; + wdaLocalPort?: number; + wdaRemotePort: number; + platformName?: string; + platformVersion?: string; + timeoutMs: number; +} + +export interface WdaTerminateOptions { + udid: string; + bundleId: string; +} + +export interface WdaResetTestProcessesOptions { + udid: string; + isSimulator: boolean; +} + +export interface WdaCleanupObsoleteProcessesOptions { + udid: string; + port: string; + commandLineIncludes: string; +} + +export interface SimulatorHostOps { + launchPreinstalled(opts: WdaLaunchOptions): Promise; + terminate(opts: WdaTerminateOptions): Promise; + resetTestProcesses?(opts: WdaResetTestProcessesOptions): Promise; +} + +export interface RealDevicePreinstalledHostOps { + launchPreinstalled(opts: WdaLaunchOptions): Promise; + terminate(opts: WdaTerminateOptions): Promise; +} + +export interface RealDeviceXcodebuildHostOps { + resetTestProcesses?(opts: WdaResetTestProcessesOptions): Promise; + cleanupObsoleteProcesses?(opts: WdaCleanupObsoleteProcessesOptions): Promise; +} + +export interface WdaHostOps { + simulator?: SimulatorHostOps; + realDevicePreinstalled?: RealDevicePreinstalledHostOps; + realDeviceXcodebuild?: RealDeviceXcodebuildHostOps; } /** diff --git a/lib/utils.ts b/lib/utils.ts deleted file mode 100644 index adadcaa92..000000000 --- a/lib/utils.ts +++ /dev/null @@ -1,442 +0,0 @@ -import {fs, plist} from '@appium/support'; -import {exec} from 'teen_process'; -import type {SubProcess} from 'teen_process'; -import path, {dirname} from 'node:path'; -import {fileURLToPath} from 'node:url'; -import {log} from './logger'; -import {PLATFORM_NAME_TVOS} from './constants'; -import _fs from 'node:fs'; -import {waitForCondition} from 'asyncbox'; -import {arch} from 'node:os'; -import type {DeviceInfo} from './types'; - -// Get current filename - works in both CommonJS and ESM -const currentFilename = - typeof __filename !== 'undefined' - ? __filename - : fileURLToPath(new Function('return import.meta.url')()); - -const currentDirname = dirname(currentFilename); - -let moduleRootCache: string | undefined; - -/** - * Calculates the path to the current module's root folder - * - * @returns {string} The full path to module root - * @throws {Error} If the current module root folder cannot be determined - */ -const getModuleRoot = function getModuleRoot(): string { - if (moduleRootCache) { - return moduleRootCache; - } - let currentDir = currentDirname; - let isAtFsRoot = false; - while (!isAtFsRoot) { - const manifestPath = path.join(currentDir, 'package.json'); - try { - if ( - _fs.existsSync(manifestPath) && - JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent' - ) { - moduleRootCache = currentDir; - return currentDir; - } - } catch {} - currentDir = path.dirname(currentDir); - isAtFsRoot = currentDir.length <= path.dirname(currentDir).length; - } - throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module'); -}; - -export const BOOTSTRAP_PATH = getModuleRoot(); - -/** - * Arguments for setting xctestrun file - */ -export interface XctestrunFileArgs { - deviceInfo: DeviceInfo; - sdkVersion: string; - bootstrapPath: string; - wdaRemotePort: number | string; - wdaBindingIP?: string; - maxHttpRequestBodySize?: number | string; -} - -/** - * Find and terminate all processes matching the given pgrep pattern. - */ -export async function killAppUsingPattern(pgrepPattern: string): Promise { - const signals = [2, 15, 9]; - for (const signal of signals) { - const matchedPids = await getPIDsUsingPattern(pgrepPattern); - if (matchedPids.length === 0) { - return; - } - const args = [`-${signal}`, ...matchedPids]; - try { - await exec('kill', args); - } catch (err: any) { - log.debug(`kill ${args.join(' ')} -> ${err.message}`); - } - if (signal === signals[signals.length - 1]) { - // there is no need to wait after SIGKILL - return; - } - try { - await waitForCondition( - async () => { - const pidCheckPromises = matchedPids.map(async (pid) => { - try { - await exec('kill', ['-0', pid]); - // the process is still alive - return false; - } catch { - // the process is dead - return true; - } - }); - return (await Promise.all(pidCheckPromises)).every((x) => x === true); - }, - { - waitMs: 1000, - intervalMs: 100, - }, - ); - return; - } catch { - // try the next signal - } - } -} - -/** - * Return true if the platformName is tvOS - * @param platformName The name of the platorm - * @returns Return true if the platformName is tvOS - */ -export function isTvOS(platformName: string): boolean { - return platformName?.toLowerCase() === PLATFORM_NAME_TVOS.toLowerCase(); -} - -/** - * Configure keychain access required for real-device code signing. - */ -export async function setRealDeviceSecurity( - keychainPath: string, - keychainPassword: string, -): Promise { - log.debug('Setting security for iOS device'); - await exec('security', ['-v', 'list-keychains', '-s', keychainPath]); - await exec('security', ['-v', 'unlock-keychain', '-p', keychainPassword, keychainPath]); - await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]); -} - -/** - * Creates xctestrun file per device & platform version. - * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device - * and WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-${x86_64|arm64}.xctestrun for simulator located @bootstrapPath - * Newer Xcode (Xcode 10.0 at least) generate xctestrun file following sdkVersion. - * e.g. Xcode which has iOS SDK Version 12.2 on an intel Mac host machine generates WebDriverAgentRunner_iphonesimulator.2-x86_64.xctestrun - * even if the cap has platform version 11.4 - * - * @param args - * @return returns xctestrunFilePath for given device - * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device - * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, - * then it will throw a file not found exception - */ -export async function setXctestrunFile(args: XctestrunFileArgs): Promise { - const { - deviceInfo, - sdkVersion, - bootstrapPath, - wdaRemotePort, - wdaBindingIP, - maxHttpRequestBodySize, - } = args; - const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); - const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); - const updateWDAPort = getAdditionalRunContent( - deviceInfo.platformName, - wdaRemotePort, - wdaBindingIP, - maxHttpRequestBodySize, - ); - const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort); - await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); - - return xctestrunFilePath; -} - -/** - * Return the WDA object which appends existing xctest runner content - * @param platformName - The name of the platform - * @param wdaRemotePort - The remote port number - * @param wdaBindingIP - The IP address to bind to. If not given, it binds to all interfaces. - * @param maxHttpRequestBodySize - The maximum HTTP request body size in bytes. - * @return returns a runner object which has USE_PORT and optionally USE_IP - */ -export function getAdditionalRunContent( - platformName: string, - wdaRemotePort: number | string, - wdaBindingIP?: string, - maxHttpRequestBodySize?: number | string, -): Record { - const runner = `WebDriverAgentRunner${isTvOS(platformName) ? '_tvOS' : ''}`; - return { - [runner]: { - EnvironmentVariables: { - // USE_PORT must be 'string' - USE_PORT: `${wdaRemotePort}`, - ...(wdaBindingIP ? {USE_IP: wdaBindingIP} : {}), - ...(maxHttpRequestBodySize - ? {MAX_HTTP_REQUEST_BODY_SIZE: `${maxHttpRequestBodySize}`} - : {}), - }, - }, - }; -} - -/** - * Return the path of xctestrun if it exists - * @param deviceInfo - * @param sdkVersion - The Xcode SDK version of OS. - * @param bootstrapPath - The folder path containing xctestrun file. - */ -export async function getXctestrunFilePath( - deviceInfo: DeviceInfo, - sdkVersion: string, - bootstrapPath: string, -): Promise { - // First try the SDK path, for Xcode 10 (at least) - const sdkBased: [string, string] = [ - path.resolve(bootstrapPath, `${deviceInfo.udid}_${sdkVersion}.xctestrun`), - sdkVersion, - ]; - // Next try Platform path, for earlier Xcode versions - const platformBased: [string, string] = [ - path.resolve(bootstrapPath, `${deviceInfo.udid}_${deviceInfo.platformVersion}.xctestrun`), - deviceInfo.platformVersion, - ]; - - for (const [filePath, version] of [sdkBased, platformBased]) { - if (await fs.exists(filePath)) { - log.info(`Using '${filePath}' as xctestrun file`); - return filePath; - } - const originalXctestrunFile = path.resolve( - bootstrapPath, - getXctestrunFileName(deviceInfo, version), - ); - if (await fs.exists(originalXctestrunFile)) { - // If this is first time run for given device, then first generate xctestrun file for device. - // We need to have a xctestrun file **per device** because we cant not have same wda port for all devices. - await fs.copyFile(originalXctestrunFile, filePath); - log.info(`Using '${filePath}' as xctestrun file copied by '${originalXctestrunFile}'`); - return filePath; - } - } - - throw new Error( - `If you are using 'useXctestrunFile' capability then you ` + - `need to have a xctestrun file (expected: ` + - `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')`, - ); -} - -/** - * Return the name of xctestrun file - * @param deviceInfo - * @param version - The Xcode SDK version of OS. - * @return returns xctestrunFilePath for given device - */ -export function getXctestrunFileName(deviceInfo: DeviceInfo, version: string): string { - const archSuffix = deviceInfo.isRealDevice - ? `os${version}-arm64` - : `simulator${version}-${arch() === 'arm64' ? 'arm64' : 'x86_64'}`; - return `WebDriverAgentRunner_${isTvOS(deviceInfo.platformName) ? 'tvOS_appletv' : 'iphone'}${archSuffix}.xctestrun`; -} - -/** - * Ensures the process is killed after the timeout - */ -export async function killProcess( - name: string, - proc: SubProcess | null | undefined, -): Promise { - if (!proc || !proc.isRunning) { - return; - } - - log.info(`Shutting down '${name}' process (pid '${proc.proc?.pid}')`); - - log.info(`Sending 'SIGTERM'...`); - try { - await proc.stop('SIGTERM', 1000); - return; - } catch (err: any) { - if (!err.message.includes(`Process didn't end after`)) { - throw err; - } - log.debug(`${name} process did not end in a timely fashion: '${err.message}'.`); - } - - log.info(`Sending 'SIGKILL'...`); - try { - await proc.stop('SIGKILL'); - } catch (err: any) { - if (err.message.includes('not currently running')) { - // the process ended but for some reason we were not informed - return; - } - throw err; - } -} - -/** - * Generate a random integer in range [low, high). `low` is inclusive and `high` is exclusive. - */ -export function randomInt(low: number, high: number): number { - return Math.floor(Math.random() * (high - low) + low); -} - -/** - * Retrieves WDA upgrade timestamp. The manifest only gets modified on package upgrade. - */ -export async function getWDAUpgradeTimestamp(): Promise { - const packageManifest = path.resolve(getModuleRoot(), 'package.json'); - if (!(await fs.exists(packageManifest))) { - return null; - } - const {mtime} = await fs.stat(packageManifest); - return mtime.getTime(); -} - -/** - * Escape regular expression metacharacters in a string. - */ -export function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -/** - * Truncate a string to the given length and append ellipsis if needed. - */ -export function truncateString(value: string, length: number): string { - if (value.length <= length) { - return value; - } - return `${value.slice(0, Math.max(0, length - 1))}…`; -} - -/** - * Kills running XCTest processes for the particular device. - */ -export async function resetTestProcesses(udid: string, isSimulator: boolean): Promise { - const processPatterns = [`xcodebuild.*${udid}`]; - if (isSimulator) { - processPatterns.push(`${udid}.*XCTRunner`); - // Some XCTest launches might not include xcodebuild in their command line - processPatterns.push(`xctest.*${udid}`); - } - log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`); - await Promise.all(processPatterns.map(killAppUsingPattern)); -} - -/** - * Get the IDs of processes listening on the particular system port. - * It is also possible to apply additional filtering based on the - * process command line. - * - * @param port - The port number. - * @param filteringFunc - Optional lambda function, which - * receives command line string of the particular process - * listening on given port, and is expected to return - * either true or false to include/exclude the corresponding PID - * from the resulting array. - * @returns - the list of matched process ids. - */ -export async function getPIDsListeningOnPort( - port: string | number, - filteringFunc: ((cmdline: string) => boolean | Promise) | null = null, -): Promise { - const result: string[] = []; - try { - // This only works since Mac OS X El Capitan - const {stdout} = await exec('lsof', ['-ti', `tcp:${port}`]); - result.push(...stdout.trim().split(/\n+/)); - } catch (e: any) { - if (e.code !== 1) { - // code 1 means no processes. Other errors need reporting - log.debug(`Error getting processes listening on port '${port}': ${e.stderr || e.message}`); - } - return result; - } - - if (typeof filteringFunc !== 'function') { - return result; - } - const filtered = await Promise.all( - result.map(async (pid) => { - let stdout: string; - try { - ({stdout} = await exec('ps', ['-p', pid, '-o', 'command'])); - } catch (e: any) { - if (e.code === 1) { - // The process does not exist anymore, there's nothing to filter - return null; - } - throw e; - } - return (await filteringFunc(stdout)) ? pid : null; - }), - ); - return filtered.filter((pid): pid is string => Boolean(pid)); -} - -// Private functions - -async function getPIDsUsingPattern(pattern: string): Promise { - const args = [ - '-if', // case insensitive, full cmdline match - pattern, - ]; - try { - const {stdout} = await exec('pgrep', args); - return stdout - .split(/\s+/) - .map((x) => parseInt(x, 10)) - .filter(Number.isInteger) - .map((x) => `${x}`); - } catch (err: any) { - log.debug( - `'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`, - ); - return []; - } -} - -function mergeObjects, U extends Record>( - target: T, - source: U, -): T & U { - const output: Record = {...target}; - for (const [key, sourceValue] of Object.entries(source)) { - const targetValue = output[key]; - if (isPlainObject(targetValue) && isPlainObject(sourceValue)) { - output[key] = mergeObjects(targetValue, sourceValue); - continue; - } - output[key] = sourceValue; - } - return output as T & U; -} - -function isPlainObject(value: unknown): value is Record { - if (value == null || typeof value !== 'object' || Array.isArray(value)) { - return false; - } - const prototype = Object.getPrototypeOf(value); - return prototype === Object.prototype || prototype === null; -} diff --git a/lib/utils/index.ts b/lib/utils/index.ts new file mode 100644 index 000000000..63783a3a8 --- /dev/null +++ b/lib/utils/index.ts @@ -0,0 +1,20 @@ +import {getWDAUpgradeTimestamp as getWDAUpgradeTimestampImpl} from './module'; + +export {BOOTSTRAP_PATH} from './module'; +export {isTvOS} from './platform'; +export {getPIDsListeningOnPort, killAppUsingPattern, resetTestProcesses} from './processes'; +export {setRealDeviceSecurity} from './security'; +export { + getAdditionalRunContent, + getXctestrunFileName, + getXctestrunFilePath, + setXctestrunFile, +} from './xctestrun'; +export type {XctestrunFileArgs} from './xctestrun'; + +/** + * Retrieves WDA upgrade timestamp. The manifest only gets modified on package upgrade. + */ +export async function getWDAUpgradeTimestamp(): Promise { + return await getWDAUpgradeTimestampImpl(); +} diff --git a/lib/utils/module.ts b/lib/utils/module.ts new file mode 100644 index 000000000..3e1cc097c --- /dev/null +++ b/lib/utils/module.ts @@ -0,0 +1,29 @@ +import {fs, node as supportNode} from '@appium/support'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +// Get current filename - works in both CommonJS and ESM +const currentFilename = + typeof __filename !== 'undefined' + ? __filename + : fileURLToPath(new Function('return import.meta.url')()); + +const moduleRoot = supportNode.getModuleRootSync('appium-webdriveragent', currentFilename); + +if (!moduleRoot) { + throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module'); +} + +export const BOOTSTRAP_PATH = moduleRoot; + +/** + * Retrieves WDA upgrade timestamp. The manifest only gets modified on package upgrade. + */ +export async function getWDAUpgradeTimestamp(): Promise { + const packageManifest = path.resolve(BOOTSTRAP_PATH, 'package.json'); + if (!(await fs.exists(packageManifest))) { + return null; + } + const {mtime} = await fs.stat(packageManifest); + return mtime.getTime(); +} diff --git a/lib/utils/platform.ts b/lib/utils/platform.ts new file mode 100644 index 000000000..0afd34c07 --- /dev/null +++ b/lib/utils/platform.ts @@ -0,0 +1,10 @@ +import {PLATFORM_NAME_TVOS} from '../constants'; + +/** + * Return true if the platformName is tvOS + * @param platformName The name of the platform + * @returns Return true if the platformName is tvOS + */ +export function isTvOS(platformName: string): boolean { + return platformName?.toLowerCase() === PLATFORM_NAME_TVOS.toLowerCase(); +} diff --git a/lib/utils/processes.ts b/lib/utils/processes.ts new file mode 100644 index 000000000..1b47d2888 --- /dev/null +++ b/lib/utils/processes.ts @@ -0,0 +1,135 @@ +import {waitForCondition} from 'asyncbox'; +import {exec} from 'teen_process'; +import {log} from '../logger'; + +/** + * Find and terminate all processes matching the given pgrep pattern. + */ +export async function killAppUsingPattern(pgrepPattern: string): Promise { + const signals = [2, 15, 9]; + for (const signal of signals) { + const matchedPids = await getPIDsUsingPattern(pgrepPattern); + if (matchedPids.length === 0) { + return; + } + const args = [`-${signal}`, ...matchedPids]; + try { + await exec('kill', args); + } catch (err: any) { + log.debug(`kill ${args.join(' ')} -> ${err.message}`); + } + if (signal === signals[signals.length - 1]) { + // there is no need to wait after SIGKILL + return; + } + try { + await waitForCondition( + async () => { + const pidCheckPromises = matchedPids.map(async (pid) => { + try { + await exec('kill', ['-0', pid]); + // the process is still alive + return false; + } catch { + // the process is dead + return true; + } + }); + return (await Promise.all(pidCheckPromises)).every((x) => x === true); + }, + { + waitMs: 1000, + intervalMs: 100, + }, + ); + return; + } catch { + // try the next signal + } + } +} + +/** + * Kills running XCTest processes for the particular device. + */ +export async function resetTestProcesses(udid: string, isSimulator: boolean): Promise { + const processPatterns = [`xcodebuild.*${udid}`]; + if (isSimulator) { + processPatterns.push(`${udid}.*XCTRunner`); + // Some XCTest launches might not include xcodebuild in their command line + processPatterns.push(`xctest.*${udid}`); + } + log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`); + await Promise.all(processPatterns.map(killAppUsingPattern)); +} + +/** + * Get the IDs of processes listening on the particular system port. + * It is also possible to apply additional filtering based on the + * process command line. + * + * @param port - The port number. + * @param filteringFunc - Optional lambda function, which + * receives command line string of the particular process + * listening on given port, and is expected to return + * either true or false to include/exclude the corresponding PID + * from the resulting array. + * @returns - the list of matched process ids. + */ +export async function getPIDsListeningOnPort( + port: string | number, + filteringFunc: ((cmdline: string) => boolean | Promise) | null = null, +): Promise { + const result: string[] = []; + try { + // This only works since Mac OS X El Capitan + const {stdout} = await exec('lsof', ['-ti', `tcp:${port}`]); + result.push(...stdout.trim().split(/\n+/)); + } catch (e: any) { + if (e.code !== 1) { + // code 1 means no processes. Other errors need reporting + log.debug(`Error getting processes listening on port '${port}': ${e.stderr || e.message}`); + } + return result; + } + + if (typeof filteringFunc !== 'function') { + return result; + } + const filtered = await Promise.all( + result.map(async (pid) => { + let stdout: string; + try { + ({stdout} = await exec('ps', ['-p', pid, '-o', 'command'])); + } catch (e: any) { + if (e.code === 1) { + // The process does not exist anymore, there's nothing to filter + return null; + } + throw e; + } + return (await filteringFunc(stdout)) ? pid : null; + }), + ); + return filtered.filter((pid): pid is string => Boolean(pid)); +} + +async function getPIDsUsingPattern(pattern: string): Promise { + const args = [ + '-if', // case insensitive, full cmdline match + pattern, + ]; + try { + const {stdout} = await exec('pgrep', args); + return stdout + .split(/\s+/) + .map((x) => parseInt(x, 10)) + .filter(Number.isInteger) + .map((x) => `${x}`); + } catch (err: any) { + log.debug( + `'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`, + ); + return []; + } +} diff --git a/lib/utils/security.ts b/lib/utils/security.ts new file mode 100644 index 000000000..9ad067287 --- /dev/null +++ b/lib/utils/security.ts @@ -0,0 +1,15 @@ +import {exec} from 'teen_process'; +import {log} from '../logger'; + +/** + * Configure keychain access required for real-device code signing. + */ +export async function setRealDeviceSecurity( + keychainPath: string, + keychainPassword: string, +): Promise { + log.debug('Setting security for iOS device'); + await exec('security', ['-v', 'list-keychains', '-s', keychainPath]); + await exec('security', ['-v', 'unlock-keychain', '-p', keychainPassword, keychainPath]); + await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]); +} diff --git a/lib/utils/xctestrun.ts b/lib/utils/xctestrun.ts new file mode 100644 index 000000000..b4af43b10 --- /dev/null +++ b/lib/utils/xctestrun.ts @@ -0,0 +1,160 @@ +import {fs, plist, util} from '@appium/support'; +import path from 'node:path'; +import {arch} from 'node:os'; +import {log} from '../logger'; +import type {DeviceInfo} from '../types'; +import {isTvOS} from './platform'; + +/** + * Arguments for setting xctestrun file + */ +export interface XctestrunFileArgs { + deviceInfo: DeviceInfo; + sdkVersion: string; + bootstrapPath: string; + wdaRemotePort: number | string; + wdaBindingIP?: string; + maxHttpRequestBodySize?: number | string; +} + +/** + * Creates xctestrun file per device & platform version. + * We expect to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device + * and WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-${x86_64|arm64}.xctestrun for simulator located @bootstrapPath + * Newer Xcode (Xcode 10.0 at least) generates xctestrun file following sdkVersion. + * e.g. Xcode which has iOS SDK Version 12.2 on an intel Mac host machine generates WebDriverAgentRunner_iphonesimulator.2-x86_64.xctestrun + * even if the cap has platform version 11.4 + * + * @param args + * @return returns xctestrunFilePath for given device + * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device + * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, + * then it will throw a file not found exception + */ +export async function setXctestrunFile(args: XctestrunFileArgs): Promise { + const { + deviceInfo, + sdkVersion, + bootstrapPath, + wdaRemotePort, + wdaBindingIP, + maxHttpRequestBodySize, + } = args; + const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath); + const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); + const updateWDAPort = getAdditionalRunContent( + deviceInfo.platformName, + wdaRemotePort, + wdaBindingIP, + maxHttpRequestBodySize, + ); + const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort); + await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); + + return xctestrunFilePath; +} + +/** + * Return the WDA object which appends existing xctest runner content + * @param platformName - The name of the platform + * @param wdaRemotePort - The remote port number + * @param wdaBindingIP - The IP address to bind to. If not given, it binds to all interfaces. + * @param maxHttpRequestBodySize - The maximum HTTP request body size in bytes. + * @return returns a runner object which has USE_PORT and optionally USE_IP + */ +export function getAdditionalRunContent( + platformName: string, + wdaRemotePort: number | string, + wdaBindingIP?: string, + maxHttpRequestBodySize?: number | string, +): Record { + const runner = `WebDriverAgentRunner${isTvOS(platformName) ? '_tvOS' : ''}`; + return { + [runner]: { + EnvironmentVariables: { + // USE_PORT must be 'string' + USE_PORT: `${wdaRemotePort}`, + ...(wdaBindingIP ? {USE_IP: wdaBindingIP} : {}), + ...(maxHttpRequestBodySize + ? {MAX_HTTP_REQUEST_BODY_SIZE: `${maxHttpRequestBodySize}`} + : {}), + }, + }, + }; +} + +/** + * Return the path of xctestrun if it exists + * @param deviceInfo + * @param sdkVersion - The Xcode SDK version of OS. + * @param bootstrapPath - The folder path containing xctestrun file. + */ +export async function getXctestrunFilePath( + deviceInfo: DeviceInfo, + sdkVersion: string, + bootstrapPath: string, +): Promise { + // First try the SDK path, for Xcode 10 (at least) + const sdkBased: [string, string] = [ + path.resolve(bootstrapPath, `${deviceInfo.udid}_${sdkVersion}.xctestrun`), + sdkVersion, + ]; + // Next try Platform path, for earlier Xcode versions + const platformBased: [string, string] = [ + path.resolve(bootstrapPath, `${deviceInfo.udid}_${deviceInfo.platformVersion}.xctestrun`), + deviceInfo.platformVersion, + ]; + + for (const [filePath, version] of [sdkBased, platformBased]) { + if (await fs.exists(filePath)) { + log.info(`Using '${filePath}' as xctestrun file`); + return filePath; + } + const originalXctestrunFile = path.resolve( + bootstrapPath, + getXctestrunFileName(deviceInfo, version), + ); + if (await fs.exists(originalXctestrunFile)) { + // If this is first time run for given device, then first generate xctestrun file for device. + // We need to have a xctestrun file **per device** because we cannot have same wda port for all devices. + await fs.copyFile(originalXctestrunFile, filePath); + log.info(`Using '${filePath}' as xctestrun file copied by '${originalXctestrunFile}'`); + return filePath; + } + } + + throw new Error( + `If you are using 'useXctestrunFile' capability then you ` + + `need to have a xctestrun file (expected: ` + + `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')`, + ); +} + +/** + * Return the name of xctestrun file + * @param deviceInfo + * @param version - The Xcode SDK version of OS. + * @return returns xctestrunFilePath for given device + */ +export function getXctestrunFileName(deviceInfo: DeviceInfo, version: string): string { + const archSuffix = deviceInfo.isRealDevice + ? `os${version}-arm64` + : `simulator${version}-${arch() === 'arm64' ? 'arm64' : 'x86_64'}`; + return `WebDriverAgentRunner_${isTvOS(deviceInfo.platformName) ? 'tvOS_appletv' : 'iphone'}${archSuffix}.xctestrun`; +} + +function mergeObjects, U extends Record>( + target: T, + source: U, +): T & U { + const output: Record = {...target}; + for (const [key, sourceValue] of Object.entries(source)) { + const targetValue = output[key]; + if (util.isPlainObject(targetValue) && util.isPlainObject(sourceValue)) { + output[key] = mergeObjects(targetValue, sourceValue); + continue; + } + output[key] = sourceValue; + } + return output as T & U; +} diff --git a/lib/wda-strategies.ts b/lib/wda-strategies.ts new file mode 100644 index 000000000..68b43a1e9 --- /dev/null +++ b/lib/wda-strategies.ts @@ -0,0 +1,340 @@ +import {exec} from 'teen_process'; +import {fs} from '@appium/support'; +import type {AppiumLogger, StringRecord} from '@appium/types'; +import {getPIDsListeningOnPort, resetTestProcesses} from './utils'; +import type {NoSessionProxy} from './no-session-proxy'; +import type {XcodeBuild} from './xcodebuild'; +import type { + AppleDevice, + RealDevicePreinstalledHostOps, + RealDeviceXcodebuildHostOps, + SimulatorHostOps, + WdaHostOps, + WdaLaunchEnvironment, + WdaStartupStrategyName, +} from './types'; + +const WDA_AGENT_PORT = 8100; +const HOST_OPS_REQUIRED_MESSAGE = + 'Host operations must be provided to launch or terminate preinstalled WebDriverAgent'; + +export interface WdaStartupStrategy { + readonly name: WdaStartupStrategyName; + launch(sessionId: string): Promise; + quit(): Promise; +} + +export interface WdaStartupStrategyContext { + readonly argsWebDriverAgentUrl?: string; + readonly webDriverAgentUrl?: string; + readonly usePreinstalledWDA?: boolean; + readonly useXctestrunFile?: boolean; + readonly usePrebuiltWDA?: boolean; + readonly prebuildWDA?: boolean; + readonly isRealDevice: boolean; + readonly device: AppleDevice; + readonly agentPath: string; + readonly bootstrapPath: string; + readonly bundleIdForXctest: string; + readonly wdaLocalPort?: number; + readonly wdaRemotePort: number; + readonly wdaBindingIP?: string; + readonly wdaLaunchTimeout: number; + readonly mjpegServerPort?: number; + readonly maxHttpRequestBodySize?: number; + readonly platformName?: string; + readonly platformVersion?: string; + readonly log: AppiumLogger; + readonly hostOps: Required; + setWebDriverAgentUrl(value?: string): void; + setUrl(value: string): void; + setupProxies(sessionId: string): void; + getStatus(timeoutMs?: number): Promise; + cleanupProjectIfFresh(): Promise; + xcodebuild(): XcodeBuild; + noSessionProxy(): NoSessionProxy; + setStarted(started: boolean): void; +} + +class ExistingWdaUrlStrategy implements WdaStartupStrategy { + readonly name = 'existing-url' as const; + + constructor(private readonly ctx: WdaStartupStrategyContext) {} + + async launch(sessionId: string): Promise { + this.ctx.log.info(`Using provided WebdriverAgent at '${this.ctx.webDriverAgentUrl}'`); + this.ctx.setUrl(this.ctx.webDriverAgentUrl as string); + this.ctx.setupProxies(sessionId); + return await this.ctx.getStatus(); + } + + async quit(): Promise { + this.ctx.log.debug( + 'Stopping neither xcodebuild nor XCTest session since WDA lifecycle is not managed by this driver', + ); + } +} + +class SimulatorWdaStrategy implements WdaStartupStrategy { + readonly name = 'simulator' as const; + + constructor(private readonly ctx: WdaStartupStrategyContext) {} + + async launch(sessionId: string): Promise { + if (this.ctx.usePreinstalledWDA) { + return await launchPreinstalled(this.ctx, this.ctx.hostOps.simulator, sessionId); + } + return await launchWithXcodebuild(this.ctx, sessionId); + } + + async quit(): Promise { + if (this.ctx.usePreinstalledWDA) { + await terminatePreinstalled(this.ctx, this.ctx.hostOps.simulator); + return; + } + await quitXcodebuild(this.ctx); + } +} + +class RealDeviceXcodebuildStrategy implements WdaStartupStrategy { + readonly name = 'real-device-xcodebuild' as const; + + constructor(private readonly ctx: WdaStartupStrategyContext) {} + + async launch(sessionId: string): Promise { + return await launchWithXcodebuild(this.ctx, sessionId); + } + + async quit(): Promise { + await quitXcodebuild(this.ctx); + } +} + +class RealDevicePreinstalledStrategy implements WdaStartupStrategy { + readonly name = 'real-device-preinstalled' as const; + + constructor(private readonly ctx: WdaStartupStrategyContext) {} + + async launch(sessionId: string): Promise { + return await launchPreinstalled(this.ctx, this.ctx.hostOps.realDevicePreinstalled, sessionId); + } + + async quit(): Promise { + await terminatePreinstalled(this.ctx, this.ctx.hostOps.realDevicePreinstalled); + } +} + +/** + * Selects the WDA startup strategy for the provided launch arguments. + */ +export function selectWdaStartupStrategyName(args: { + realDevice?: boolean; + webDriverAgentUrl?: string; + usePreinstalledWDA?: boolean; +}): WdaStartupStrategyName { + if (args.webDriverAgentUrl) { + return 'existing-url'; + } + if (!args.realDevice) { + return 'simulator'; + } + if (args.usePreinstalledWDA) { + return 'real-device-preinstalled'; + } + return 'real-device-xcodebuild'; +} + +/** + * Creates a WDA startup strategy for the current facade state. + */ +export function createWdaStartupStrategy(ctx: WdaStartupStrategyContext): WdaStartupStrategy { + const startupStrategy = selectWdaStartupStrategyName({ + realDevice: ctx.isRealDevice, + webDriverAgentUrl: ctx.webDriverAgentUrl, + usePreinstalledWDA: ctx.usePreinstalledWDA, + }); + switch (startupStrategy) { + case 'existing-url': + return new ExistingWdaUrlStrategy(ctx); + case 'simulator': + return new SimulatorWdaStrategy(ctx); + case 'real-device-preinstalled': + return new RealDevicePreinstalledStrategy(ctx); + case 'real-device-xcodebuild': + return new RealDeviceXcodebuildStrategy(ctx); + default: + throw new Error(`Unknown WDA startup strategy: ${startupStrategy}`); + } +} + +/** + * Creates default host operations for flows the package can own directly. + */ +export function createDefaultWdaHostOps(): Required { + return { + simulator: createDefaultSimulatorWdaHostOps(), + realDevicePreinstalled: createDefaultRealDevicePreinstalledHostOps(), + realDeviceXcodebuild: createDefaultRealDeviceXcodebuildHostOps(), + }; +} + +/** + * Creates default simulator host operations. + */ +export function createDefaultSimulatorWdaHostOps(): SimulatorHostOps { + return { + async launchPreinstalled() { + throw new Error(HOST_OPS_REQUIRED_MESSAGE); + }, + async terminate() { + throw new Error(HOST_OPS_REQUIRED_MESSAGE); + }, + async resetTestProcesses({udid, isSimulator}) { + await resetTestProcesses(udid, isSimulator); + }, + }; +} + +/** + * Creates default real-device preinstalled host operations. + */ +export function createDefaultRealDevicePreinstalledHostOps(): RealDevicePreinstalledHostOps { + return { + async launchPreinstalled() { + throw new Error(HOST_OPS_REQUIRED_MESSAGE); + }, + async terminate() { + throw new Error(HOST_OPS_REQUIRED_MESSAGE); + }, + }; +} + +/** + * Creates default real-device xcodebuild host operations. + */ +export function createDefaultRealDeviceXcodebuildHostOps(): RealDeviceXcodebuildHostOps { + return { + async resetTestProcesses({udid, isSimulator}) { + await resetTestProcesses(udid, isSimulator); + }, + async cleanupObsoleteProcesses({udid, port, commandLineIncludes}) { + const obsoletePids = await getPIDsListeningOnPort( + port, + (cmdLine) => + cmdLine.includes(commandLineIncludes) && + !cmdLine.toLowerCase().includes(udid.toLowerCase()), + ); + + if (obsoletePids.length > 0) { + await exec('kill', obsoletePids); + } + }, + }; +} + +async function launchWithXcodebuild( + ctx: WdaStartupStrategyContext, + sessionId: string, +): Promise { + ctx.log.info('Launching WebDriverAgent on the device'); + + ctx.setupProxies(sessionId); + + if (!ctx.useXctestrunFile && !(await fs.exists(ctx.agentPath))) { + throw new Error( + `Trying to use WebDriverAgent project at '${ctx.agentPath}' but the ` + 'file does not exist', + ); + } + + if (ctx.useXctestrunFile || ctx.usePrebuiltWDA) { + ctx.log.info('Skipped WDA project cleanup according to the provided capabilities'); + } else { + await ctx.cleanupProjectIfFresh(); + } + + const resetTestProcesses = ctx.isRealDevice + ? ctx.hostOps.realDeviceXcodebuild.resetTestProcesses + : ctx.hostOps.simulator.resetTestProcesses; + await resetTestProcesses?.({ + udid: ctx.device.udid, + isSimulator: !ctx.isRealDevice, + }); + + const xcodebuild = ctx.xcodebuild(); + await xcodebuild.init(ctx.noSessionProxy()); + + if (ctx.prebuildWDA) { + await xcodebuild.prebuild(); + } + return (await xcodebuild.start()) as StringRecord | null; +} + +async function launchPreinstalled( + ctx: WdaStartupStrategyContext, + hostOps: SimulatorHostOps | RealDevicePreinstalledHostOps, + sessionId: string, +): Promise { + const xctestEnv = createPreinstalledWdaEnvironment(ctx); + ctx.log.info('Launching WebDriverAgent on the device without xcodebuild'); + await hostOps.launchPreinstalled({ + udid: ctx.device.udid, + bundleId: ctx.bundleIdForXctest, + env: xctestEnv, + wdaLocalPort: ctx.wdaLocalPort, + wdaRemotePort: ctx.wdaRemotePort, + platformName: ctx.platformName, + platformVersion: ctx.platformVersion, + timeoutMs: ctx.wdaLaunchTimeout, + }); + + ctx.setupProxies(sessionId); + let status: StringRecord | null; + try { + status = await ctx.getStatus(ctx.wdaLaunchTimeout); + } catch { + throw new Error( + `Failed to start the preinstalled WebDriverAgent in ${ctx.wdaLaunchTimeout} ms. ` + + `The WebDriverAgent might not be properly built or the device might be locked. ` + + `The 'appium:wdaLaunchTimeout' capability modifies the timeout.`, + ); + } + ctx.setStarted(true); + return status; +} + +async function terminatePreinstalled( + ctx: WdaStartupStrategyContext, + hostOps: SimulatorHostOps | RealDevicePreinstalledHostOps, +): Promise { + ctx.log.info('Stopping the XCTest session'); + try { + await hostOps.terminate({ + udid: ctx.device.udid, + bundleId: ctx.bundleIdForXctest, + }); + } catch (e: any) { + ctx.log.warn(e.message); + } +} + +async function quitXcodebuild(ctx: WdaStartupStrategyContext): Promise { + ctx.log.info('Shutting down sub-processes'); + await ctx.xcodebuild().quit(); +} + +function createPreinstalledWdaEnvironment(ctx: WdaStartupStrategyContext): WdaLaunchEnvironment { + const xctestEnv: WdaLaunchEnvironment = { + USE_PORT: ctx.wdaLocalPort || WDA_AGENT_PORT, + WDA_PRODUCT_BUNDLE_IDENTIFIER: ctx.bundleIdForXctest, + }; + if (ctx.mjpegServerPort) { + xctestEnv.MJPEG_SERVER_PORT = ctx.mjpegServerPort; + } + if (ctx.wdaBindingIP) { + xctestEnv.USE_IP = ctx.wdaBindingIP; + } + if (ctx.maxHttpRequestBodySize) { + xctestEnv.MAX_HTTP_REQUEST_BODY_SIZE = ctx.maxHttpRequestBodySize; + } + return xctestEnv; +} diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index fed35274b..2e7d97b4d 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -5,15 +5,9 @@ import {fs, util} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; import {log as defaultLogger} from './logger'; import {NoSessionProxy} from './no-session-proxy'; -import { - getWDAUpgradeTimestamp, - resetTestProcesses, - getPIDsListeningOnPort, - BOOTSTRAP_PATH, -} from './utils'; +import {BOOTSTRAP_PATH, getWDAUpgradeTimestamp} from './utils'; import {XcodeBuild} from './xcodebuild'; import AsyncLock from 'async-lock'; -import {exec} from 'teen_process'; import { WDA_RUNNER_BUNDLE_ID, WDA_BASE_URL, @@ -26,9 +20,14 @@ import type { AppleDevice, XcodeBuildSettings, RetrieveBuildSettingsOptions, + WdaHostOps, } from './types'; -import type {Simctl} from 'node-simctl'; -import type {Devicectl} from 'node-devicectl'; +import { + createDefaultWdaHostOps, + createWdaStartupStrategy, + type WdaStartupStrategy, + type WdaStartupStrategyContext, +} from './wda-strategies'; const WDA_LAUNCH_TIMEOUT = 60 * 1000; const WDA_AGENT_PORT = 8100; @@ -66,6 +65,8 @@ export class WebDriverAgent { private readonly wdaLaunchTimeout: number; private readonly usePreinstalledWDA?: boolean; private readonly updatedWDABundleIdSuffix: string; + private readonly hostOps: Required; + private activeStartupStrategy?: WdaStartupStrategy; private _xcodebuild?: XcodeBuild | null; private _url?: URL; @@ -113,6 +114,21 @@ export class WebDriverAgent { this.wdaLaunchTimeout = args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT; this.usePreinstalledWDA = args.usePreinstalledWDA; this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? DEFAULT_TEST_BUNDLE_SUFFIX; + const defaultHostOps = createDefaultWdaHostOps(); + this.hostOps = { + simulator: { + ...defaultHostOps.simulator, + ...args.hostOps?.simulator, + }, + realDevicePreinstalled: { + ...defaultHostOps.realDevicePreinstalled, + ...args.hostOps?.realDevicePreinstalled, + }, + realDeviceXcodebuild: { + ...defaultHostOps.realDeviceXcodebuild, + ...args.hostOps?.realDeviceXcodebuild, + }, + }; this._xcodebuild = this.canSkipXcodebuild ? null @@ -244,32 +260,14 @@ export class WebDriverAgent { * that are listening on the same port but belong to different devices. */ async cleanupObsoleteProcesses(): Promise { - const obsoletePids = await getPIDsListeningOnPort( - this.url.port as string, - (cmdLine) => - cmdLine.includes('/WebDriverAgentRunner') && - !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase()), - ); - - if (obsoletePids.length === 0) { - this.log.debug( - `No obsolete cached processes from previous WDA sessions ` + - `listening on port ${this.url.port} have been found`, - ); - return; - } - - this.log.info( - `Detected ${obsoletePids.length} obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} ` + - `from previous WDA sessions. Cleaning them up`, - ); try { - await exec('kill', obsoletePids); + await this.hostOps.realDeviceXcodebuild.cleanupObsoleteProcesses?.({ + udid: this.device.udid, + port: this.url.port as string, + commandLineIncludes: '/WebDriverAgentRunner', + }); } catch (e: any) { - this.log.warn( - `Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` + - `Original error: ${e.message}`, - ); + this.log.warn(`Failed to clean obsolete cached processes. Original error: ${e.message}`); } } @@ -298,53 +296,10 @@ export class WebDriverAgent { * @param sessionId Launch WDA and establish the session with this sessionId */ async launch(sessionId: string): Promise { - if (this.webDriverAgentUrl) { - this.log.info(`Using provided WebdriverAgent at '${this.webDriverAgentUrl}'`); - this.url = this.webDriverAgentUrl; - this.setupProxies(sessionId); - return await this.getStatus(); - } - - if (this.usePreinstalledWDA) { - return await this.launchWithPreinstalledWDA(sessionId); - } - - this.log.info('Launching WebDriverAgent on the device'); - - this.setupProxies(sessionId); - - if (!this.useXctestrunFile && !(await fs.exists(this.agentPath))) { - throw new Error( - `Trying to use WebDriverAgent project at '${this.agentPath}' but the ` + - 'file does not exist', - ); - } - - // useXctestrunFile and usePrebuiltWDA use existing dependencies - // It depends on user side - if (this.useXctestrunFile || this.usePrebuiltWDA) { - this.log.info('Skipped WDA project cleanup according to the provided capabilities'); - } else { - const synchronizationKey = path.normalize(this.bootstrapPath); - await SHARED_RESOURCES_GUARD.acquire( - synchronizationKey, - async () => await this._cleanupProjectIfFresh(), - ); - } - - // We need to provide WDA local port, because it might be occupied - await resetTestProcesses(this.device.udid, !this.isRealDevice); - - if (!this.noSessionProxy) { - throw new Error('noSessionProxy is not available'); - } - await this.xcodebuild.init(this.noSessionProxy); - - // Start the xcodebuild process - if (this.prebuildWDA) { - await this.xcodebuild.prebuild(); - } - return (await this.xcodebuild.start()) as StringRecord | null; + const startupStrategy = this.createStartupStrategy(); + this.log.info(`Selected '${startupStrategy.name}' WebDriverAgent startup strategy`); + this.activeStartupStrategy = startupStrategy; + return await startupStrategy.launch(sessionId); } /** @@ -364,27 +319,7 @@ export class WebDriverAgent { * Handles both preinstalled WDA and xcodebuild-based sessions. */ async quit(): Promise { - if (this.usePreinstalledWDA) { - this.log.info('Stopping the XCTest session'); - try { - if (this.device.simctl) { - await this.device.simctl.terminateApp(this.bundleIdForXctest); - } else if (this.device.devicectl) { - await this.device.devicectl.terminateApp(this.bundleIdForXctest); - } - } catch (e: any) { - this.log.warn(e.message); - } - } else if (!this.args.webDriverAgentUrl) { - this.log.info('Shutting down sub-processes'); - if (this._xcodebuild) { - await this.xcodebuild.quit(); - } - } else { - this.log.debug( - 'Stopping neither xcodebuild nor XCTest session since WDA lifecycle is not managed by this driver', - ); - } + await (this.activeStartupStrategy ?? this.createStartupStrategy()).quit(); if (this.jwproxy) { this.jwproxy.sessionId = null; @@ -397,6 +332,7 @@ export class WebDriverAgent { // then clean that up. If the url was supplied, we want to keep it this.webDriverAgentUrl = undefined; } + this.activeStartupStrategy = undefined; } /** @@ -487,6 +423,58 @@ export class WebDriverAgent { return cachedUrl; } + private createStartupStrategy(): WdaStartupStrategy { + const context: WdaStartupStrategyContext = { + argsWebDriverAgentUrl: this.args.webDriverAgentUrl, + webDriverAgentUrl: this.webDriverAgentUrl, + usePreinstalledWDA: this.usePreinstalledWDA, + useXctestrunFile: this.useXctestrunFile, + usePrebuiltWDA: this.usePrebuiltWDA, + prebuildWDA: this.prebuildWDA, + isRealDevice: this.isRealDevice, + device: this.device, + agentPath: this.agentPath, + bootstrapPath: this.bootstrapPath, + bundleIdForXctest: this.bundleIdForXctest, + wdaLocalPort: this.wdaLocalPort, + wdaRemotePort: this.wdaRemotePort, + wdaBindingIP: this.wdaBindingIP, + wdaLaunchTimeout: this.wdaLaunchTimeout, + mjpegServerPort: this.mjpegServerPort, + maxHttpRequestBodySize: this.maxHttpRequestBodySize, + platformName: this.platformName, + platformVersion: this.platformVersion, + log: this.log, + hostOps: this.hostOps, + setWebDriverAgentUrl: (value) => { + this.webDriverAgentUrl = value; + }, + setUrl: (value) => { + this.url = value; + }, + setupProxies: (sessionId) => this.setupProxies(sessionId), + getStatus: async (timeoutMs) => await this.getStatus(timeoutMs), + cleanupProjectIfFresh: async () => { + const synchronizationKey = path.normalize(this.bootstrapPath); + await SHARED_RESOURCES_GUARD.acquire( + synchronizationKey, + async () => await this._cleanupProjectIfFresh(), + ); + }, + xcodebuild: () => this.xcodebuild, + noSessionProxy: () => { + if (!this.noSessionProxy) { + throw new Error('noSessionProxy is not available'); + } + return this.noSessionProxy; + }, + setStarted: (started) => { + this.started = started; + }, + }; + return createWdaStartupStrategy(context); + } + private setupProxies(sessionId: string): void { const proxyOpts: any = { log: this.log, @@ -671,66 +659,4 @@ export class WebDriverAgent { this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`); } } - - /** - * Launch WDA with preinstalled package with 'xcrun devicectl device process launch'. - * The WDA package must be prepared properly like published via - * https://github.com/appium/WebDriverAgent/releases - * with proper sign for this case. - * - * @param opts launching WDA with devicectl command options. - */ - private async _launchViaDevicectl( - opts: {env?: Record} = {}, - ): Promise { - const {env} = opts; - - await (this.device.devicectl as Devicectl).launchApp(this.bundleIdForXctest, { - env, - terminateExisting: true, - }); - } - - /** - * Launch WDA with preinstalled package without xcodebuild. - * @param sessionId Launch WDA and establish the session with this sessionId - */ - private async launchWithPreinstalledWDA(sessionId: string): Promise { - const xctestEnv: Record = { - USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT, - WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest, - }; - if (this.mjpegServerPort) { - xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort; - } - if (this.wdaBindingIP) { - xctestEnv.USE_IP = this.wdaBindingIP; - } - if (this.maxHttpRequestBodySize) { - xctestEnv.MAX_HTTP_REQUEST_BODY_SIZE = this.maxHttpRequestBodySize; - } - this.log.info('Launching WebDriverAgent on the device without xcodebuild'); - if (this.isRealDevice) { - await this._launchViaDevicectl({env: xctestEnv}); - } else { - await (this.device.simctl as Simctl).exec('launch', { - args: ['--terminate-running-process', this.device.udid, this.bundleIdForXctest], - env: xctestEnv, - }); - } - - this.setupProxies(sessionId); - let status: StringRecord | null; - try { - status = await this.getStatus(this.wdaLaunchTimeout); - } catch { - throw new Error( - `Failed to start the preinstalled WebDriverAgent in ${this.wdaLaunchTimeout} ms. ` + - `The WebDriverAgent might not be properly built or the device might be locked. ` + - `The 'appium:wdaLaunchTimeout' capability modifies the timeout.`, - ); - } - this.started = true; - return status; - } } diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 68e94d444..962607915 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -1,17 +1,9 @@ import {retryInterval} from 'asyncbox'; import {SubProcess, exec} from 'teen_process'; -import {logger, timing} from '@appium/support'; +import {logger, timing, util} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; import {log as defaultLogger} from './logger'; -import { - setRealDeviceSecurity, - setXctestrunFile, - killProcess, - getWDAUpgradeTimestamp, - isTvOS, - escapeRegExp, - truncateString, -} from './utils'; +import {getWDAUpgradeTimestamp, isTvOS, setRealDeviceSecurity, setXctestrunFile} from './utils'; import path from 'node:path'; import {WDA_RUNNER_BUNDLE_ID} from './constants'; import type { @@ -36,7 +28,7 @@ const IGNORED_ERRORS = [ 'Failed to remove screenshot at path', ]; const IGNORED_ERRORS_PATTERN = new RegExp( - '(' + IGNORED_ERRORS.map((errStr) => escapeRegExp(errStr)).join('|') + ')', + '(' + IGNORED_ERRORS.map((errStr) => util.escapeRegExp(errStr)).join('|') + ')', ); const RUNNER_SCHEME_TV = 'WebDriverAgentRunner_tvOS'; @@ -308,7 +300,34 @@ export class XcodeBuild { * Stops the xcodebuild process and cleans up resources. */ async quit(): Promise { - await killProcess('xcodebuild', this.xcodebuild); + const xcodebuild = this.xcodebuild; + if (!xcodebuild || !xcodebuild.isRunning) { + return; + } + + this.log.info(`Shutting down 'xcodebuild' process (pid '${xcodebuild.proc?.pid}')`); + + try { + await xcodebuild.stop('SIGTERM', 1000); + return; + } catch (err: unknown) { + if (!(err as Error)?.message?.includes(`Process didn't end after`)) { + throw err; + } + this.log.debug( + `xcodebuild process did not end in a timely fashion: '${(err as Error)?.message}'.`, + ); + } + + try { + await xcodebuild.stop('SIGKILL'); + } catch (err: unknown) { + if ((err as Error)?.message?.includes('not currently running')) { + // The process ended but for some reason we were not informed. + return; + } + throw err; + } } private async fetchBuildSettings( @@ -336,7 +355,7 @@ export class XcodeBuild { entries = JSON.parse(stdout) as XcodeShowBuildSettingsEntry[]; } catch (err: any) { this.log.warn( - `Cannot parse WDA build settings for scheme '${schemeLabel}' from ${truncateString(stdout, 300)}. ` + + `Cannot parse WDA build settings for scheme '${schemeLabel}' from ${util.truncateString(stdout, 300)}. ` + `Original error: ${err.message}`, ); return; @@ -433,10 +452,8 @@ export class XcodeBuild { } private async createSubProcess(buildOnly: boolean = false): Promise { - if (!this.useXctestrunFile && this.realDevice) { - if (this.keychainPath && this.keychainPassword) { - await setRealDeviceSecurity(this.keychainPath, this.keychainPassword); - } + if (!this.useXctestrunFile && this.realDevice && this.keychainPath && this.keychainPassword) { + await setRealDeviceSecurity(this.keychainPath, this.keychainPassword); } const {cmd, args} = this.getCommand(buildOnly); diff --git a/package.json b/package.json index 4e2664aa8..410ad89c7 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "chai-as-promised": "^8.0.0", "conventional-changelog-conventionalcommits": "^9.0.0", "mocha": "^11.0.1", - "node-devicectl": "^1.4.0", "node-simctl": "^8.0.0", "prettier": "^3.0.0", "semantic-release": "^25.0.2", @@ -77,7 +76,7 @@ "dependencies": { "@appium/base-driver": "^10.3.0", "@appium/strongbox": "^1.0.0-rc.1", - "@appium/support": "^7.0.0-rc.1", + "@appium/support": "^7.2.1", "appium-ios-simulator": "^8.0.0", "async-lock": "^1.0.0", "asyncbox": "^6.1.0", diff --git a/test/functional/helpers/simulator.ts b/test/functional/helpers/simulator.ts index 166d4910e..a72197741 100644 --- a/test/functional/helpers/simulator.ts +++ b/test/functional/helpers/simulator.ts @@ -4,6 +4,8 @@ import {killAllSimulators as simKill} from 'appium-ios-simulator'; import {resetTestProcesses} from '../../../lib/utils'; import type {AppleDevice} from '../../../lib/types'; +type SimulatorTestDevice = AppleDevice & {simctl: Simctl}; + export async function killAllSimulators(): Promise { const simctl = new Simctl(); const allDevices = Object.values(await simctl.getDevices()).flat(); @@ -19,10 +21,10 @@ export async function killAllSimulators(): Promise { await simKill(); } -export async function shutdownSimulator(device: AppleDevice): Promise { +export async function shutdownSimulator(device: SimulatorTestDevice): Promise { // stop XCTest processes if running to avoid unexpected side effects await resetTestProcesses(device.udid, true); - await (device.simctl as Simctl).shutdownDevice(); + await device.simctl.shutdownDevice(); } export async function deleteDeviceWithRetry(udid: string): Promise { diff --git a/test/functional/webdriveragent-e2e-specs.ts b/test/functional/webdriveragent-e2e-specs.ts index 7eee7eac7..0e93cd912 100644 --- a/test/functional/webdriveragent-e2e-specs.ts +++ b/test/functional/webdriveragent-e2e-specs.ts @@ -12,6 +12,8 @@ import type {AppleDevice} from '../../lib/types'; chai.use(chaiAsPromised); +type SimulatorTestDevice = AppleDevice & {simctl: Simctl}; + const MOCHA_TIMEOUT_MS = 60 * 1000 * 5; const SIM_DEVICE_NAME = 'webDriverAgentTest'; @@ -35,13 +37,13 @@ describe('WebDriverAgent', function () { this.timeout(MOCHA_TIMEOUT_MS); describe('with fresh sim', function () { - let device: AppleDevice; + let device: SimulatorTestDevice; let simctl: Simctl; before(async function () { simctl = new Simctl(); simctl.udid = await simctl.createDevice(SIM_DEVICE_NAME, DEVICE_NAME, PLATFORM_VERSION); - device = await getSimulator(simctl.udid); + device = (await getSimulator(simctl.udid)) as SimulatorTestDevice; // Prebuild WDA const wda = new WebDriverAgent({ @@ -67,7 +69,7 @@ describe('WebDriverAgent', function () { this.timeout(6 * 60 * 1000); beforeEach(async function () { await killAllSimulators(); - await (device.simctl as Simctl).startBootMonitor({ + await device.simctl.startBootMonitor({ shouldPreboot: true, timeout: SIM_STARTUP_TIMEOUT_MS, }); diff --git a/test/unit/webdriveragent-specs.ts b/test/unit/webdriveragent-specs.ts index 560de6dfc..92889df6a 100644 --- a/test/unit/webdriveragent-specs.ts +++ b/test/unit/webdriveragent-specs.ts @@ -2,20 +2,17 @@ import chai, {expect} from 'chai'; import chaiAsPromised from 'chai-as-promised'; import {BOOTSTRAP_PATH} from '../../lib/utils'; import {WebDriverAgent} from '../../lib/webdriveragent'; +import {selectWdaStartupStrategyName} from '../../lib/wda-strategies'; import * as utils from '../../lib/utils'; import path from 'node:path'; import sinon from 'sinon'; import type {WebDriverAgentArgs} from '../../lib/types'; -import type {Simctl} from 'node-simctl'; -import type {Devicectl} from 'node-devicectl'; chai.use(chaiAsPromised); const fakeConstructorArgs: WebDriverAgentArgs = { device: { udid: 'some-sim-udid', - simctl: {} as Simctl, - devicectl: {} as Devicectl, }, platformVersion: '9', host: 'me', @@ -28,6 +25,28 @@ const customAgentPath = '/path/to/some/agent/WebDriverAgent.xcodeproj'; const customDerivedDataPath = '/path/to/some/agent/DerivedData/'; describe('WebDriverAgent', function () { + describe('startup strategy selection', function () { + it('should select an existing-url strategy for external WDA URLs', function () { + expect(selectWdaStartupStrategyName({webDriverAgentUrl: 'http://127.0.0.1:8100'})).to.equal( + 'existing-url', + ); + }); + + it('should select a simulator strategy for simulator sessions', function () { + expect(selectWdaStartupStrategyName({realDevice: false})).to.equal('simulator'); + }); + + it('should select a real-device preinstalled strategy for no-xcode real-device sessions', function () { + expect(selectWdaStartupStrategyName({realDevice: true, usePreinstalledWDA: true})).to.equal( + 'real-device-preinstalled', + ); + }); + + it('should select a real-device xcodebuild strategy for default real-device sessions', function () { + expect(selectWdaStartupStrategyName({realDevice: true})).to.equal('real-device-xcodebuild'); + }); + }); + describe('Constructor', function () { it('should have a default wda agent if not specified', function () { const agent = new WebDriverAgent(fakeConstructorArgs); @@ -60,6 +79,15 @@ describe('WebDriverAgent', function () { expect(await agent.retrieveDerivedDataPath()).to.eql(customDerivedDataPath); } }); + + it('should not create xcodebuild for real-device preinstalled sessions', function () { + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + realDevice: true, + usePreinstalledWDA: true, + }); + expect(() => agent.xcodebuild).to.throw('xcodebuild is not available'); + }); }); describe('launch', function () { @@ -402,5 +430,74 @@ describe('WebDriverAgent', function () { expect(agent.bundleIdForXctest).to.equal('io.appium.wda.customsuffix'); }); }); + + describe('host operations', function () { + let sandbox: sinon.SinonSandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should delegate real-device preinstalled launch and terminate to injected host ops', async function () { + const launchPreinstalled = sandbox.stub().resolves(); + const terminate = sandbox.stub().resolves(); + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + device: {udid: 'real-device-udid'}, + realDevice: true, + usePreinstalledWDA: true, + wdaLocalPort: 9100, + updatedWDABundleId: 'io.appium.wda', + mjpegServerPort: 9200, + wdaBindingIP: '127.0.0.1', + maxHttpRequestBodySize: 1024, + hostOps: { + realDevicePreinstalled: { + launchPreinstalled, + terminate, + }, + }, + }); + sandbox.stub(agent as any, 'getStatus').resolves({build: 'data'}); + + await expect(agent.launch('sessionId')).to.eventually.eql({build: 'data'}); + sinon.assert.calledOnce(launchPreinstalled); + expect(launchPreinstalled.firstCall.args[0]).to.include({ + udid: 'real-device-udid', + bundleId: 'io.appium.wda.xctrunner', + wdaLocalPort: 9100, + }); + expect(launchPreinstalled.firstCall.args[0].env).to.eql({ + USE_PORT: 9100, + WDA_PRODUCT_BUNDLE_IDENTIFIER: 'io.appium.wda.xctrunner', + MJPEG_SERVER_PORT: 9200, + USE_IP: '127.0.0.1', + MAX_HTTP_REQUEST_BODY_SIZE: 1024, + }); + + await agent.quit(); + sinon.assert.calledOnceWithExactly(terminate, { + udid: 'real-device-udid', + bundleId: 'io.appium.wda.xctrunner', + }); + }); + + it('should require injected host ops for real-device preinstalled launch', async function () { + const agent = new WebDriverAgent({ + ...fakeConstructorArgs, + device: {udid: 'real-device-udid'}, + realDevice: true, + usePreinstalledWDA: true, + }); + + await expect(agent.launch('sessionId')).to.be.rejectedWith( + 'Host operations must be provided', + ); + }); + }); }); }); From 2409b6e0fb9893ad57acd839f7b3658a865d0196 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 22 Jun 2026 10:26:56 +0000 Subject: [PATCH 54/55] chore(release): 15.0.0 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [15.0.0](https://github.com/appium/WebDriverAgent/compare/v14.2.1...v15.0.0) (2026-06-22) ### ⚠ BREAKING CHANGES * AppleDevice is now abstract and only contains udid; it no longer exposes simctl or devicectl. * Preinstalled WDA launch/terminate no longer falls back to package-owned simctl or devicectl behavior. Callers must provide hostOps.simulator or hostOps.realDevicePreinstalled for those flows. ### Features * Abstract out platform-specific actions ([#1160](https://github.com/appium/WebDriverAgent/issues/1160)) ([890d32b](https://github.com/appium/WebDriverAgent/commit/890d32b4ac3fa881784dacc012650d58274941c8)) --- CHANGELOG.md | 11 +++++++++++ WebDriverAgentLib/Info.plist | 4 ++-- package.json | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c16972a21..12c1ec8d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [15.0.0](https://github.com/appium/WebDriverAgent/compare/v14.2.1...v15.0.0) (2026-06-22) + +### ⚠ BREAKING CHANGES + +* AppleDevice is now abstract and only contains udid; it no longer exposes simctl or devicectl. +* Preinstalled WDA launch/terminate no longer falls back to package-owned simctl or devicectl behavior. Callers must provide hostOps.simulator or hostOps.realDevicePreinstalled for those flows. + +### Features + +* Abstract out platform-specific actions ([#1160](https://github.com/appium/WebDriverAgent/issues/1160)) ([890d32b](https://github.com/appium/WebDriverAgent/commit/890d32b4ac3fa881784dacc012650d58274941c8)) + ## [14.2.1](https://github.com/appium/WebDriverAgent/compare/v14.2.0...v14.2.1) (2026-06-19) ### Miscellaneous Chores diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 7161ba5a9..9f335d622 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 14.2.1 + 15.0.0 CFBundleSignature ???? CFBundleVersion - 14.2.1 + 15.0.0 NSPrincipalClass diff --git a/package.json b/package.json index 410ad89c7..bb44e64e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appium-webdriveragent", - "version": "14.2.1", + "version": "15.0.0", "description": "Package bundling WebDriverAgent", "main": "./build/lib/index.js", "types": "./build/lib/index.d.ts", From bad42bad0d7ece2d5589bbc9b18acc4e79c8ea1b Mon Sep 17 00:00:00 2001 From: Elton Carreiro Date: Wed, 24 Jun 2026 17:58:21 -0300 Subject: [PATCH 55/55] Rebase QA Wolf customizations onto upstream WebDriverAgent v15.0.0 (GEN-2002) Replays the QA Wolf fork's customizations (previously based on upstream v11.4.1) on top of upstream appium/WebDriverAgent v15.0.0. Conflict resolutions: - FBSessionCommands.m: v15 extracted settings get/set into the new FBSettingsHandler. Took v15's version and re-applied our `preWarmPageSource` setting in FBSettingsHandler's set/get maps. - FBXPath.m/.h: kept our "attribute @interface decls moved to the header" change; ported v15's new FBNativeAccessibilityElementAttribute interface into FBXPath.h so v15's @implementation still resolves. - Regenerated SPM public-header symlinks via headers.sh: adds v15's XCUIDevice+FBVoiceOver.h; preserves the deliberately-exposed XCUIElement+FBCustomActions.h. package.json inherits upstream's 15.0.0 (we never pinned a fork version). Verification: standalone `xcodebuild -scheme WebDriverAgentLib` framework build fails identically on both this branch and the current production v11 fork (pre-existing: the framework build does not honor the fork's publicHeadersPath header exposure) -- i.e. no regression from the rebase. The authoritative compile gate is the ios-agent SPM integration build. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 1 + Package.swift | 92 +++++ .../Categories/XCUIApplication+FBQuiescence.h | 2 +- .../Categories/XCUIElement+FBIsVisible.m | 35 +- .../Commands/FBAlertViewCommands.h | 6 + WebDriverAgentLib/Commands/FBCustomCommands.h | 51 +++ WebDriverAgentLib/Commands/FBDebugCommands.h | 3 + .../Commands/FBElementCommands.h | 53 +++ .../Commands/FBFindElementCommands.h | 13 + .../Commands/FBOrientationCommands.h | 5 + .../Commands/FBScreenshotCommands.h | 2 + .../Commands/FBSessionCommands.h | 35 ++ .../Commands/FBTouchActionCommands.h | 2 + .../Commands/FBTouchIDCommands.h | 2 + .../Commands/FBTouchIDCommands.m | 17 +- WebDriverAgentLib/Commands/FBVideoCommands.h | 4 + WebDriverAgentLib/QAWolf/ExecuteWDACommand.h | 15 + WebDriverAgentLib/QAWolf/ExecuteWDACommand.m | 20 ++ .../QAWolf/HandleWDACommandException.h | 15 + .../QAWolf/HandleWDACommandException.m | 52 +++ .../QAWolf/QAWObjCExceptionHandler.h | 31 ++ .../QAWolf/QAWObjCExceptionHandler.m | 40 +++ WebDriverAgentLib/QAWolf/QAWSnapshotResult.h | 18 + WebDriverAgentLib/QAWolf/QAWSnapshotResult.m | 43 +++ WebDriverAgentLib/QAWolf/QAWXML.h | 46 +++ WebDriverAgentLib/QAWolf/QAWXML.m | 320 ++++++++++++++++++ .../QAWolf/XCUIElement+QAWSnapshotUtilities.h | 8 + .../QAWolf/XCUIElement+QAWSnapshotUtilities.m | 82 +++++ .../QAWolf/XCUIElementSnapshotParser.h | 19 ++ .../QAWolf/XCUIElementSnapshotParser.m | 18 + .../Routing/FBResponseJSONPayload.h | 3 + .../Routing/FBResponseJSONPayload.m | 2 +- WebDriverAgentLib/Routing/FBRouteRequest.h | 13 + WebDriverAgentLib/Routing/FBRouteRequest.m | 10 + WebDriverAgentLib/Utilities/FBConfiguration.h | 16 + WebDriverAgentLib/Utilities/FBConfiguration.m | 11 + WebDriverAgentLib/Utilities/FBSettings.h | 1 + WebDriverAgentLib/Utilities/FBSettings.m | 1 + .../Utilities/FBSettingsHandler.m | 7 + .../Utilities/FBXCodeCompatibility.h | 2 +- WebDriverAgentLib/Utilities/FBXPath.h | 155 +++++++++ WebDriverAgentLib/Utilities/FBXPath.m | 172 +++------- WebDriverAgentLib/WebDriverAgentLib.h | 32 ++ .../include/WebDriverAgentLib/CDStructures.h | 1 + .../WebDriverAgentLib/ExecuteWDACommand.h | 1 + .../include/WebDriverAgentLib/FBAlert.h | 1 + .../WebDriverAgentLib/FBAlertViewCommands.h | 1 + .../WebDriverAgentLib/FBCapabilities.h | 1 + .../WebDriverAgentLib/FBCommandHandler.h | 1 + .../WebDriverAgentLib/FBCommandStatus.h | 1 + .../WebDriverAgentLib/FBConfiguration.h | 1 + .../WebDriverAgentLib/FBCustomCommands.h | 1 + .../WebDriverAgentLib/FBDebugCommands.h | 1 + .../FBDebugLogDelegateDecorator.h | 1 + .../include/WebDriverAgentLib/FBElement.h | 1 + .../WebDriverAgentLib/FBElementCache.h | 1 + .../WebDriverAgentLib/FBElementCommands.h | 1 + .../FBElementTypeTransformer.h | 1 + .../WebDriverAgentLib/FBErrorBuilder.h | 1 + .../WebDriverAgentLib/FBExceptionHandler.h | 1 + .../include/WebDriverAgentLib/FBExceptions.h | 1 + .../FBFailureProofTestCase.h | 1 + .../WebDriverAgentLib/FBFindElementCommands.h | 1 + .../WebDriverAgentLib/FBHTTPStatusCodes.h | 1 + .../include/WebDriverAgentLib/FBKeyboard.h | 1 + .../include/WebDriverAgentLib/FBLogger.h | 1 + .../include/WebDriverAgentLib/FBMacros.h | 1 + .../include/WebDriverAgentLib/FBMathUtils.h | 1 + .../WebDriverAgentLib/FBOrientationCommands.h | 1 + .../WebDriverAgentLib/FBProtocolHelpers.h | 1 + .../WebDriverAgentLib/FBResponseJSONPayload.h | 1 + .../WebDriverAgentLib/FBResponsePayload.h | 1 + .../include/WebDriverAgentLib/FBRoute.h | 1 + .../WebDriverAgentLib/FBRouteRequest.h | 1 + .../WebDriverAgentLib/FBRunLoopSpinner.h | 1 + .../WebDriverAgentLib/FBRuntimeUtils.h | 1 + .../WebDriverAgentLib/FBScreenshotCommands.h | 1 + .../include/WebDriverAgentLib/FBSession.h | 1 + .../WebDriverAgentLib/FBSessionCommands.h | 1 + .../include/WebDriverAgentLib/FBSettings.h | 1 + .../WebDriverAgentLib/FBTouchActionCommands.h | 1 + .../WebDriverAgentLib/FBTouchIDCommands.h | 1 + .../WebDriverAgentLib/FBVideoCommands.h | 1 + .../include/WebDriverAgentLib/FBWebServer.h | 1 + .../WebDriverAgentLib/FBXCElementSnapshot.h | 1 + .../FBXCElementSnapshotWrapper.h | 1 + .../WebDriverAgentLib/FBXCodeCompatibility.h | 1 + .../FBXMLGenerationOptions.h | 1 + .../include/WebDriverAgentLib/FBXPath.h | 1 + .../HandleWDACommandException.h | 1 + .../QAWObjCExceptionHandler.h | 1 + .../WebDriverAgentLib/QAWSnapshotResult.h | 1 + .../include/WebDriverAgentLib/QAWXML.h | 1 + .../WebDriverAgentLib/WebDriverAgentLib.h | 1 + .../XCDebugLogDelegate-Protocol.h | 1 + .../WebDriverAgentLib/XCPointerEvent.h | 1 + .../WebDriverAgentLib/XCTIssue+FBPatcher.h | 1 + .../include/WebDriverAgentLib/XCTestCase.h | 1 + .../XCUIApplication+FBHelpers.h | 1 + .../XCUIApplication+FBQuiescence.h | 1 + .../WebDriverAgentLib/XCUIApplication.h | 1 + .../XCUIApplicationProcessDelay.h | 1 + .../XCUIDevice+FBHealthCheck.h | 1 + .../WebDriverAgentLib/XCUIDevice+FBHelpers.h | 1 + .../WebDriverAgentLib/XCUIDevice+FBRotation.h | 1 + .../XCUIDevice+FBVoiceOver.h | 1 + .../XCUIElement+FBAccessibility.h | 1 + .../XCUIElement+FBCustomActions.h | 1 + .../WebDriverAgentLib/XCUIElement+FBFind.h | 1 + .../XCUIElement+FBForceTouch.h | 1 + .../XCUIElement+FBIsVisible.h | 1 + .../XCUIElement+FBScrolling.h | 1 + .../XCUIElement+FBUtilities.h | 1 + .../XCUIElement+FBWebDriverAttributes.h | 1 + .../XCUIElement+QAWSnapshotUtilities.h | 1 + .../include/WebDriverAgentLib/XCUIElement.h | 1 + .../XCUIElementSnapshotParser.h | 1 + headers.sh | 55 +++ 118 files changed, 1469 insertions(+), 135 deletions(-) create mode 100644 Package.swift create mode 100644 WebDriverAgentLib/QAWolf/ExecuteWDACommand.h create mode 100644 WebDriverAgentLib/QAWolf/ExecuteWDACommand.m create mode 100644 WebDriverAgentLib/QAWolf/HandleWDACommandException.h create mode 100644 WebDriverAgentLib/QAWolf/HandleWDACommandException.m create mode 100644 WebDriverAgentLib/QAWolf/QAWObjCExceptionHandler.h create mode 100644 WebDriverAgentLib/QAWolf/QAWObjCExceptionHandler.m create mode 100644 WebDriverAgentLib/QAWolf/QAWSnapshotResult.h create mode 100644 WebDriverAgentLib/QAWolf/QAWSnapshotResult.m create mode 100644 WebDriverAgentLib/QAWolf/QAWXML.h create mode 100644 WebDriverAgentLib/QAWolf/QAWXML.m create mode 100644 WebDriverAgentLib/QAWolf/XCUIElement+QAWSnapshotUtilities.h create mode 100644 WebDriverAgentLib/QAWolf/XCUIElement+QAWSnapshotUtilities.m create mode 100644 WebDriverAgentLib/QAWolf/XCUIElementSnapshotParser.h create mode 100644 WebDriverAgentLib/QAWolf/XCUIElementSnapshotParser.m create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/CDStructures.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/ExecuteWDACommand.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBAlert.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBAlertViewCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBCapabilities.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBCommandHandler.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBCommandStatus.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBConfiguration.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBCustomCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBDebugCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBDebugLogDelegateDecorator.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBElement.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBElementCache.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBElementCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBElementTypeTransformer.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBErrorBuilder.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBExceptionHandler.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBExceptions.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBFailureProofTestCase.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBFindElementCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBHTTPStatusCodes.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBKeyboard.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBLogger.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBMacros.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBMathUtils.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBOrientationCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBProtocolHelpers.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBResponseJSONPayload.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBResponsePayload.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBRoute.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBRouteRequest.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBRunLoopSpinner.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBRuntimeUtils.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBScreenshotCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBSession.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBSessionCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBSettings.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBTouchActionCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBTouchIDCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBVideoCommands.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBWebServer.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBXCElementSnapshot.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBXCElementSnapshotWrapper.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBXCodeCompatibility.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBXMLGenerationOptions.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/FBXPath.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/HandleWDACommandException.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/QAWObjCExceptionHandler.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/QAWSnapshotResult.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/QAWXML.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/WebDriverAgentLib.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCDebugLogDelegate-Protocol.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCPointerEvent.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCTIssue+FBPatcher.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCTestCase.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication+FBHelpers.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication+FBQuiescence.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplicationProcessDelay.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBHealthCheck.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBHelpers.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBRotation.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBVoiceOver.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBAccessibility.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBCustomActions.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBFind.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBForceTouch.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBIsVisible.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBScrolling.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBUtilities.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBWebDriverAttributes.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+QAWSnapshotUtilities.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement.h create mode 120000 WebDriverAgentLib/include/WebDriverAgentLib/XCUIElementSnapshotParser.h create mode 100755 headers.sh diff --git a/.gitignore b/.gitignore index 75359860f..1f5672e65 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## Build generated +.build/ build/ clang/ DerivedData diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..6b34c9bc4 --- /dev/null +++ b/Package.swift @@ -0,0 +1,92 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "WebDriverAgent", + defaultLocalization: "en", + platforms: [ + .iOS(.v12) + ], + products: [ + // Expose the Obj-C library as a SwiftPM product so it can be imported from Swift. + .library(name: "WebDriverAgentLib", targets: ["WebDriverAgentLib"]) + ], + targets: [ + // ---- Vendor: CocoaAsyncSocket ---- + .target( + name: "CocoaAsyncSocket", + path: "WebDriverAgentLib/Vendor/CocoaAsyncSocket", + publicHeadersPath: "." + ), + + // ---- Vendor: CocoaHTTPServer ---- + .target( + name: "CocoaHTTPServer", + dependencies: ["CocoaAsyncSocket"], + path: "WebDriverAgentLib/Vendor/CocoaHTTPServer", + publicHeadersPath: ".", + cSettings: [ + .headerSearchPath("Categories"), + .headerSearchPath("Responses") + ] + ), + + // ---- Vendor: RoutingHTTPServer ---- + .target( + name: "RoutingHTTPServer", + dependencies: ["CocoaHTTPServer"], + path: "WebDriverAgentLib/Vendor/RoutingHTTPServer", + publicHeadersPath: ".", + cSettings: [ + // Need CocoaHTTPServer headers in include path for quote-style imports + .headerSearchPath("../CocoaHTTPServer"), + .headerSearchPath("../CocoaHTTPServer/Responses"), + .headerSearchPath("../CocoaHTTPServer/Categories") + ] + ), + + // ---- Main library ---- + .target( + name: "WebDriverAgentLib", + dependencies: [ + "RoutingHTTPServer" + ], + path: "WebDriverAgentLib", + exclude: [ + "Vendor" // compiled in dedicated targets above + ], + publicHeadersPath: "include", + cSettings: [ + .headerSearchPath(".."), + .headerSearchPath("include"), + .headerSearchPath("Routing"), + .headerSearchPath("Utilities"), + .headerSearchPath("Utilities/LRUCache"), + .headerSearchPath("Categories"), + .headerSearchPath("Commands"), + .headerSearchPath("include/WebDriverAgentLib"), + .headerSearchPath("QAWolf"), + .headerSearchPath("../PrivateHeaders/XCTest"), + .headerSearchPath("../PrivateHeaders/MobileCoreServices"), + .headerSearchPath("../PrivateHeaders/AccessibilityUtilities"), + .headerSearchPath("../PrivateHeaders/TextInput"), + .headerSearchPath("../PrivateHeaders/UIKitCore"), + ], + linkerSettings: [ + // Link against XCTest and system UIKit/Foundation frameworks that + // are referenced across the Objective-C implementation files. + .linkedFramework("XCTest"), + .linkedFramework("UIKit"), + .linkedFramework("Foundation"), + .linkedFramework("MobileCoreServices"), + .linkedFramework("UniformTypeIdentifiers") + ] + ), + // Unit-test target (optional – keeps parity with the original project). + .testTarget( + name: "WebDriverAgentLibTests", + dependencies: ["WebDriverAgentLib"], + path: "WebDriverAgentTests" + ) + ] +) diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h index 962a48146..3f6c5f393 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h @@ -7,7 +7,7 @@ */ #import -#import "XCUIApplication.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m index 75a228a3e..1e9d9dabc 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m @@ -8,7 +8,10 @@ #import "XCUIElement+FBIsVisible.h" +#import + #import "FBElementUtils.h" +#import "FBLogger.h" #import "FBXCodeCompatibility.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "XCUIElement+FBUtilities.h" @@ -36,12 +39,40 @@ @implementation FBXCElementSnapshotWrapper (FBIsVisible) - (BOOL)fb_hasVisibleDescendants { + // Instrument the descendant-cache short-circuit so we can measure how often + // it fires vs falls through to the synchronous AX-framework IPC. Counters + // are process-wide and accumulate across requests; this is intentional — + // the rate (hits/calls) is the useful signal, not the absolute totals. + // Logging every 50 calls keeps the output tractable on dense screens + // (500+ nodes per /source). + static _Atomic NSUInteger fbVisCacheCalls = 0; + static _Atomic NSUInteger fbVisCacheHits = 0; + + NSUInteger descendantsWalked = 0; + BOOL hit = NO; for (id descendant in (self._allDescendants ?: @[])) { + descendantsWalked++; if ([fetchSnapshotVisibility(descendant) boolValue]) { - return YES; + hit = YES; + break; } } - return NO; + + NSUInteger calls = atomic_fetch_add(&fbVisCacheCalls, 1) + 1; + if (hit) { + atomic_fetch_add(&fbVisCacheHits, 1); + } + if (0 == (calls % 50)) { + NSUInteger hits = atomic_load(&fbVisCacheHits); + double hitRate = 100.0 * (double)hits / (double)calls; + [FBLogger logFmt:@"[QA_WOLF] [INFO] [VisCache] descendant-cache stats: calls=%lu hits=%lu (%.1f%%) lastWalked=%lu lastResult=%@", + (unsigned long)calls, + (unsigned long)hits, + hitRate, + (unsigned long)descendantsWalked, + hit ? @"YES" : @"NO"]; + } + return hit; } - (BOOL)fb_isVisible diff --git a/WebDriverAgentLib/Commands/FBAlertViewCommands.h b/WebDriverAgentLib/Commands/FBAlertViewCommands.h index 687c39ce0..1cb6b6564 100644 --- a/WebDriverAgentLib/Commands/FBAlertViewCommands.h +++ b/WebDriverAgentLib/Commands/FBAlertViewCommands.h @@ -14,6 +14,12 @@ NS_ASSUME_NONNULL_BEGIN @interface FBAlertViewCommands : NSObject ++ (id)handleAlertGetTextCommand:(FBRouteRequest *)request; ++ (id)handleAlertSetTextCommand:(FBRouteRequest *)request; ++ (id)handleAlertAcceptCommand:(FBRouteRequest *)request; ++ (id)handleAlertDismissCommand:(FBRouteRequest *)request; ++ (id)handleGetAlertButtonsCommand:(FBRouteRequest *)request; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.h b/WebDriverAgentLib/Commands/FBCustomCommands.h index dca154229..830f5ba29 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.h +++ b/WebDriverAgentLib/Commands/FBCustomCommands.h @@ -14,6 +14,57 @@ NS_ASSUME_NONNULL_BEGIN @interface FBCustomCommands : NSObject +// Session & application lifecycle ++ (id)handleHomescreenCommand:(FBRouteRequest *)request; ++ (id)handleDeactivateAppCommand:(FBRouteRequest *)request; ++ (id)handleTimeouts:(FBRouteRequest *)request; + +// Keyboard & input ++ (id)handleDismissKeyboardCommand:(FBRouteRequest *)request; ++ (id)handlePingCommand:(FBRouteRequest *)request; + +// Screen & device state ++ (id)handleGetScreen:(FBRouteRequest *)request; ++ (id)handleLock:(FBRouteRequest *)request; ++ (id)handleIsLocked:(FBRouteRequest *)request; ++ (id)handleUnlock:(FBRouteRequest *)request; ++ (id)handleActiveAppInfo:(FBRouteRequest *)request; + +#if !TARGET_OS_TV +// Clipboard & battery (iOS only) ++ (id)handleSetPasteboard:(FBRouteRequest *)request; ++ (id)handleGetPasteboard:(FBRouteRequest *)request; ++ (id)handleGetBatteryInfo:(FBRouteRequest *)request; +#endif + +// Hardware buttons & Siri ++ (id)handlePressButtonCommand:(FBRouteRequest *)request; ++ (id)handleActivateSiri:(FBRouteRequest *)request; ++ (id)handlePeformIOHIDEvent:(FBRouteRequest *)request; + +// App launching & permissions ++ (id)handleLaunchUnattachedApp:(FBRouteRequest *)request; ++ (id)handleResetAppAuth:(FBRouteRequest *)request; + +// Device information & appearance ++ (id)handleGetDeviceInfo:(FBRouteRequest *)request; ++ (id)handleSetDeviceAppearance:(FBRouteRequest *)request; + +// Location ++ (id)handleGetLocation:(FBRouteRequest *)request; +#if !TARGET_OS_TV ++ (id)handleGetSimulatedLocation:(FBRouteRequest *)request; ++ (id)handleSetSimulatedLocation:(FBRouteRequest *)request; ++ (id)handleClearSimulatedLocation:(FBRouteRequest *)request; +#if __clang_major__ >= 15 ++ (id)handleKeyboardInput:(FBRouteRequest *)request; +#endif +#endif + +// Notifications & audits ++ (id)handleExpectNotification:(FBRouteRequest *)request; ++ (id)handlePerformAccessibilityAudit:(FBRouteRequest *)request; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBDebugCommands.h b/WebDriverAgentLib/Commands/FBDebugCommands.h index 99728e03a..890b79d2a 100644 --- a/WebDriverAgentLib/Commands/FBDebugCommands.h +++ b/WebDriverAgentLib/Commands/FBDebugCommands.h @@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN @interface FBDebugCommands : NSObject ++ (id)handleGetSourceCommand:(FBRouteRequest *)request; ++ (id)handleGetAccessibleSourceCommand:(FBRouteRequest *)request; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBElementCommands.h b/WebDriverAgentLib/Commands/FBElementCommands.h index 3c07ee6d6..6fa12ee96 100644 --- a/WebDriverAgentLib/Commands/FBElementCommands.h +++ b/WebDriverAgentLib/Commands/FBElementCommands.h @@ -14,6 +14,59 @@ NS_ASSUME_NONNULL_BEGIN @interface FBElementCommands : NSObject +// Element attribute retrieval ++ (id)handleGetEnabled:(FBRouteRequest *)request; ++ (id)handleGetRect:(FBRouteRequest *)request; ++ (id)handleGetAttribute:(FBRouteRequest *)request; ++ (id)handleGetText:(FBRouteRequest *)request; ++ (id)handleGetDisplayed:(FBRouteRequest *)request; ++ (id)handleGetAccessible:(FBRouteRequest *)request; ++ (id)handleGetIsAccessibilityContainer:(FBRouteRequest *)request; ++ (id)handleGetName:(FBRouteRequest *)request; ++ (id)handleGetSelected:(FBRouteRequest *)request; + +// Element interaction ++ (id)handleSetValue:(FBRouteRequest *)request; ++ (id)handleClick:(FBRouteRequest *)request; ++ (id)handleClear:(FBRouteRequest *)request; + +#if TARGET_OS_TV +// tvOS specific ++ (id)handleGetFocused:(FBRouteRequest *)request; ++ (id)handleFocuse:(FBRouteRequest *)request; +#else +// iOS specific gestures ++ (id)handleDoubleTap:(FBRouteRequest *)request; ++ (id)handleTwoFingerTap:(FBRouteRequest *)request; ++ (id)handleTapWithNumberOfTaps:(FBRouteRequest *)request; ++ (id)handleTouchAndHold:(FBRouteRequest *)request; ++ (id)handlePressAndDragWithVelocity:(FBRouteRequest *)request; ++ (id)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request; ++ (id)handleScroll:(FBRouteRequest *)request; ++ (id)handleScrollTo:(FBRouteRequest *)request; ++ (id)handleDrag:(FBRouteRequest *)request; ++ (id)handleSwipe:(FBRouteRequest *)request; ++ (id)handleTap:(FBRouteRequest *)request; ++ (id)handlePinch:(FBRouteRequest *)request; ++ (id)handleRotate:(FBRouteRequest *)request; ++ (id)handleForceTouch:(FBRouteRequest *)request; +#endif + +// Keyboard input ++ (id)handleKeys:(FBRouteRequest *)request; + +// Window metrics ++ (id)handleGetWindowSize:(FBRouteRequest *)request; ++ (id)handleGetWindowRect:(FBRouteRequest *)request; + +// Screenshots ++ (id)handleElementScreenshot:(FBRouteRequest *)request; + +#if !TARGET_OS_TV +// Picker wheel ++ (id)handleWheelSelect:(FBRouteRequest *)request; +#endif + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBFindElementCommands.h b/WebDriverAgentLib/Commands/FBFindElementCommands.h index 7349e10c9..6ae1d83cf 100644 --- a/WebDriverAgentLib/Commands/FBFindElementCommands.h +++ b/WebDriverAgentLib/Commands/FBFindElementCommands.h @@ -13,6 +13,19 @@ NS_ASSUME_NONNULL_BEGIN @interface FBFindElementCommands : NSObject + ++ (id)handleFindElement:(FBRouteRequest *)request; ++ (id)handleFindElements:(FBRouteRequest *)request; ++ (id)handleFindVisibleCells:(FBRouteRequest *)request; ++ (id)handleFindSubElement:(FBRouteRequest *)request; ++ (id)handleFindSubElements:(FBRouteRequest *)request; + +#if TARGET_OS_TV ++ (id)handleGetFocusedElement:(FBRouteRequest *)request; +#else ++ (id)handleGetActiveElement:(FBRouteRequest *)request; +#endif + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBOrientationCommands.h b/WebDriverAgentLib/Commands/FBOrientationCommands.h index 1aaaacd63..198d43b89 100644 --- a/WebDriverAgentLib/Commands/FBOrientationCommands.h +++ b/WebDriverAgentLib/Commands/FBOrientationCommands.h @@ -14,6 +14,11 @@ NS_ASSUME_NONNULL_BEGIN @interface FBOrientationCommands : NSObject ++ (id)handleGetOrientation:(FBRouteRequest *)request; ++ (id)handleSetOrientation:(FBRouteRequest *)request; ++ (id)handleGetRotation:(FBRouteRequest *)request; ++ (id)handleSetRotation:(FBRouteRequest *)request; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBScreenshotCommands.h b/WebDriverAgentLib/Commands/FBScreenshotCommands.h index 3f4fa4a2c..69fb23c74 100644 --- a/WebDriverAgentLib/Commands/FBScreenshotCommands.h +++ b/WebDriverAgentLib/Commands/FBScreenshotCommands.h @@ -14,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN @interface FBScreenshotCommands : NSObject ++ (id)handleGetScreenshot:(FBRouteRequest *)request; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.h b/WebDriverAgentLib/Commands/FBSessionCommands.h index 95f3f258f..44fff856f 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.h +++ b/WebDriverAgentLib/Commands/FBSessionCommands.h @@ -7,6 +7,7 @@ */ #import +#import #import @@ -14,6 +15,40 @@ NS_ASSUME_NONNULL_BEGIN @interface FBSessionCommands : NSObject +// Session management ++ (id)handleCreateSession:(FBRouteRequest *)request; ++ (id)handleGetActiveSession:(FBRouteRequest *)request; ++ (id)handleDeleteSession:(FBRouteRequest *)request; + +// URL handling ++ (id)handleOpenURL:(FBRouteRequest *)request; + +// App management ++ (id)handleSessionAppLaunch:(FBRouteRequest *)request; ++ (id)handleSessionAppActivate:(FBRouteRequest *)request; ++ (id)handleSessionAppTerminate:(FBRouteRequest *)request; ++ (id)handleSessionAppState:(FBRouteRequest *)request; ++ (id)handleGetActiveAppsList:(FBRouteRequest *)request; + +// Status and health ++ (id)handleGetStatus:(FBRouteRequest *)request; ++ (id)handleGetHealthCheck:(FBRouteRequest *)request; + +// Settings ++ (id)handleGetSettings:(FBRouteRequest *)request; ++ (id)handleSetSettings:(FBRouteRequest *)request; + +// Utility methods ++ (NSDictionary *)sessionInformation; ++ (NSDictionary *)currentCapabilities; + +// Device helper ++ (NSString *)deviceNameByUserInterfaceIdiom:(UIUserInterfaceIdiom)userInterfaceIdiom; + +// Deep link opening helper ++ (nullable id)openDeepLink:(NSString *)initialUrl + withApplication:(nullable NSString *)bundleID + timeout:(nullable NSNumber *)timeout; @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBTouchActionCommands.h b/WebDriverAgentLib/Commands/FBTouchActionCommands.h index d9b84fc7d..6893346b3 100644 --- a/WebDriverAgentLib/Commands/FBTouchActionCommands.h +++ b/WebDriverAgentLib/Commands/FBTouchActionCommands.h @@ -14,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN @interface FBTouchActionCommands : NSObject ++ (id)handlePerformW3CTouchActions:(FBRouteRequest *)request; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBTouchIDCommands.h b/WebDriverAgentLib/Commands/FBTouchIDCommands.h index ffcf2e812..7a0a70474 100644 --- a/WebDriverAgentLib/Commands/FBTouchIDCommands.h +++ b/WebDriverAgentLib/Commands/FBTouchIDCommands.h @@ -14,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN @interface FBTouchIDCommands : NSObject ++ (id)handleFingerTouchShouldMatch:(FBRouteRequest *)request; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBTouchIDCommands.m b/WebDriverAgentLib/Commands/FBTouchIDCommands.m index 9594dd6f5..d53746ce8 100644 --- a/WebDriverAgentLib/Commands/FBTouchIDCommands.m +++ b/WebDriverAgentLib/Commands/FBTouchIDCommands.m @@ -17,14 +17,17 @@ @implementation FBTouchIDCommands + (NSArray *)routes { return @[ - [[FBRoute POST:@"/wda/touch_id"] respondWithBlock: ^ id (FBRouteRequest *request) { - BOOL isMatch = [request.arguments[@"match"] boolValue]; - if (![[XCUIDevice sharedDevice] fb_fingerTouchShouldMatch:isMatch]) { - return FBResponseWithUnknownErrorFormat(@"Cannot perform Touch Id %@match", isMatch ? @"" : @"non-"); - } - return FBResponseWithOK(); - }], + [[FBRoute POST:@"/wda/touch_id"] respondWithTarget:self action:@selector(handleFingerTouchShouldMatch:)], ]; } ++ (id)handleFingerTouchShouldMatch:(FBRouteRequest *)request +{ + BOOL isMatch = [request.arguments[@"match"] boolValue]; + if (![[XCUIDevice sharedDevice] fb_fingerTouchShouldMatch:isMatch]) { + return FBResponseWithUnknownErrorFormat(@"Cannot perform Touch Id %@match", isMatch ? @"" : @"non-"); + } + return FBResponseWithOK(); +} + @end diff --git a/WebDriverAgentLib/Commands/FBVideoCommands.h b/WebDriverAgentLib/Commands/FBVideoCommands.h index a3e7a0a65..75c60d5b5 100644 --- a/WebDriverAgentLib/Commands/FBVideoCommands.h +++ b/WebDriverAgentLib/Commands/FBVideoCommands.h @@ -14,6 +14,10 @@ NS_ASSUME_NONNULL_BEGIN @interface FBVideoCommands : NSObject ++ (id)handleStartVideoRecording:(FBRouteRequest *)request; ++ (id)handleStopVideoRecording:(FBRouteRequest *)request; ++ (id)handleGetVideoRecording:(FBRouteRequest *)request; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/QAWolf/ExecuteWDACommand.h b/WebDriverAgentLib/QAWolf/ExecuteWDACommand.h new file mode 100644 index 000000000..33576d8c8 --- /dev/null +++ b/WebDriverAgentLib/QAWolf/ExecuteWDACommand.h @@ -0,0 +1,15 @@ +// +// WDACommandExecute.h +// ios-agent +// +// Created by Elton Carreiro on 28/08/25. +// + +#import +#import +#import + +typedef FBResponseJSONPayload * _Nonnull (^WDACommand)(void); + +/// Runs a block safely, catching NSException and mapping it to FBCommandStatus +FBResponseJSONPayload * _Nonnull ExecuteWDACommand(WDACommand _Nonnull block); diff --git a/WebDriverAgentLib/QAWolf/ExecuteWDACommand.m b/WebDriverAgentLib/QAWolf/ExecuteWDACommand.m new file mode 100644 index 000000000..d504e0163 --- /dev/null +++ b/WebDriverAgentLib/QAWolf/ExecuteWDACommand.m @@ -0,0 +1,20 @@ +// +// ExecuteWDACommand.m +// ios-agent +// +// Created by Elton Carreiro on 28/08/25. +// + +#import "ExecuteWDACommand.h" +#import "HandleWDACommandException.h" +#import + +FBResponseJSONPayload * _Nonnull ExecuteWDACommand(WDACommand block) { + @try { + // Run the block (ignore result here, or change signature to return both) + return block(); + } + @catch (NSException *exception) { + return FBCommandStatusForException(exception); + } +} diff --git a/WebDriverAgentLib/QAWolf/HandleWDACommandException.h b/WebDriverAgentLib/QAWolf/HandleWDACommandException.h new file mode 100644 index 000000000..c06d9e1c6 --- /dev/null +++ b/WebDriverAgentLib/QAWolf/HandleWDACommandException.h @@ -0,0 +1,15 @@ +// +// HandleWDACommandException.h +// ios-agent +// +// Created by Elton Carreiro on 27/08/25. +// + +#import +#import +#import +#import +#import + +// Declare the function so it can be imported into Swift +FBResponseJSONPayload *FBCommandStatusForException(NSException *exception); diff --git a/WebDriverAgentLib/QAWolf/HandleWDACommandException.m b/WebDriverAgentLib/QAWolf/HandleWDACommandException.m new file mode 100644 index 000000000..e7f426da5 --- /dev/null +++ b/WebDriverAgentLib/QAWolf/HandleWDACommandException.m @@ -0,0 +1,52 @@ +// +// HandleWDACommandException.m +// ios-agent +// +// Created by Elton Carreiro on 27/08/25. +// + +#import +#import +#import +#import + + +FBResponseJSONPayload *FBCommandStatusForException(NSException *exception) { + FBCommandStatus *commandStatus; + NSString *traceback = [NSString stringWithFormat:@"%@", exception.callStackSymbols]; + + if ([exception.name isEqualToString:FBSessionDoesNotExistException]) { + commandStatus = [FBCommandStatus noSuchDriverErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBInvalidArgumentException] + || [exception.name isEqualToString:FBElementAttributeUnknownException] + || [exception.name isEqualToString:FBApplicationMissingException]) { + commandStatus = [FBCommandStatus invalidArgumentErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBApplicationCrashedException] + || [exception.name isEqualToString:FBApplicationDeadlockDetectedException]) { + commandStatus = [FBCommandStatus invalidElementStateErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBInvalidXPathException] + || [exception.name isEqualToString:FBClassChainQueryParseException]) { + commandStatus = [FBCommandStatus invalidSelectorErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBElementNotVisibleException]) { + commandStatus = [FBCommandStatus elementNotVisibleErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBStaleElementException]) { + commandStatus = [FBCommandStatus staleElementReferenceErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBTimeoutException]) { + commandStatus = [FBCommandStatus timeoutErrorWithMessage:exception.reason + traceback:traceback]; + } else if ([exception.name isEqualToString:FBSessionCreationException]) { + commandStatus = [FBCommandStatus sessionNotCreatedError:exception.reason + traceback:traceback]; + } else { + commandStatus = [FBCommandStatus unknownErrorWithMessage:exception.reason + traceback:traceback]; + } + + return FBResponseWithStatus(commandStatus); +} diff --git a/WebDriverAgentLib/QAWolf/QAWObjCExceptionHandler.h b/WebDriverAgentLib/QAWolf/QAWObjCExceptionHandler.h new file mode 100644 index 000000000..139e71407 --- /dev/null +++ b/WebDriverAgentLib/QAWolf/QAWObjCExceptionHandler.h @@ -0,0 +1,31 @@ +// +// ObjCExceptionHandler.h +// WebDriverAgent +// +// Created by E. P. CARREIRO - SERVICOS DE INFORMATICA on 06/01/26. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** Domain used for NSErrors produced by QAWObjCExceptionHandler. */ +extern NSString* const QAWObjCExceptionDomain; + +typedef id _Nullable (^ObjCExceptionBlock)(void); + +@interface QAWObjCExceptionHandler : NSObject + +/** + Executes a block and captures any Objective-C exceptions. + + @param block The block to execute + @param error Output parameter for any exception that occurs + @return The return value from the block, or nil if an exception occurred + */ ++ (id _Nullable)tryBlock:(ObjCExceptionBlock)block + error:(NSError * _Nullable * _Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/QAWolf/QAWObjCExceptionHandler.m b/WebDriverAgentLib/QAWolf/QAWObjCExceptionHandler.m new file mode 100644 index 000000000..e9725cb2f --- /dev/null +++ b/WebDriverAgentLib/QAWolf/QAWObjCExceptionHandler.m @@ -0,0 +1,40 @@ +// +// ObjCExceptionHandler.m +// WebDriverAgent +// +// Created by E. P. CARREIRO - SERVICOS DE INFORMATICA on 06/01/26. +// +#import "QAWObjCExceptionHandler.h" + +NSString* const QAWObjCExceptionDomain = @"QAWObjCExceptionDomain"; + +@implementation QAWObjCExceptionHandler + ++ (id _Nullable)tryBlock:(ObjCExceptionBlock)block + error:(NSError * _Nullable * _Nullable)error { + @try { + return block(); + } + @catch (NSException *exception) { + if (error != NULL) { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + userInfo[NSLocalizedDescriptionKey] = exception.reason ?: @"Unknown exception"; + userInfo[@"exceptionName"] = exception.name; + + if (exception.userInfo) { + userInfo[@"exceptionUserInfo"] = exception.userInfo; + } + + if (exception.callStackSymbols) { + userInfo[@"callStackSymbols"] = exception.callStackSymbols; + } + + *error = [NSError errorWithDomain:QAWObjCExceptionDomain + code:-1 + userInfo:userInfo]; + } + return nil; + } +} + +@end diff --git a/WebDriverAgentLib/QAWolf/QAWSnapshotResult.h b/WebDriverAgentLib/QAWolf/QAWSnapshotResult.h new file mode 100644 index 000000000..98dd3928d --- /dev/null +++ b/WebDriverAgentLib/QAWolf/QAWSnapshotResult.h @@ -0,0 +1,18 @@ +// +// QAWSnapshotResult.h +// WebDriverAgent +// +// Created by E. P. CARREIRO - SERVICOS DE INFORMATICA on 09/09/25. +// + +#import +#import "FBXCElementSnapshot.h" + +@interface QAWSnapshotResult : NSObject +@property (nonatomic, strong, nullable) id snapshot; +@property (nonatomic, strong, nullable) NSException *exception; +@property (nonatomic, readonly) BOOL isSuccess; + ++ (instancetype _Nonnull )resultWithSnapshot:(id _Nonnull)snapshot; ++ (instancetype _Nonnull )resultWithException:(NSException * _Nonnull)exception; +@end diff --git a/WebDriverAgentLib/QAWolf/QAWSnapshotResult.m b/WebDriverAgentLib/QAWolf/QAWSnapshotResult.m new file mode 100644 index 000000000..4f763950e --- /dev/null +++ b/WebDriverAgentLib/QAWolf/QAWSnapshotResult.m @@ -0,0 +1,43 @@ +// +// QAWSnapshotResult.m.m +// WebDriverAgent +// +// Created by E. P. CARREIRO - SERVICOS DE INFORMATICA on 09/09/25. +// + +#import "QAWSnapshotResult.h" +#import + +@implementation QAWSnapshotResult + +- (BOOL)isSuccess { + return self.snapshot != nil && self.exception == nil; +} + +- (instancetype)initWithSnapshot:(id _Nonnull)snapshot { + self = [super init]; + if (self) { + _snapshot = snapshot; + _exception = nil; + } + return self; +} + +- (instancetype)initWithException:(NSException *)exception { + self = [super init]; + if (self) { + _snapshot = nil; + _exception = exception; + } + return self; +} + ++ (instancetype)resultWithSnapshot:(id _Nonnull)snapshot { + return [[self alloc] initWithSnapshot:snapshot]; +} + ++ (instancetype)resultWithException:(NSException *)exception { + return [[self alloc] initWithException:exception]; +} + +@end diff --git a/WebDriverAgentLib/QAWolf/QAWXML.h b/WebDriverAgentLib/QAWolf/QAWXML.h new file mode 100644 index 000000000..be4d73707 --- /dev/null +++ b/WebDriverAgentLib/QAWolf/QAWXML.h @@ -0,0 +1,46 @@ +// +// XCUIElementSnapshot+XML.h +// WebDriverAgent +// +// Created by E. P. CARREIRO - SERVICOS DE INFORMATICA on 22/09/25. +// + +#import +#import "FBXCElementSnapshot.h" + +@class FBXMLGenerationOptions; + +@interface SnapshotWithId : NSObject + +@property (nonatomic, strong) id _Nonnull snapshot; +@property (nonatomic, strong) NSString * _Nonnull id; + ++ (instancetype _Nonnull) wrap:(id _Nonnull)snapshot; + +@end + +@interface XMLWithParentId : NSObject +@property (nonatomic, strong) NSString * _Nonnull parentId; +@property (nonatomic, strong) NSString * _Nonnull xmlString; + ++ (instancetype _Nonnull )initWithParentId:(NSString * _Nonnull)parentId + xmlString:(NSString *_Nonnull) xmlString; + +@end + +@interface XMLRepresentationAndLeaves : NSObject +@property (nonatomic, strong) XMLWithParentId * _Nonnull xml; +@property (nonatomic, strong) NSArray * _Nonnull leaves; + ++ (instancetype _Nonnull )initWithXMLRepresentation:(XMLWithParentId * _Nonnull)xml + leaves:(NSArray * _Nonnull) leaves; + +@end + +@interface QAWXML : NSObject + ++ (XMLRepresentationAndLeaves * _Nonnull)generateXMLStringAndLeaves:(SnapshotWithId * _Nonnull)root + withOptions:(nullable FBXMLGenerationOptions *)options + withMaxDepth:(int)maxDepth + parentLeafId:(nullable NSString *)parentLeafId; +@end diff --git a/WebDriverAgentLib/QAWolf/QAWXML.m b/WebDriverAgentLib/QAWolf/QAWXML.m new file mode 100644 index 000000000..205b9e8bc --- /dev/null +++ b/WebDriverAgentLib/QAWolf/QAWXML.m @@ -0,0 +1,320 @@ +// +// XCUIElementSnapshot+XML.m +// WebDriverAgent +// +// Created by E. P. CARREIRO - SERVICOS DE INFORMATICA on 22/09/25. +// + +#import +#import "FBElement.h" +#import "QAWXML.h" +#import "FBXPath.h" +#import "FBConfiguration.h" +#import "FBLogger.h" +#import "FBXMLGenerationOptions.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBWebDriverAttributes.h" + + +const static char *_UTF8Encoding = "UTF-8"; + +static NSString *const kXMLIndexPathKey = @"private_indexPath"; +static NSString *const topNodeIndexPath = @"top"; + +@implementation QAWXML + ++ (XMLRepresentationAndLeaves * _Nonnull)generateXMLStringAndLeaves:(SnapshotWithId * _Nonnull)root + withOptions:(nullable FBXMLGenerationOptions *)options + withMaxDepth:(int)maxDepth + parentLeafId:(nullable NSString *)parentLeafId; +{ + xmlDocPtr doc; + xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); + int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc]; + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + + BOOL hasScope = nil != options.scope && [options.scope length] > 0; + if (hasScope) { + rc = xmlTextWriterStartElement(writer, + (xmlChar *)[[FBXPath safeXmlStringWithString:options.scope] UTF8String]); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc]; + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + } + + // The batched page-source path (production uses use_batch=true) emits + // `visible` through the same fb_isVisible chain as the classic path. Warm + // here so the descendant-cache short-circuit fires on this path too. + // Gated by the same FBConfiguration.preWarmPageSource setting as the + // classic path so a single toggle (via /appium/settings or the + // appium:settings capability) controls both. + BOOL preWarm = [FBConfiguration preWarmPageSource]; + [FBLogger logFmt:@"[QA_WOLF] [INFO] [VisCache] (QAWXML) preWarmPageSource=%@", + preWarm ? @"YES" : @"NO"]; + if (preWarm) { + NSDate *warmStart = [NSDate date]; + NSUInteger leavesWarmed = [FBXPath warmVisibilityCacheForSnapshot:root.snapshot]; + [FBLogger logFmt:@"[QA_WOLF] [INFO] [VisCache] (QAWXML) Warmed %lu leaves in %.3fs", + (unsigned long)leavesWarmed, + ABS([warmStart timeIntervalSinceNow])]; + } + + NSMutableArray *leaves = [NSMutableArray array]; + if (rc >= 0) { + // If 'includeHittableInPageSource' setting is enabled, then use native snapshots + // to calculate a more accurate value for the 'hittable' attribute. + NSArray * newLeaves = [self xmlRepresentationWithRootElement:root + writer:writer + elementStore:nil + query:nil + excludingAttributes:options.excludedAttributes + maxDepth:maxDepth + parentLeafId:parentLeafId]; + + [leaves addObjectsFromArray:newLeaves]; + } + + if (rc >= 0 && hasScope) { + rc = xmlTextWriterEndElement(writer); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc]; + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + } + + if (rc >= 0) { + rc = xmlTextWriterEndDocument(writer); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc]; + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + } + + if (rc < 0) { + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + + int buffersize; + xmlChar *xmlbuff; + xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1); + xmlFreeTextWriter(writer); + xmlFreeDoc(doc); + NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding]; + xmlFree(xmlbuff); + + XMLWithParentId *xmlWithParentId = [XMLWithParentId initWithParentId:parentLeafId xmlString:result]; + XMLRepresentationAndLeaves *xmlRepresentationAndLeaves = [XMLRepresentationAndLeaves initWithXMLRepresentation:xmlWithParentId leaves:leaves]; + return xmlRepresentationAndLeaves; +} + ++ (NSArray * _Nonnull)xmlRepresentationWithRootElement:(SnapshotWithId *)root + writer:(xmlTextWriterPtr)writer + elementStore:(nullable NSMutableDictionary *)elementStore + query:(nullable NSString*)query + excludingAttributes:(nullable NSArray *)excludedAttributes + maxDepth:(int)maxDepth + parentLeafId:(nullable NSString *)parentLeafId; +{ + // Trying to be smart here and only including attributes, that were asked in the query, to the resulting document. + // This may speed up the lookup significantly in some cases + NSMutableSet *includedAttributes; + if (nil == query) { + includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes]; + if (!FBConfiguration.includeHittableInPageSource) { + // The hittable attribute is expensive to calculate for each snapshot item + // thus we only include it when requested explicitly + [includedAttributes removeObject:FBHittableAttribute.class]; + } + if (!FBConfiguration.includeNativeFrameInPageSource) { + // Include nativeFrame only when requested + [includedAttributes removeObject:FBNativeFrameAttribute.class]; + } + if (!FBConfiguration.includeMinMaxValueInPageSource) { + // minValue/maxValue are retrieved from private APIs and may be slow on deep trees + [includedAttributes removeObject:FBMinValueAttribute.class]; + [includedAttributes removeObject:FBMaxValueAttribute.class]; + } + if (nil != excludedAttributes) { + for (NSString *excludedAttributeName in excludedAttributes) { + for (Class supportedAttribute in FBElementAttribute.supportedAttributes) { + if ([[supportedAttribute name] caseInsensitiveCompare:excludedAttributeName] == NSOrderedSame) { + [includedAttributes removeObject:supportedAttribute]; + break; + } + } + } + } + } else { + includedAttributes = [self.class elementAttributesWithXPathQuery:query].mutableCopy; + } + [FBLogger logFmt:@"The following attributes were requested to be included into the XML: %@", includedAttributes]; + + return [self writeXmlWithRootElementSnapshot:root + indexPath:(elementStore != nil ? topNodeIndexPath : nil) + elementStore:elementStore + includedAttributes:includedAttributes.copy + writer:writer + maxDepth:maxDepth + currentDepth:0 + parentLeafId:parentLeafId]; +} + ++ (NSArray * _Nonnull)writeXmlWithRootElementSnapshot:(SnapshotWithId *)root + indexPath:(nullable NSString *)indexPath + elementStore:(nullable NSMutableDictionary *)elementStore + includedAttributes:(nullable NSSet *)includedAttributes + writer:(xmlTextWriterPtr)writer + maxDepth:(int)maxDepth + currentDepth:(int)currentDepth + parentLeafId:(nullable NSString *)parentLeafId; +{ + [FBLogger logFmt:@"Current depth: '%d'. Max depth: '%d'", currentDepth, maxDepth]; + NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil); + + NSArray> *children = root.snapshot.children; + + if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) { + [elementStore setObject:root forKey:topNodeIndexPath]; + } + + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root.snapshot]; + int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc]; + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + + NSMutableArray *leaves = [NSMutableArray array]; + if (nil != parentLeafId && currentDepth == 0) { + int rc = xmlTextWriterWriteAttribute(writer, + (xmlChar *)[[FBXPath safeXmlStringWithString:@"parent-leaf-id"] UTF8String], + (xmlChar *)[[FBXPath safeXmlStringWithString:parentLeafId] UTF8String]); + + if (rc < 0) { + [FBLogger logFmt:@"Failed to write element parent-leaf-id. Error code: %d", rc]; + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + } + + rc = [FBXPath recordElementAttributes:writer + forElement:root.snapshot + indexPath:indexPath + includedAttributes:includedAttributes]; + if (rc < 0) { + [FBLogger logFmt:@"Failed to write element attributes. Error code: %d", rc]; + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + + if (currentDepth == maxDepth) { + int rc = xmlTextWriterWriteAttribute(writer, + (xmlChar *)[[FBXPath safeXmlStringWithString:@"leaf-id"] UTF8String], + (xmlChar *)[[FBXPath safeXmlStringWithString:root.id] UTF8String]); + if (rc < 0) { + [FBLogger logFmt:@"Failed to write element leaf-id. Error code: %d", rc]; + [QAWXML throwXMLErrorWithErrorCode:rc]; + } + + [leaves addObject:root]; + } else { + for (NSUInteger i = 0; i < [children count]; i++) { + @autoreleasepool { + id childSnapshot = [children objectAtIndex:i]; + NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil; + if (elementStore != nil && newIndexPath != nil) { + [elementStore setObject:childSnapshot forKey:(id)newIndexPath]; + } + + SnapshotWithId *childSnapshotElement = [SnapshotWithId wrap:childSnapshot]; + + NSArray *newLeaves = [self writeXmlWithRootElementSnapshot:childSnapshotElement + indexPath:newIndexPath + elementStore:elementStore + includedAttributes:includedAttributes + writer:writer + maxDepth:maxDepth + currentDepth:currentDepth + 1 + parentLeafId:parentLeafId]; + + [leaves addObjectsFromArray:newLeaves]; + } + } + } + + rc = xmlTextWriterEndElement(writer); + if (rc < 0) { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc]; + [QAWXML throwXMLErrorWithErrorCode:@(rc)]; + } + + return [leaves copy]; +} + ++ throwXMLErrorWithErrorCode:(int)rc { + @throw [NSException exceptionWithName:@"XMLErrorException" reason:[NSString stringWithFormat:@"Got XML error code %d", rc] userInfo: @{NSLocalizedDescriptionKey: @"XML Error"}]; +} + +@end + +@implementation SnapshotWithId + +- (instancetype)initWithSnapshot:(id _Nonnull)snapshot + withId:(NSString *) id{ + self = [super init]; + if (self) { + _snapshot = snapshot; + _id = id; + } + + return self; +} + ++ (instancetype) wrap:(id _Nonnull)snapshot { + return [[self alloc] initWithSnapshot:snapshot withId:[[NSUUID UUID] UUIDString]]; +} + +@end + +@implementation XMLWithParentId + +- (instancetype)init:(NSString *)parentId xmlString:(NSString *)xmlString { + self = [super init]; + if (self) { + _parentId = parentId; + _xmlString = xmlString; + } + return self; +} + ++ (instancetype)initWithParentId:(NSString *)parentId xmlString:(NSString *)xmlString { + return [[self alloc] init:parentId xmlString:xmlString]; +} + +@end + + +@implementation XMLRepresentationAndLeaves + + +- (instancetype)init:(XMLWithParentId *)xml leaves:(NSArray *)leaves { + self = [super init]; + if (self) { + _xml = xml; + _leaves = leaves; + } + return self; +} + + ++ (instancetype)initWithXMLRepresentation:(XMLWithParentId *)xml leaves:(NSArray *)leaves { + return [[self alloc] init:xml leaves:leaves]; +} + +@end + + diff --git a/WebDriverAgentLib/QAWolf/XCUIElement+QAWSnapshotUtilities.h b/WebDriverAgentLib/QAWolf/XCUIElement+QAWSnapshotUtilities.h new file mode 100644 index 000000000..e05aa2b45 --- /dev/null +++ b/WebDriverAgentLib/QAWolf/XCUIElement+QAWSnapshotUtilities.h @@ -0,0 +1,8 @@ +#import "XCUIElement+FBUtilities.h" +#import "QAWSnapshotResult.h" + +@interface XCUIElement (QAWSnapshotUtilities) + +- (QAWSnapshotResult * _Nonnull)qaw_snapshot; + +@end diff --git a/WebDriverAgentLib/QAWolf/XCUIElement+QAWSnapshotUtilities.m b/WebDriverAgentLib/QAWolf/XCUIElement+QAWSnapshotUtilities.m new file mode 100644 index 000000000..6f54f82a2 --- /dev/null +++ b/WebDriverAgentLib/QAWolf/XCUIElement+QAWSnapshotUtilities.m @@ -0,0 +1,82 @@ +#import +#import "FBConfiguration.h" +#import "XCUIElement+FBUtilities.h" +#import "QAWSnapshotResult.h" +#import "FBXPath.h" +#import "XCUIElement+FBUtilities.h" +#import "XCUIElement.h" +#import "FBElementHelpers.h" +#import "FBLogger.h" + +@implementation XCUIElement (QAWSnapshotUtilities) + +- (QAWSnapshotResult * _Nonnull)qaw_snapshot { + + + id snapshot = nil; + NSException *caughtException = nil; + + id root = (id)self; + + @try { + [FBLogger logFmt:@"Waiting for element to become stable..."]; + [self waitUntilStableWithElement:root]; + [FBLogger logFmt:@"Element is stable. Taking snapshot..."]; + snapshot = [self snapshotWithRoot:root + useNative:FBConfiguration.includeHittableInPageSource]; + [FBLogger logFmt:@"Snapshot taken."]; + } + @catch (NSException *exception) { + [FBLogger logFmt:@"Snapshot failed. Wrapping exception and returning it to caller."]; + caughtException = exception; + } + + if (caughtException) { + return [QAWSnapshotResult resultWithException:caughtException]; + } else { + return [QAWSnapshotResult resultWithSnapshot:snapshot]; + } +} + +/** + Extracted from FBXPath. Waits for element to be stable based on session configuration. + */ +- (void)waitUntilStableWithElement:(id)root +{ + if ([root isKindOfClass:XCUIElement.class]) { + [FBLogger logFmt:@"Element is of XCUIElement type. Waiting for element stability. Timeout: %f", FBConfiguration.animationCoolOffTimeout]; + // If the app is not idle state while we retrieve the visiblity state + // then the snapshot retrieval operation might freeze and time out + [[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + } else { + [FBLogger logFmt:@"Element is not of XCUIElement type. Skipping stability wait."]; + } +} + +/** + Extracted from FBXPath. Takes element snapshot based on session configuration. + */ +- (id)snapshotWithRoot:(id)root + useNative:(BOOL)useNative +{ + [FBLogger logFmt:@"Started snapshot process."]; + if (![root isKindOfClass:XCUIElement.class]) { + [FBLogger logFmt:@"Element is already a snapshot.It has type FBXCElementSnapshot, skipping."]; + return (id)root; + } + + if (useNative) { + [FBLogger logFmt:@"useNative is true, taking native snapshot."]; + return [(XCUIElement *)root fb_nativeSnapshot]; + } + + if ([root isKindOfClass:XCUIApplication.class]) { + [FBLogger logFmt:@"Taking standard snapshot"]; + return [(XCUIElement *)root fb_standardSnapshot]; + } + + [FBLogger logFmt:@"Taking custom snapshot"]; + return [(XCUIElement *)root fb_customSnapshot]; +} + +@end diff --git a/WebDriverAgentLib/QAWolf/XCUIElementSnapshotParser.h b/WebDriverAgentLib/QAWolf/XCUIElementSnapshotParser.h new file mode 100644 index 000000000..ea72c46bf --- /dev/null +++ b/WebDriverAgentLib/QAWolf/XCUIElementSnapshotParser.h @@ -0,0 +1,19 @@ +// +// XCUIElementSnapshot+FBXCElementSnapshot.h +// WebDriverAgent +// +// Created by E. P. CARREIRO - SERVICOS DE INFORMATICA on 22/09/25. +// + +#import +#import "FBXCElementSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElementSnapshotParser : NSObject + ++ (id) toFBSnapshot:(id) snapshot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/QAWolf/XCUIElementSnapshotParser.m b/WebDriverAgentLib/QAWolf/XCUIElementSnapshotParser.m new file mode 100644 index 000000000..316e58edc --- /dev/null +++ b/WebDriverAgentLib/QAWolf/XCUIElementSnapshotParser.m @@ -0,0 +1,18 @@ +// +// XCUIElementSnapshot+FBXCElementSnapshotWrapper.m.h +// WebDriverAgent +// +// Created by E. P. CARREIRO - SERVICOS DE INFORMATICA on 22/09/25. +// + + + +#import "XCUIElementSnapshotParser.h" +@implementation XCUIElementSnapshotParser + ++ (id) toFBSnapshot:(id) snapshot { + return (id) snapshot; +} + +@end + diff --git a/WebDriverAgentLib/Routing/FBResponseJSONPayload.h b/WebDriverAgentLib/Routing/FBResponseJSONPayload.h index 8c863e999..f39fa73e7 100644 --- a/WebDriverAgentLib/Routing/FBResponseJSONPayload.h +++ b/WebDriverAgentLib/Routing/FBResponseJSONPayload.h @@ -18,6 +18,9 @@ NS_ASSUME_NONNULL_BEGIN */ @interface FBResponseJSONPayload : NSObject +// Raw JSON dictionary that will be serialised when the payload is dispatched. +@property (nonatomic, copy, readonly) NSDictionary *dictionary; + /** Initializer for JSON respond that converts given 'dictionary' to JSON */ diff --git a/WebDriverAgentLib/Routing/FBResponseJSONPayload.m b/WebDriverAgentLib/Routing/FBResponseJSONPayload.m index 8782b8e9e..df0063867 100644 --- a/WebDriverAgentLib/Routing/FBResponseJSONPayload.m +++ b/WebDriverAgentLib/Routing/FBResponseJSONPayload.m @@ -14,7 +14,7 @@ @interface FBResponseJSONPayload () -@property (nonatomic, copy, readonly) NSDictionary *dictionary; +@property (nonatomic, copy, readwrite) NSDictionary *dictionary; @property (nonatomic, readonly) HTTPStatusCode httpStatusCode; @end diff --git a/WebDriverAgentLib/Routing/FBRouteRequest.h b/WebDriverAgentLib/Routing/FBRouteRequest.h index c7938d5d6..a03e1e757 100644 --- a/WebDriverAgentLib/Routing/FBRouteRequest.h +++ b/WebDriverAgentLib/Routing/FBRouteRequest.h @@ -34,6 +34,19 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)routeRequestWithURL:(NSURL *)URL parameters:(NSDictionary *)parameters arguments:(NSDictionary *)arguments; +/** + Convenience constructor for request that also sets the associated session. + + @param URL Request URL + @param parameters Path/query parameters + @param arguments JSON body arguments + @param session The FBSession instance to associate with the request. May be nil. + */ ++ (instancetype)routeRequestWithURL:(NSURL *)URL + parameters:(NSDictionary *)parameters + arguments:(NSDictionary *)arguments + session:(nullable FBSession *)session; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBRouteRequest.m b/WebDriverAgentLib/Routing/FBRouteRequest.m index b8656d285..b38091937 100644 --- a/WebDriverAgentLib/Routing/FBRouteRequest.m +++ b/WebDriverAgentLib/Routing/FBRouteRequest.m @@ -19,6 +19,16 @@ + (instancetype)routeRequestWithURL:(NSURL *)URL parameters:(NSDictionary *)para return request; } ++ (instancetype)routeRequestWithURL:(NSURL *)URL + parameters:(NSDictionary *)parameters + arguments:(NSDictionary *)arguments + session:(FBSession *)session +{ + FBRouteRequest *request = [self routeRequestWithURL:URL parameters:parameters arguments:arguments]; + request.session = session; + return request; +} + - (NSString *)description { return [NSString stringWithFormat: diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgentLib/Utilities/FBConfiguration.h index 94ff68ee0..9e4c8b996 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.h +++ b/WebDriverAgentLib/Utilities/FBConfiguration.h @@ -413,6 +413,22 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + (void)setIncludeCustomActionsInPageSource:(BOOL)enabled; + (BOOL)includeCustomActionsInPageSource; +/** + * Whether to pre-warm the snapshot's visibility cache before page-source XML + * generation. + * + * When enabled, the XML generator walks the leaves of the snapshot tree and + * resolves each leaf's `visible` attribute up front. Internal nodes then + * short-circuit in `fb_hasVisibleDescendants` (they find a cached-visible + * descendant and skip the synchronous AX-framework IPC). Skipping internal + * nodes during the warm pass avoids redundant `_allDescendants` traversals. + * Enabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setPreWarmPageSource:(BOOL)enabled; ++ (BOOL)preWarmPageSource; + /** * Whether to enforce the use of custom snapshots instead of standard snapshots. * When enabled, fb_customSnapshot is always invoked instead of fb_standardSnapshot diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgentLib/Utilities/FBConfiguration.m index fa856d967..de63a6223 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.m +++ b/WebDriverAgentLib/Utilities/FBConfiguration.m @@ -65,6 +65,7 @@ static BOOL FBShouldIncludeNativeAccessibilityElementInPageSource = NO; static BOOL FBShouldIncludeMinMaxValueInPageSource = NO; static BOOL FBShouldIncludeCustomActionsInPageSource = NO; +static BOOL FBShouldPreWarmPageSource = YES; static BOOL FBShouldEnforceCustomSnapshots = NO; @implementation FBConfiguration @@ -705,6 +706,16 @@ + (void)setIncludeCustomActionsInPageSource:(BOOL)enabled FBShouldIncludeCustomActionsInPageSource = enabled; } ++ (void)setPreWarmPageSource:(BOOL)enabled +{ + FBShouldPreWarmPageSource = enabled; +} + ++ (BOOL)preWarmPageSource +{ + return FBShouldPreWarmPageSource; +} + + (BOOL)includeCustomActionsInPageSource { return FBShouldIncludeCustomActionsInPageSource; diff --git a/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgentLib/Utilities/FBSettings.h index c3f1523e2..3a95e0801 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.h +++ b/WebDriverAgentLib/Utilities/FBSettings.h @@ -44,6 +44,7 @@ extern NSString *const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE; extern NSString *const FB_SETTING_INCLUDE_NATIVE_ACCESSIBILITY_ELEMENT_IN_PAGE_SOURCE; extern NSString *const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE; extern NSString *const FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_PRE_WARM_PAGE_SOURCE; extern NSString *const FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS; NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgentLib/Utilities/FBSettings.m index b2b219d85..06c9a683b 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.m +++ b/WebDriverAgentLib/Utilities/FBSettings.m @@ -40,4 +40,5 @@ NSString* const FB_SETTING_INCLUDE_NATIVE_ACCESSIBILITY_ELEMENT_IN_PAGE_SOURCE = @"includeNativeAccessibilityElementInPageSource"; NSString* const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE = @"includeMinMaxValueInPageSource"; NSString* const FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE = @"includeCustomActionsInPageSource"; +NSString* const FB_SETTING_PRE_WARM_PAGE_SOURCE = @"preWarmPageSource"; NSString* const FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS = @"enforceCustomSnapshots"; diff --git a/WebDriverAgentLib/Utilities/FBSettingsHandler.m b/WebDriverAgentLib/Utilities/FBSettingsHandler.m index 641687dae..6786d881a 100644 --- a/WebDriverAgentLib/Utilities/FBSettingsHandler.m +++ b/WebDriverAgentLib/Utilities/FBSettingsHandler.m @@ -174,6 +174,10 @@ @implementation FBSettingsHandler [FBConfiguration setIncludeCustomActionsInPageSource:[value boolValue]]; return nil; }; + map[FB_SETTING_PRE_WARM_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) { + [FBConfiguration setPreWarmPageSource:[value boolValue]]; + return nil; + }; map[FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS] = ^FBCommandStatus *(FBSession *session, id value) { [FBConfiguration setEnforceCustomSnapshots:[value boolValue]]; return nil; @@ -293,6 +297,9 @@ @implementation FBSettingsHandler map[FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE] = ^id(FBSession *session) { return @([FBConfiguration includeCustomActionsInPageSource]); }; + map[FB_SETTING_PRE_WARM_PAGE_SOURCE] = ^id(FBSession *session) { + return @([FBConfiguration preWarmPageSource]); + }; map[FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS] = ^id(FBSession *session) { return @([FBConfiguration enforceCustomSnapshots]); }; diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h index 247f056bd..85ce1d091 100644 --- a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h @@ -7,7 +7,7 @@ */ #import -#import "XCPointerEvent.h" +#import @class FBXCElementSnapshot; diff --git a/WebDriverAgentLib/Utilities/FBXPath.h b/WebDriverAgentLib/Utilities/FBXPath.h index 1135c7228..6bfe6bd04 100644 --- a/WebDriverAgentLib/Utilities/FBXPath.h +++ b/WebDriverAgentLib/Utilities/FBXPath.h @@ -54,6 +54,161 @@ NS_ASSUME_NONNULL_BEGIN + (nullable NSString *)xmlStringWithRootElement:(id)root options:(nullable FBXMLGenerationOptions *)options; +/** + Exported by QAWolf + */ ++ (NSSet *)elementAttributesWithXPathQuery:(NSString *)query; + + +/** + Exported by QAWolf + */ ++ (int)recordElementAttributes:(xmlTextWriterPtr)writer + forElement:(id)element + indexPath:(nullable NSString *)indexPath + includedAttributes:(nullable NSSet *)includedAttributes; + +/** + Exported by QAWolf + */ ++ (nullable NSString *)safeXmlStringWithString:(NSString *)str; + +/** + Resolves visibility on leaf snapshots before XML generation so the + descendant-cache short-circuit in `fb_hasVisibleDescendants` fires for + internal nodes, instead of every node paying the synchronous AX-framework IPC. + + Exposed for the QAWXML batched page-source path. Gated by the + `preWarmPageSource` session setting at call sites. + + @return Number of leaves that were warmed (used for diagnostic logging). + */ ++ (NSUInteger)warmVisibilityCacheForSnapshot:(nullable id)snapshot; + +@end + +/** + Exported by QAWolf + */ + +@interface FBElementAttribute : NSObject + +@property (nonatomic, readonly) id element; + ++ (nonnull NSString *)name; ++ (nullable NSString *)valueForElement:(id)element; + ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element; ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value; + ++ (NSArray *)supportedAttributes; + +@end + +@interface FBTypeAttribute : FBElementAttribute + +@end + +@interface FBValueAttribute : FBElementAttribute + +@end + +@interface FBNameAttribute : FBElementAttribute + +@end + +@interface FBLabelAttribute : FBElementAttribute + +@end + +@interface FBEnabledAttribute : FBElementAttribute + +@end + +@interface FBVisibleAttribute : FBElementAttribute + +@end + +@interface FBAccessibleAttribute : FBElementAttribute + +@end + +@interface FBDimensionAttribute : FBElementAttribute + +@end + +@interface FBXAttribute : FBDimensionAttribute + @end +@interface FBYAttribute : FBDimensionAttribute + +@end + +@interface FBWidthAttribute : FBDimensionAttribute + +@end + +@interface FBHeightAttribute : FBDimensionAttribute + +@end + +@interface FBIndexAttribute : FBElementAttribute + +@end + +@interface FBHittableAttribute : FBElementAttribute + +@end + +@interface FBInternalIndexAttribute : FBElementAttribute + +@property (nonatomic, nonnull, readonly) NSString* indexValue; + +@end + +@interface FBApplicationBundleIdAttribute : FBElementAttribute + +@end + +@interface FBApplicationPidAttribute : FBElementAttribute + +@end + +@interface FBPlaceholderValueAttribute : FBElementAttribute + +@end + +@interface FBNativeFrameAttribute : FBElementAttribute + +@end + +@interface FBNativeAccessibilityElementAttribute : FBElementAttribute + +@end + +@interface FBTraitsAttribute : FBElementAttribute + +@end + +@interface FBMinValueAttribute : FBElementAttribute + +@end + +@interface FBMaxValueAttribute : FBElementAttribute + +@end + +@interface FBCustomActionsAttribute : FBElementAttribute + +@end + +#if TARGET_OS_TV + +@interface FBFocusedAttribute : FBElementAttribute + +@end + +#endif + NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgentLib/Utilities/FBXPath.m index 6e00daf7f..8acedae76 100644 --- a/WebDriverAgentLib/Utilities/FBXPath.m +++ b/WebDriverAgentLib/Utilities/FBXPath.m @@ -27,127 +27,6 @@ #import "FBXCAXClientProxy.h" #import "FBXCAccessibilityElement.h" - -@interface FBElementAttribute : NSObject - -@property (nonatomic, readonly) id element; - -+ (nonnull NSString *)name; -+ (nullable NSString *)valueForElement:(id)element; - -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element; -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value; - -+ (NSArray *)supportedAttributes; - -@end - -@interface FBTypeAttribute : FBElementAttribute - -@end - -@interface FBValueAttribute : FBElementAttribute - -@end - -@interface FBNameAttribute : FBElementAttribute - -@end - -@interface FBLabelAttribute : FBElementAttribute - -@end - -@interface FBEnabledAttribute : FBElementAttribute - -@end - -@interface FBVisibleAttribute : FBElementAttribute - -@end - -@interface FBAccessibleAttribute : FBElementAttribute - -@end - -@interface FBDimensionAttribute : FBElementAttribute - -@end - -@interface FBXAttribute : FBDimensionAttribute - -@end - -@interface FBYAttribute : FBDimensionAttribute - -@end - -@interface FBWidthAttribute : FBDimensionAttribute - -@end - -@interface FBHeightAttribute : FBDimensionAttribute - -@end - -@interface FBIndexAttribute : FBElementAttribute - -@end - -@interface FBHittableAttribute : FBElementAttribute - -@end - -@interface FBInternalIndexAttribute : FBElementAttribute - -@property (nonatomic, nonnull, readonly) NSString* indexValue; - -@end - -@interface FBApplicationBundleIdAttribute : FBElementAttribute - -@end - -@interface FBApplicationPidAttribute : FBElementAttribute - -@end - -@interface FBPlaceholderValueAttribute : FBElementAttribute - -@end - -@interface FBNativeFrameAttribute : FBElementAttribute - -@end - -@interface FBNativeAccessibilityElementAttribute : FBElementAttribute - -@end - -@interface FBTraitsAttribute : FBElementAttribute - -@end - -@interface FBMinValueAttribute : FBElementAttribute - -@end - -@interface FBMaxValueAttribute : FBElementAttribute - -@end - -@interface FBCustomActionsAttribute : FBElementAttribute - -@end - -#if TARGET_OS_TV - -@interface FBFocusedAttribute : FBElementAttribute - -@end - -#endif - const static char *_UTF8Encoding = "UTF-8"; static NSString *const kXMLIndexPathKey = @"private_indexPath"; @@ -172,6 +51,42 @@ + (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery detail:(nu return nil; } +/** + Walks the snapshot tree and resolves `wdVisible` (a.k.a. `fb_isVisible`) + only for **leaf** nodes — those without children. + + Why only leaves? The descendant-cache short-circuit in + `fb_hasVisibleDescendants` iterates `_allDescendants` and returns YES as soon + as it finds any descendant whose visibility is already cached as YES. The + descendant doesn't have to be a direct child or a leaf — it just has to be + cached. Warming the leaves guarantees that every internal node with at + least one visible leaf in its subtree will short-circuit. The IPC cost for + fully-invisible subtrees is unchanged either way, but skipping internal + nodes during the warm pass avoids redundant `_allDescendants` traversals + (one per internal node, ~O(n²) in aggregate). + + Side effects: mutates `snapshot.additionalAttributes` on each visited leaf + (via the cache backfill inside `fb_isVisible`). No throwing. + */ ++ (NSUInteger)warmVisibilityCacheForSnapshot:(nullable id)snapshot +{ + if (nil == snapshot) { + return 0; + } + NSArray *children = snapshot.children; + if (0 == children.count) { + // Leaf: warm here. Internal ancestors piggy-back on this via + // `fb_hasVisibleDescendants` finding the cached entry and short-circuiting. + (void)[FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdVisible; + return 1; + } + NSUInteger leavesWarmed = 0; + for (id child in children) { + leavesWarmed += [self warmVisibilityCacheForSnapshot:child]; + } + return leavesWarmed; +} + + (nullable NSString *)xmlStringWithRootElement:(id)root options:(nullable FBXMLGenerationOptions *)options { @@ -194,8 +109,19 @@ + (nullable NSString *)xmlStringWithRootElement:(id)root [self waitUntilStableWithElement:root]; // If 'includeHittableInPageSource' setting is enabled, then use native snapshots // to calculate a more accurate value for the 'hittable' attribute. - rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root - useNative:FBConfiguration.includeHittableInPageSource] + id snap = [self snapshotWithRoot:root + useNative:FBConfiguration.includeHittableInPageSource]; + BOOL preWarm = [FBConfiguration preWarmPageSource]; + [FBLogger logFmt:@"[QA_WOLF] [INFO] [VisCache] (FBXPath) preWarmPageSource=%@", + preWarm ? @"YES" : @"NO"]; + if (preWarm) { + NSDate *start = [NSDate date]; + NSUInteger leavesWarmed = [self warmVisibilityCacheForSnapshot:snap]; + [FBLogger logFmt:@"[QA_WOLF] [INFO] [VisCache] (FBXPath) Warmed %lu leaves in %.3fs", + (unsigned long)leavesWarmed, + ABS([start timeIntervalSinceNow])]; + } + rc = [self xmlRepresentationWithRootElement:snap writer:writer elementStore:nil query:nil diff --git a/WebDriverAgentLib/WebDriverAgentLib.h b/WebDriverAgentLib/WebDriverAgentLib.h index de916ee2e..f38e851c5 100644 --- a/WebDriverAgentLib/WebDriverAgentLib.h +++ b/WebDriverAgentLib/WebDriverAgentLib.h @@ -16,37 +16,60 @@ FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; #import #import +#import +#import #import #import #import +#import +#import #import #import #import +#import #import #import +#import #import +#import #import #import #import #import #import +#import +#import +#import #import #import #import #import #import #import +#import #import +#import +#import +#import +#import +#import #import #import #import +#import +#import #import #import #import +#import #import #import +#import #import +#import +#import #import +#import #import #import #import @@ -58,3 +81,12 @@ FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; #import #import #import + +// QAWolf code +#import +#import +#import +#import +#import +#import +#import diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/CDStructures.h b/WebDriverAgentLib/include/WebDriverAgentLib/CDStructures.h new file mode 120000 index 000000000..472715e10 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/CDStructures.h @@ -0,0 +1 @@ +../../../PrivateHeaders/XCTest/CDStructures.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/ExecuteWDACommand.h b/WebDriverAgentLib/include/WebDriverAgentLib/ExecuteWDACommand.h new file mode 120000 index 000000000..f2119549e --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/ExecuteWDACommand.h @@ -0,0 +1 @@ +../../QAWolf/ExecuteWDACommand.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBAlert.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBAlert.h new file mode 120000 index 000000000..ee9de5254 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBAlert.h @@ -0,0 +1 @@ +../../FBAlert.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBAlertViewCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBAlertViewCommands.h new file mode 120000 index 000000000..d8a9e6c07 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBAlertViewCommands.h @@ -0,0 +1 @@ +../../Commands/FBAlertViewCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBCapabilities.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBCapabilities.h new file mode 120000 index 000000000..e823500f7 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBCapabilities.h @@ -0,0 +1 @@ +../../Utilities/FBCapabilities.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBCommandHandler.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBCommandHandler.h new file mode 120000 index 000000000..a0b753c9a --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBCommandHandler.h @@ -0,0 +1 @@ +../../Routing/FBCommandHandler.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBCommandStatus.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBCommandStatus.h new file mode 120000 index 000000000..368c8c4f8 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBCommandStatus.h @@ -0,0 +1 @@ +../../Routing/FBCommandStatus.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBConfiguration.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBConfiguration.h new file mode 120000 index 000000000..052fb53f8 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBConfiguration.h @@ -0,0 +1 @@ +../../Utilities/FBConfiguration.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBCustomCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBCustomCommands.h new file mode 120000 index 000000000..af1b631f3 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBCustomCommands.h @@ -0,0 +1 @@ +../../Commands/FBCustomCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBDebugCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBDebugCommands.h new file mode 120000 index 000000000..b85ea67bf --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBDebugCommands.h @@ -0,0 +1 @@ +../../Commands/FBDebugCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBDebugLogDelegateDecorator.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBDebugLogDelegateDecorator.h new file mode 120000 index 000000000..d0b977e5d --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBDebugLogDelegateDecorator.h @@ -0,0 +1 @@ +../../Utilities/FBDebugLogDelegateDecorator.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBElement.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBElement.h new file mode 120000 index 000000000..ce26e1d54 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBElement.h @@ -0,0 +1 @@ +../../Routing/FBElement.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBElementCache.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBElementCache.h new file mode 120000 index 000000000..a8771464e --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBElementCache.h @@ -0,0 +1 @@ +../../Routing/FBElementCache.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBElementCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBElementCommands.h new file mode 120000 index 000000000..bfcd5d229 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBElementCommands.h @@ -0,0 +1 @@ +../../Commands/FBElementCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBElementTypeTransformer.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBElementTypeTransformer.h new file mode 120000 index 000000000..cc086de73 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBElementTypeTransformer.h @@ -0,0 +1 @@ +../../Utilities/FBElementTypeTransformer.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBErrorBuilder.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBErrorBuilder.h new file mode 120000 index 000000000..d60b1a0c0 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBErrorBuilder.h @@ -0,0 +1 @@ +../../Utilities/FBErrorBuilder.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBExceptionHandler.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBExceptionHandler.h new file mode 120000 index 000000000..7db7056ad --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBExceptionHandler.h @@ -0,0 +1 @@ +../../Routing/FBExceptionHandler.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBExceptions.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBExceptions.h new file mode 120000 index 000000000..868f9613a --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBExceptions.h @@ -0,0 +1 @@ +../../Routing/FBExceptions.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBFailureProofTestCase.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBFailureProofTestCase.h new file mode 120000 index 000000000..b3e0a98cf --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBFailureProofTestCase.h @@ -0,0 +1 @@ +../../Utilities/FBFailureProofTestCase.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBFindElementCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBFindElementCommands.h new file mode 120000 index 000000000..beee532e3 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBFindElementCommands.h @@ -0,0 +1 @@ +../../Commands/FBFindElementCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBHTTPStatusCodes.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBHTTPStatusCodes.h new file mode 120000 index 000000000..cde182008 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBHTTPStatusCodes.h @@ -0,0 +1 @@ +../../Routing/FBHTTPStatusCodes.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBKeyboard.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBKeyboard.h new file mode 120000 index 000000000..9ba05b57a --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBKeyboard.h @@ -0,0 +1 @@ +../../Utilities/FBKeyboard.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBLogger.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBLogger.h new file mode 120000 index 000000000..9d658f70b --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBLogger.h @@ -0,0 +1 @@ +../../Utilities/FBLogger.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBMacros.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBMacros.h new file mode 120000 index 000000000..a3fd887ff --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBMacros.h @@ -0,0 +1 @@ +../../Utilities/FBMacros.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBMathUtils.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBMathUtils.h new file mode 120000 index 000000000..992875dd3 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBMathUtils.h @@ -0,0 +1 @@ +../../Utilities/FBMathUtils.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBOrientationCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBOrientationCommands.h new file mode 120000 index 000000000..91e8f3445 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBOrientationCommands.h @@ -0,0 +1 @@ +../../Commands/FBOrientationCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBProtocolHelpers.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBProtocolHelpers.h new file mode 120000 index 000000000..4eb702773 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBProtocolHelpers.h @@ -0,0 +1 @@ +../../Utilities/FBProtocolHelpers.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBResponseJSONPayload.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBResponseJSONPayload.h new file mode 120000 index 000000000..f6e941822 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBResponseJSONPayload.h @@ -0,0 +1 @@ +../../Routing/FBResponseJSONPayload.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBResponsePayload.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBResponsePayload.h new file mode 120000 index 000000000..dd65ff4d7 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBResponsePayload.h @@ -0,0 +1 @@ +../../Routing/FBResponsePayload.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBRoute.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBRoute.h new file mode 120000 index 000000000..e0d1033e6 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBRoute.h @@ -0,0 +1 @@ +../../Routing/FBRoute.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBRouteRequest.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBRouteRequest.h new file mode 120000 index 000000000..622eb890b --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBRouteRequest.h @@ -0,0 +1 @@ +../../Routing/FBRouteRequest.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBRunLoopSpinner.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBRunLoopSpinner.h new file mode 120000 index 000000000..d33a94132 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBRunLoopSpinner.h @@ -0,0 +1 @@ +../../Utilities/FBRunLoopSpinner.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBRuntimeUtils.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBRuntimeUtils.h new file mode 120000 index 000000000..89f1b09c9 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBRuntimeUtils.h @@ -0,0 +1 @@ +../../Utilities/FBRuntimeUtils.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBScreenshotCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBScreenshotCommands.h new file mode 120000 index 000000000..dbc42a723 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBScreenshotCommands.h @@ -0,0 +1 @@ +../../Commands/FBScreenshotCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBSession.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBSession.h new file mode 120000 index 000000000..0b6260903 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBSession.h @@ -0,0 +1 @@ +../../Routing/FBSession.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBSessionCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBSessionCommands.h new file mode 120000 index 000000000..6d7f71fa8 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBSessionCommands.h @@ -0,0 +1 @@ +../../Commands/FBSessionCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBSettings.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBSettings.h new file mode 120000 index 000000000..e47c63bde --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBSettings.h @@ -0,0 +1 @@ +../../Utilities/FBSettings.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBTouchActionCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBTouchActionCommands.h new file mode 120000 index 000000000..63d08282d --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBTouchActionCommands.h @@ -0,0 +1 @@ +../../Commands/FBTouchActionCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBTouchIDCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBTouchIDCommands.h new file mode 120000 index 000000000..8bb2dfbd3 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBTouchIDCommands.h @@ -0,0 +1 @@ +../../Commands/FBTouchIDCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBVideoCommands.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBVideoCommands.h new file mode 120000 index 000000000..7e5fbaaf9 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBVideoCommands.h @@ -0,0 +1 @@ +../../Commands/FBVideoCommands.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBWebServer.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBWebServer.h new file mode 120000 index 000000000..e35ed4fe3 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBWebServer.h @@ -0,0 +1 @@ +../../Routing/FBWebServer.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBXCElementSnapshot.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBXCElementSnapshot.h new file mode 120000 index 000000000..d1e3e2e3d --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBXCElementSnapshot.h @@ -0,0 +1 @@ +../../Routing/FBXCElementSnapshot.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBXCElementSnapshotWrapper.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBXCElementSnapshotWrapper.h new file mode 120000 index 000000000..86839c737 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBXCElementSnapshotWrapper.h @@ -0,0 +1 @@ +../../Routing/FBXCElementSnapshotWrapper.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBXCodeCompatibility.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBXCodeCompatibility.h new file mode 120000 index 000000000..10f2386aa --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBXCodeCompatibility.h @@ -0,0 +1 @@ +../../Utilities/FBXCodeCompatibility.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBXMLGenerationOptions.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBXMLGenerationOptions.h new file mode 120000 index 000000000..44ad8cb3a --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBXMLGenerationOptions.h @@ -0,0 +1 @@ +../../Utilities/FBXMLGenerationOptions.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/FBXPath.h b/WebDriverAgentLib/include/WebDriverAgentLib/FBXPath.h new file mode 120000 index 000000000..c6d93de5f --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/FBXPath.h @@ -0,0 +1 @@ +../../Utilities/FBXPath.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/HandleWDACommandException.h b/WebDriverAgentLib/include/WebDriverAgentLib/HandleWDACommandException.h new file mode 120000 index 000000000..be98e54a7 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/HandleWDACommandException.h @@ -0,0 +1 @@ +../../QAWolf/HandleWDACommandException.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/QAWObjCExceptionHandler.h b/WebDriverAgentLib/include/WebDriverAgentLib/QAWObjCExceptionHandler.h new file mode 120000 index 000000000..d82d942c1 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/QAWObjCExceptionHandler.h @@ -0,0 +1 @@ +../../QAWolf/QAWObjCExceptionHandler.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/QAWSnapshotResult.h b/WebDriverAgentLib/include/WebDriverAgentLib/QAWSnapshotResult.h new file mode 120000 index 000000000..7d87ee50b --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/QAWSnapshotResult.h @@ -0,0 +1 @@ +../../QAWolf/QAWSnapshotResult.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/QAWXML.h b/WebDriverAgentLib/include/WebDriverAgentLib/QAWXML.h new file mode 120000 index 000000000..7169cd816 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/QAWXML.h @@ -0,0 +1 @@ +../../QAWolf/QAWXML.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/WebDriverAgentLib.h b/WebDriverAgentLib/include/WebDriverAgentLib/WebDriverAgentLib.h new file mode 120000 index 000000000..ddbf0d77f --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/WebDriverAgentLib.h @@ -0,0 +1 @@ +../../WebDriverAgentLib.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCDebugLogDelegate-Protocol.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCDebugLogDelegate-Protocol.h new file mode 120000 index 000000000..922b9b8cd --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCDebugLogDelegate-Protocol.h @@ -0,0 +1 @@ +../../../PrivateHeaders/XCTest/XCDebugLogDelegate-Protocol.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCPointerEvent.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCPointerEvent.h new file mode 120000 index 000000000..a34a0e4a1 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCPointerEvent.h @@ -0,0 +1 @@ +../../../PrivateHeaders/XCTest/XCPointerEvent.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCTIssue+FBPatcher.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCTIssue+FBPatcher.h new file mode 120000 index 000000000..38c1f3b16 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCTIssue+FBPatcher.h @@ -0,0 +1 @@ +../../Categories/XCTIssue+FBPatcher.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCTestCase.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCTestCase.h new file mode 120000 index 000000000..b58ccc867 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCTestCase.h @@ -0,0 +1 @@ +../../../PrivateHeaders/XCTest/XCTestCase.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication+FBHelpers.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication+FBHelpers.h new file mode 120000 index 000000000..26af3c19f --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication+FBHelpers.h @@ -0,0 +1 @@ +../../Categories/XCUIApplication+FBHelpers.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication+FBQuiescence.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication+FBQuiescence.h new file mode 120000 index 000000000..8d89c156c --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication+FBQuiescence.h @@ -0,0 +1 @@ +../../Categories/XCUIApplication+FBQuiescence.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication.h new file mode 120000 index 000000000..e51fe2eba --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplication.h @@ -0,0 +1 @@ +../../../PrivateHeaders/XCTest/XCUIApplication.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplicationProcessDelay.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplicationProcessDelay.h new file mode 120000 index 000000000..d37568c5d --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIApplicationProcessDelay.h @@ -0,0 +1 @@ +../../Utilities/XCUIApplicationProcessDelay.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBHealthCheck.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBHealthCheck.h new file mode 120000 index 000000000..8a918c72d --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBHealthCheck.h @@ -0,0 +1 @@ +../../Categories/XCUIDevice+FBHealthCheck.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBHelpers.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBHelpers.h new file mode 120000 index 000000000..50a6b7820 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBHelpers.h @@ -0,0 +1 @@ +../../Categories/XCUIDevice+FBHelpers.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBRotation.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBRotation.h new file mode 120000 index 000000000..a897c6491 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBRotation.h @@ -0,0 +1 @@ +../../Categories/XCUIDevice+FBRotation.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBVoiceOver.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBVoiceOver.h new file mode 120000 index 000000000..e48c0c5a7 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIDevice+FBVoiceOver.h @@ -0,0 +1 @@ +../../Categories/XCUIDevice+FBVoiceOver.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBAccessibility.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBAccessibility.h new file mode 120000 index 000000000..c442e358f --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBAccessibility.h @@ -0,0 +1 @@ +../../Categories/XCUIElement+FBAccessibility.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBCustomActions.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBCustomActions.h new file mode 120000 index 000000000..d4fbf5605 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBCustomActions.h @@ -0,0 +1 @@ +../../Categories/XCUIElement+FBCustomActions.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBFind.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBFind.h new file mode 120000 index 000000000..969533bdc --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBFind.h @@ -0,0 +1 @@ +../../Categories/XCUIElement+FBFind.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBForceTouch.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBForceTouch.h new file mode 120000 index 000000000..112b8246d --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBForceTouch.h @@ -0,0 +1 @@ +../../Categories/XCUIElement+FBForceTouch.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBIsVisible.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBIsVisible.h new file mode 120000 index 000000000..34c997989 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBIsVisible.h @@ -0,0 +1 @@ +../../Categories/XCUIElement+FBIsVisible.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBScrolling.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBScrolling.h new file mode 120000 index 000000000..679ddc999 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBScrolling.h @@ -0,0 +1 @@ +../../Categories/XCUIElement+FBScrolling.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBUtilities.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBUtilities.h new file mode 120000 index 000000000..690540eca --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBUtilities.h @@ -0,0 +1 @@ +../../Categories/XCUIElement+FBUtilities.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBWebDriverAttributes.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBWebDriverAttributes.h new file mode 120000 index 000000000..af760f94b --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+FBWebDriverAttributes.h @@ -0,0 +1 @@ +../../Categories/XCUIElement+FBWebDriverAttributes.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+QAWSnapshotUtilities.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+QAWSnapshotUtilities.h new file mode 120000 index 000000000..b312ec06c --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement+QAWSnapshotUtilities.h @@ -0,0 +1 @@ +../../QAWolf/XCUIElement+QAWSnapshotUtilities.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement.h new file mode 120000 index 000000000..ee575ffe7 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElement.h @@ -0,0 +1 @@ +../../../PrivateHeaders/XCTest/XCUIElement.h \ No newline at end of file diff --git a/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElementSnapshotParser.h b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElementSnapshotParser.h new file mode 120000 index 000000000..c094a3623 --- /dev/null +++ b/WebDriverAgentLib/include/WebDriverAgentLib/XCUIElementSnapshotParser.h @@ -0,0 +1 @@ +../../QAWolf/XCUIElementSnapshotParser.h \ No newline at end of file diff --git a/headers.sh b/headers.sh new file mode 100755 index 000000000..60b4d7062 --- /dev/null +++ b/headers.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# sync-public-headers.sh +# +# 1. Scans all .h/.m/.mm files under WebDriverAgentLib/ for +# #import statements. +# 2. Resolves each header's actual location (either in WebDriverAgentLib/* +# or PrivateHeaders/*). +# 3. Creates/updates a symlink inside WebDriverAgentLib/include/ +# so the header is exported by SwiftPM. +# +# ./sync-public-headers.sh +# +# Run this from the repository root. Re-run whenever you add or move headers. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +WDA_DIR="$ROOT_DIR/WebDriverAgentLib" +INCLUDE_DIR="$WDA_DIR/include" + +mkdir -p "$INCLUDE_DIR" + +echo "Generating symlinks in ${INCLUDE_DIR#$ROOT_DIR/}" +echo + +# 1. collect unique imports (portable, no mapfile) +IMPORTS=$(find "$WDA_DIR" -type f \( -name '*.h' -o -name '*.m' -o -name '*.mm' \) -print0 | + xargs -0 grep -h -o '#import ]*>' | + sed 's/^#import[[:space:]]*//' | + sort -u) + +# 2. process each import +while IFS= read -r import; do + header=${import#} + + # where is the real file? + real_path=$(find \ + "$WDA_DIR" \ + "$ROOT_DIR/PrivateHeaders" \ + -type f -name "$header" | head -n 1 || true) + + if [[ -z $real_path ]]; then + echo "⚠️ $header … NOT FOUND" + continue + fi + + link="$INCLUDE_DIR/WebDriverAgentLib/$header" + mkdir -p "$(dirname "$link")" + + # portable relative path for the symlink + rel_dest=$(python3 -c "import os,sys,os.path; print(os.path.relpath(sys.argv[2], sys.argv[1]))" "$(dirname "$link")" "$real_path") + printf 'Creating symlink %s → %s\n' "${link#$ROOT_DIR/}" "$rel_dest" + + ln -sf "$rel_dest" "$link" +done <<< "$IMPORTS"