diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c8c1c..b89928a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [6.1.3](https://github.com/NativeScript/nativescript-dev-appium/compare/6.1.2...6.1.3) (2019-11-12) + + +### Bug Fixes + +* **ios:** apply getActualRectangle ([#267](https://github.com/NativeScript/nativescript-dev-appium/issues/267)) ([ebe92e0](https://github.com/NativeScript/nativescript-dev-appium/commit/ebe92e0)) +* compareElement to use proper rectangle and generate proper diff image. ([cd76798](https://github.com/NativeScript/nativescript-dev-appium/commit/cd76798)) +* resolve device type by device name ([#277](https://github.com/NativeScript/nativescript-dev-appium/issues/277)) ([e7d7345](https://github.com/NativeScript/nativescript-dev-appium/commit/e7d7345)) + + + + +## [6.1.2](https://github.com/NativeScript/nativescript-dev-appium/compare/6.1.0...6.1.2) (2019-10-04) + + +### Bug Fixes + +* **ios-13:** remove statusbar height from viewportRect ([c1a993c](https://github.com/NativeScript/nativescript-dev-appium/commit/c1a993c)) + + + # [6.1.0](https://github.com/NativeScript/nativescript-dev-appium/compare/6.0.0...6.1.0) (2019-10-03) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index e30d196..f4615cc 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -30,7 +30,8 @@ import { encodeImageToBase64, ensureReportsDirExists, checkImageLogType, - adbShellCommand + adbShellCommand, + logWarn } from "./utils"; import { INsCapabilities } from "./interfaces/ns-capabilities"; @@ -167,6 +168,11 @@ export class AppiumDriver { } public async navBack() { + if (this.isAndroid) { + logInfo("=== Navigate back with hardware button!"); + } else { + logInfo("=== Navigate back."); + } return await this._driver.back(); } @@ -244,7 +250,7 @@ export class AppiumDriver { prepareApp(args); if (!args.device) { if (args.isAndroid) { - args.device = DeviceManager.getDefaultDevice(args, sessionInfo.capabilities.desired.deviceName, sessionInfo.capabilities.deviceUDID.replace("emulator-", ""), sessionInfo.capabilities.deviceUDID.includes("emulator") ? DeviceType.EMULATOR : DeviceType.SIMULATOR, sessionInfo.capabilities.desired.platformVersion || sessionInfo.capabilities.platformVersion); + args.device = DeviceManager.getDefaultDevice(args, sessionInfo.capabilities.desired.deviceName, sessionInfo.capabilities.deviceUDID.replace("emulator-", ""), sessionInfo.capabilities.deviceUDID.includes("emulator") ? DeviceType.EMULATOR : DeviceType.SIMULATOR, sessionInfo.capabilities.deviceApiLevel || sessionInfo.capabilities.platformVersion); } else { args.device = DeviceManager.getDefaultDevice(args); } @@ -257,6 +263,8 @@ export class AppiumDriver { } } catch (error) { args.verbose = true; + console.log("==============================="); + console.log("", error) if (!args.ignoreDeviceController && error && error.message && error.message.includes("Failure [INSTALL_FAILED_INSUFFICIENT_STORAGE]")) { await DeviceManager.kill(args.device); await DeviceController.startDevice(args.device); @@ -272,7 +280,7 @@ export class AppiumDriver { logInfo("Current version of appium doesn't support appium settings!"); } - await DeviceManager.applyDeviceAdditionsSettings(driver, args, appiumCapsFromConfig); + await DeviceManager.applyDeviceAdditionsSettings(driver, args, sessionInfoDetails); hasStarted = true; } catch (error) { @@ -280,11 +288,11 @@ export class AppiumDriver { console.log("Retry launching appium driver!"); hasStarted = false; - if (error && error.message && error.message.includes("WebDriverAgent")) { - const freePort = await findFreePort(100, args.wdaLocalPort); - console.log("args.appiumCaps['wdaLocalPort']", freePort); - args.appiumCaps["wdaLocalPort"] = freePort; - } + // if (error && error.message && error.message.includes("WebDriverAgent")) { + // const freePort = await findFreePort(100, args.wdaLocalPort); + // console.log("args.appiumCaps['wdaLocalPort']", freePort); + // args.appiumCaps["wdaLocalPort"] = freePort; + // } } if (hasStarted) { @@ -318,10 +326,19 @@ export class AppiumDriver { && sessionInfoDetails.platformName.toLowerCase() === "ios" && sessionInfoDetails.platformVersion.startsWith("13")) { try { - const devicesInfos = IOSController.devicesDisplaysInfos(); - const matches = devicesInfos.filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); - const deviceType = matches[matches.length - 1]; - args.device.viewportRect.y += deviceType.statBarHeight * deviceType.density; + const devicesInfos = IOSController.devicesDisplaysInfos() + .filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); + + if (devicesInfos.length > 0) { + // sort devices by best match - in case we have iPhone XR 13 -> it will match device type 'iPhone X' and 'iPhone XR' -> after sort we will pick first longest match + devicesInfos + .sort((a, b) => { + return sessionInfoDetails.deviceName.replace(a.deviceType, "").length - sessionInfoDetails.deviceName.replace(b.deviceType, "").length + }); + const deviceType = devicesInfos[0]; + args.device.viewportRect.y += deviceType.actionBarHeight; + args.device.viewportRect.height -= deviceType.actionBarHeight; + } } catch (error) { } } @@ -482,7 +499,7 @@ export class AppiumDriver { * @param xOffset */ public async scroll(direction: Direction, y: number, x: number, yOffset: number, xOffset: number = 0) { - await scroll(this._wd, this._driver, direction, this._webio.isIOS, y, x, yOffset, xOffset, this._args.verbose); + await scroll(this._wd, this._driver, direction, this._webio.isIOS, y, x, yOffset, xOffset); } /** @@ -500,13 +517,15 @@ export class AppiumDriver { while ((el === null || !isDisplayed) && retryCount > 0) { try { el = await element(); - isDisplayed = await el.isDisplayed(); + isDisplayed = el && await el.isDisplayed(); if (!isDisplayed) { - await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.x, offsetPoint.y, this._args.verbose); + await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.y, offsetPoint.x); el = null; } } catch (error) { console.log("scrollTo Error: " + error); + await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.y, offsetPoint.x); + el = null; } retryCount--; @@ -598,11 +617,11 @@ export class AppiumDriver { return await this.driver.getSessionId(); } - public async compareElement(element: UIElement, imageName?: string, tolerance: number = 0, timeOutSeconds: number = 3, toleranceType: ImageOptions = ImageOptions.percent) { + public async compareElement(element: UIElement, imageName?: string, tolerance: number = this.imageHelper.defaultTolerance, timeOutSeconds: number = 3, toleranceType: ImageOptions = this.imageHelper.defaultToleranceType) { return await this.compareRectangle(await element.getActualRectangle(), imageName, timeOutSeconds, tolerance, toleranceType); } - public async compareRectangle(rect: IRectangle, imageName?: string, timeOutSeconds: number = 3, tolerance: number = 0, toleranceType: ImageOptions = ImageOptions.percent) { + public async compareRectangle(rect: IRectangle, imageName?: string, timeOutSeconds: number = 3, tolerance: number = this.imageHelper.defaultTolerance, toleranceType: ImageOptions = this.imageHelper.defaultToleranceType) { imageName = imageName || this.imageHelper.testName; const options = this.imageHelper.extendOptions({ imageName: imageName, @@ -616,7 +635,7 @@ export class AppiumDriver { return await this.imageHelper.compare(options); } - public async compareScreen(imageName?: string, timeOutSeconds: number = 3, tolerance: number = 0, toleranceType: ImageOptions = ImageOptions.percent) { + public async compareScreen(imageName?: string, timeOutSeconds: number = 3, tolerance: number = this.imageHelper.defaultTolerance, toleranceType: ImageOptions = this.imageHelper.defaultToleranceType) { imageName = imageName || this.imageHelper.testName; const options = this.imageHelper.extendOptions({ imageName: imageName, @@ -855,7 +874,7 @@ export class AppiumDriver { } } - private static async applyAdditionalSettings(args) { + private static async applyAdditionalSettings(args: INsCapabilities) { if (args.isSauceLab) return; args.appiumCaps['udid'] = args.appiumCaps['udid'] || args.device.token; @@ -872,6 +891,11 @@ export class AppiumDriver { args.appiumCaps["wdaStartupRetries"] = 5; args.appiumCaps["shouldUseSingletonTestManager"] = args.appiumCaps.shouldUseSingletonTestManager; + if (args.derivedDataPath) { + args.appiumCaps["derivedDataPath"] = `${args.derivedDataPath}/${args.device.token}`; + logWarn('Changed derivedDataPath to: ', args.appiumCaps["derivedDataPath"]); + } + // It looks we need it for XCTest (iOS 10+ automation) if (args.appiumCaps.platformVersion >= 10 && args.wdaLocalPort) { console.log(`args.appiumCaps['wdaLocalPort']: ${args.wdaLocalPort}`); diff --git a/lib/appium-server.ts b/lib/appium-server.ts index e22ef5e..85230b4 100644 --- a/lib/appium-server.ts +++ b/lib/appium-server.ts @@ -126,7 +126,7 @@ export class AppiumServer { private startAppiumServer(logLevel: string, isSauceLab: boolean) { const startingServerArgs: Array = isSauceLab ? ["--log-level", logLevel] : ["-p", this.port.toString(), "--log-level", logLevel]; - if (this._args.isAndroid && this._args.ignoreDeviceController && !this._args.isSauceLab) { + if (this._args.isAndroid) { this._args.relaxedSecurity ? startingServerArgs.push("--relaxed-security") : console.log("'relaxedSecurity' is not enabled!\nTo enabled it use '--relaxedSecurity'!"); } diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 63d3105..385f0ab 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -171,7 +171,7 @@ export class DeviceManager implements IDeviceManager { type: type, platform: args.appiumCaps.platformName.toLowerCase(), token: token, - apiLevel: platformVersion || args.appiumCaps.platformVersion, + apiLevel: platformVersion || args.appiumCaps.deviceApiLevel || args.appiumCaps.platformVersion, config: { "density": args.appiumCaps.density, "offsetPixels": args.appiumCaps.offsetPixels } } @@ -197,18 +197,19 @@ export class DeviceManager implements IDeviceManager { const sizeArr = sessionInfoDetails.deviceScreenSize.split("x"); args.device.deviceScreenSize = { width: sizeArr[0], height: sizeArr[1] }; - args.device.apiLevel = sessionInfoDetails.deviceApiLevel; + args.device.apiLevel = sessionInfoDetails.deviceApiLevel || args.device.apiLevel; args.device.deviceScreenDensity = sessionInfoDetails.deviceScreenDensity / 100; args.device.config = { "density": args.device.deviceScreenDensity || args.device.config.density, "offsetPixels": +sessionInfoDetails.statBarHeight || args.device.config.offsetPixels }; } else { args.device.apiLevel = sessionInfoDetails.platformVersion; - args.device.deviceScreenDensity = sessionInfoDetails.pixelRatio; + args.device.deviceScreenDensity = sessionInfoDetails.pixelRatio || args.device.config.density; const offsetPixels = +sessionInfoDetails.viewportRect.top - +sessionInfoDetails.statBarHeight; args.device.config = { "density": sessionInfoDetails.pixelRatio || args.device.config.density, "offsetPixels": isNumber(offsetPixels) ? offsetPixels : args.device.config.offsetPixels }; } args.device.statBarHeight = sessionInfoDetails.statBarHeight; args.device.viewportRect = DeviceManager.convertViewportRectToIRectangle(sessionInfoDetails.viewportRect); + args.device.token = args.device.token || sessionInfoDetails.udid; return args.device; } @@ -217,14 +218,14 @@ export class DeviceManager implements IDeviceManager { const status = value ? 1 : 0; try { if (args.isAndroid) { - if (!args.ignoreDeviceController) { - AndroidController.setDontKeepActivities(value, args.device); - } else if (args.relaxedSecurity) { + if (args.relaxedSecurity) { const output = await DeviceManager.executeShellCommand(driver, { command: "settings", args: ['put', 'global', 'always_finish_activities', status] }); console.log(`Output from setting always_finish_activities to ${status}: ${output}`); //check if set const check = await DeviceManager.executeShellCommand(driver, { command: "settings", args: ['get', 'global', 'always_finish_activities'] }); console.info(`Check if always_finish_activities is set correctly: ${check}`); + } else if (!args.ignoreDeviceController) { + AndroidController.setDontKeepActivities(value, args.device); } } else { // Do nothing for iOS ... @@ -272,11 +273,11 @@ export class DeviceManager implements IDeviceManager { // } public static async applyDeviceAdditionsSettings(driver, args: INsCapabilities, sessionInfo: any) { - if ((!args.device.viewportRect || !args.device.viewportRect.x) && (!args.device.config || !args.device.config.offsetPixels)) { + if ((!args.device.viewportRect || !args.device.viewportRect.x) && (!args.device.config || !isNumber(args.device.config.offsetPixels))) { args.device.config = {}; let density: number; - if (sessionInfo && sessionInfo.length >= 1) { - density = sessionInfo[1].deviceScreenDensity ? sessionInfo[1].deviceScreenDensity / 100 : undefined; + if (sessionInfo && Object.getOwnPropertyNames(sessionInfo).length >= 1) { + density = sessionInfo.pixelRatio ? sessionInfo.pixelRatio : undefined; } if (density) { diff --git a/lib/direction.d.ts b/lib/direction.d.ts index 8fe9856..2a7257e 100644 --- a/lib/direction.d.ts +++ b/lib/direction.d.ts @@ -1,4 +1,4 @@ -export declare const enum Direction { +export declare enum Direction { down = 0, up = 1, left = 2, diff --git a/lib/direction.ts b/lib/direction.ts index d1a3683..0fd5bd8 100644 --- a/lib/direction.ts +++ b/lib/direction.ts @@ -1,4 +1,4 @@ -export const enum Direction { +export enum Direction { down, up, left, diff --git a/lib/image-helper.d.ts b/lib/image-helper.d.ts index ddc080a..3842db1 100644 --- a/lib/image-helper.d.ts +++ b/lib/image-helper.d.ts @@ -64,6 +64,8 @@ export declare class ImageHelper { private _blockOutAreas; private _imagesResults; private _options; + private _defaultToleranceType; + private _defaultTolerance; private _defaultOptions; constructor(_args: INsCapabilities, _driver: AppiumDriver); static readonly pngFileExt = ".png"; @@ -80,6 +82,8 @@ export declare class ImageHelper { delta: number; options: IImageCompareOptions; blockOutAreas: IRectangle[]; + defaultToleranceType: ImageOptions; + defaultTolerance: number; compareScreen(options?: IImageCompareOptions): Promise; compareElement(element: UIElement, options?: IImageCompareOptions): Promise; compareRectangle(cropRectangle: IRectangle, options?: IImageCompareOptions): Promise; diff --git a/lib/image-helper.ts b/lib/image-helper.ts index 3d88ad7..139024d 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -83,6 +83,8 @@ export class ImageHelper { private _blockOutAreas: IRectangle[]; private _imagesResults = new Map(); private _options: IImageCompareOptions = {}; + private _defaultToleranceType: ImageOptions = ImageOptions.percent; + private _defaultTolerance: number = 0; private _defaultOptions: IImageCompareOptions = { timeOutSeconds: 2, tolerance: 0, @@ -148,6 +150,22 @@ export class ImageHelper { this._blockOutAreas = rectangles; } + get defaultToleranceType(): ImageOptions { + return this._defaultToleranceType; + } + + set defaultToleranceType(toleranceType: ImageOptions) { + this._defaultToleranceType = toleranceType; + } + + get defaultTolerance(): number { + return this._defaultTolerance; + } + + set defaultTolerance(tolerance: number) { + this._defaultTolerance = tolerance; + } + public async compareScreen(options?: IImageCompareOptions) { options = this.extendOptions(options); options.imageName = this.increaseImageName(options.imageName || this.testName, options); @@ -314,13 +332,13 @@ export class ImageHelper { public compareImages(options: IImageCompareOptions, actual: string, expected: string, output: string) { const clipRect = { - x: this.options.cropRectangle.x, - y: this.options.cropRectangle.y, - width: this.options.cropRectangle.width, - height: this.options.cropRectangle.height + x: options.cropRectangle.x, + y: options.cropRectangle.y, + width: options.cropRectangle.width, + height: options.cropRectangle.height } - if (!this.options.keepOriginalImageSize) { + if (!options.keepOriginalImageSize) { clipRect.x = 0; clipRect.y = 0; clipRect.width = undefined; diff --git a/lib/interfaces/ns-capabilities-args.d.ts b/lib/interfaces/ns-capabilities-args.d.ts index f1b29c9..a0c9a71 100644 --- a/lib/interfaces/ns-capabilities-args.d.ts +++ b/lib/interfaces/ns-capabilities-args.d.ts @@ -4,6 +4,7 @@ import { AutomationName } from "../automation-name"; import { ITestReporter } from "./test-reporter"; import { LogImageType } from "../enums/log-image-type"; export interface INsCapabilitiesArgs { + derivedDataPath?: string; port?: number; wdaLocalPort?: number; projectDir?: string; diff --git a/lib/interfaces/ns-capabilities-args.ts b/lib/interfaces/ns-capabilities-args.ts index 5e0ea58..2375303 100644 --- a/lib/interfaces/ns-capabilities-args.ts +++ b/lib/interfaces/ns-capabilities-args.ts @@ -5,6 +5,7 @@ import { ITestReporter } from "./test-reporter"; import { LogImageType } from "../enums/log-image-type"; export interface INsCapabilitiesArgs { + derivedDataPath?: string; port?: number; wdaLocalPort?: number; projectDir?: string; diff --git a/lib/ns-capabilities.d.ts b/lib/ns-capabilities.d.ts index ae4f03a..267ed98 100644 --- a/lib/ns-capabilities.d.ts +++ b/lib/ns-capabilities.d.ts @@ -47,6 +47,7 @@ export declare class NsCapabilities implements INsCapabilities { deviceTypeOrPlatform: string; driverConfig: any; logImageTypes: Array; + derivedDataPath: string; constructor(_parser: INsCapabilitiesArgs); readonly isAndroid: any; readonly isIOS: boolean; diff --git a/lib/ns-capabilities.ts b/lib/ns-capabilities.ts index cb99c78..b9a123a 100644 --- a/lib/ns-capabilities.ts +++ b/lib/ns-capabilities.ts @@ -53,6 +53,7 @@ export class NsCapabilities implements INsCapabilities { public deviceTypeOrPlatform: string; public driverConfig: any; public logImageTypes: Array; + public derivedDataPath: string; constructor(private _parser: INsCapabilitiesArgs) { this.projectDir = this._parser.projectDir; @@ -76,6 +77,7 @@ export class NsCapabilities implements INsCapabilities { this.isSauceLab = this._parser.isSauceLab; this.ignoreDeviceController = this._parser.ignoreDeviceController; this.wdaLocalPort = this._parser.wdaLocalPort; + this.derivedDataPath = this._parser.derivedDataPath; this.path = this._parser.path; this.capabilitiesName = this._parser.capabilitiesName; this.imagesPath = this._parser.imagesPath; diff --git a/lib/parser.d.ts b/lib/parser.d.ts index 40edb2e..b9aae80 100644 --- a/lib/parser.d.ts +++ b/lib/parser.d.ts @@ -1,2 +1,2 @@ import { LogImageType } from "./enums/log-image-type"; -export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; +export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, derivedDataPath: string, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; diff --git a/lib/parser.ts b/lib/parser.ts index 977e173..dfd21e9 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -64,6 +64,9 @@ const config = (() => { type: "string" }) .option("wdaLocalPort", { alias: "wda", describe: "WDA port", type: "number" }) + .options("derivedDataPath", { + describe: "set the unique derived data path root for each driver instance. This will help to avoid possible conflicts and to speed up the parallel execution", + type: "string" }) .option("verbose", { alias: "v", describe: "Log actions", type: "boolean" }) .option("path", { describe: "Execution path", default: process.cwd(), type: "string" }) .option("relaxedSecurity", { describe: "appium relaxedSecurity", default: false, type: "boolean" }) @@ -160,6 +163,7 @@ const config = (() => { pluginRoot: pluginRoot, pluginBinary: pluginBinary, wdaLocalPort: options.wdaLocalPort || process.env.npm_config_wdaLocalPort || process.env["WDA_LOCAL_PORT"] || 8410, + derivedDataPath: options.derivedDataPath || process.env.npm_config_derivedDataPath || process.env["DERIVED_DATA_PATH"], testFolder: options.testFolder || process.env.npm_config_testFolder || "e2e", runType: options.runType || process.env.npm_config_runType, appiumCapsLocation: options.appiumCapsLocation || process.env.npm_config_appiumCapsLocation || join(projectDir, options.testFolder, "config", options.capabilitiesName), @@ -208,6 +212,7 @@ export const { devMode, ignoreDeviceController, wdaLocalPort, + derivedDataPath, path, relaxedSecurity, cleanApp, diff --git a/lib/ui-element.d.ts b/lib/ui-element.d.ts index 2bb26d3..ee66fbb 100644 --- a/lib/ui-element.d.ts +++ b/lib/ui-element.d.ts @@ -16,6 +16,10 @@ export declare class UIElement { * Click on element */ click(): Promise; + getCenter(): Promise<{ + x: number; + y: number; + }>; tapCenter(): Promise; tapAtTheEnd(): Promise; /** @@ -26,9 +30,14 @@ export declare class UIElement { */ tap(): Promise; /** + * @experimental * Double tap on element */ - doubleTap(): Promise; + doubleTap(offset?: { + x: number; + y: number; + }): Promise; + longPress(duration: number): Promise; /** * Get location of element */ @@ -36,7 +45,10 @@ export declare class UIElement { /** * Get size of element */ - size(): Promise; + size(): Promise<{ + width: number; + height: number; + }>; /** * Get text of element */ @@ -113,13 +125,6 @@ export declare class UIElement { */ scrollTo(direction: Direction, elementToSearch: () => Promise, yOffset?: number, xOffset?: number, retries?: number): Promise; /** - * Drag element with specific offset - * @param direction - * @param yOffset - * @param xOffset - default value 0 - */ - drag(direction: Direction, yOffset: number, xOffset?: number): Promise; - /** * Click and hold over an element * @param time in milliseconds to increase the default press period. */ @@ -165,4 +170,50 @@ export declare class UIElement { * @param direction */ swipe(direction: Direction): Promise; + /** + * Drag element with specific offset + * @experimental + * @param direction + * @param yOffset + * @param xOffset - default value 0 + */ + drag(direction: Direction, yOffset: number, xOffset?: number, duration?: number): Promise; + /** + *@experimental + * Pan element with specific offset + * @param offsets where the finger should move to. + * @param initPointOffset element.getRectangle() is used as start point. In case some additional offset should be provided use this param. + */ + pan(offsets: { + x: number; + y: number; + }[], initPointOffset?: { + x: number; + y: number; + }): Promise; + /** + * @experimental + * This method will try to move two fingers from opposite corners. + * One finger starts from + * { x: elementRect.x + offset.x, y: elementRect.y + offset.y } + * and ends to + * { x: elementRect.x + elementRect.width - offset.x, y: elementRect.height + elementRect.y - offset.y } + * and the other finger starts from + * { x: elementRect.width + elementRect.x - offset.x, y: elementRect.height + elementRect.y - offset.y } + * and ends to + * { x: elementRect.x + offset.x, y: elementRect.y + offset.y } + */ + rotate(offset?: { + x: number; + y: number; + }): Promise; + /** + * @experimental + * @param zoomFactory - zoomIn or zoomOut. Only zoomIn action is implemented + * @param offset + */ + pinch(zoomType: "in" | "out", offset?: { + x: number; + y: number; + }): Promise; } diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 3f89674..62a37fa 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -2,7 +2,7 @@ import { Point } from "./point"; import { Direction } from "./direction"; import { INsCapabilities } from "./interfaces/ns-capabilities"; import { AutomationName } from "./automation-name"; -import { calculateOffset, adbShellCommand, logError } from "./utils"; +import { calculateOffset, adbShellCommand, logError, wait, logInfo } from "./utils"; import { AndroidKeyEvent } from "mobile-devices-controller"; export class UIElement { @@ -24,12 +24,17 @@ export class UIElement { return await (await this.element()).click(); } + public async getCenter() { + const rect = await this.getRectangle(); + return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }; + } + public async tapCenter() { let action = new this._wd.TouchAction(this._driver); - const rect = await this.getActualRectangle(); - this._args.testReporterLog(`Tap on center element ${{ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }}`); + const centerRect = await this.getCenter(); + this._args.testReporterLog(`Tap on center element x: ${centerRect.x} y: ${centerRect.y}`); action - .tap({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }); + .tap(centerRect); await action.perform(); await this._driver.sleep(150); } @@ -59,10 +64,40 @@ export class UIElement { } /** + * @experimental * Double tap on element */ - public async doubleTap() { - return await this._driver.execute('mobile: doubleTap', { element: (await this.element()).value.ELEMENT }); + public async doubleTap(offset: { x: number, y: number } = { x: 0, y: 0 }) { + if (this._args.isAndroid) { + // hack double tap for android + const rect = await this.getRectangle(); + + if (`${this._args.device.apiLevel}`.startsWith("29") + || `${this._args.device.apiLevel}`.startsWith("9.")) { + const offsetPoint = { x: (rect.x + offset.x), y: (rect.y + offset.y) }; + await adbShellCommand(this._driver, "input", ["tap", `${offsetPoint.x} ${offsetPoint.y}`]); + await adbShellCommand(this._driver, "input", ["tap", `${offsetPoint.x} ${offsetPoint.y}`]); + } else { + let action = new this._wd.TouchAction(this._driver); + action.press({ x: rect.x, y: rect.y }).release().perform(); + action.press({ x: rect.x, y: rect.y }).release().perform(); + await action.perform(); + } + } else { + // this works only for ios, otherwise it throws error + return await this._driver.execute('mobile: doubleTap', { element: this._element.value }); + } + } + + public async longPress(duration: number) { + const rect = await this.getCenter(); + console.log("LongPress at ", rect); + const action = new this._wd.TouchAction(this._driver); + action + .press({ x: rect.x, y: rect.y }) + .wait(duration) + .release(); + await action.perform(); } /** @@ -77,10 +112,9 @@ export class UIElement { /** * Get size of element */ - public async size() { + public async size(): Promise<{ width: number, height: number }> { const size = await (await this.element()).getSize(); - const point = new Point(size.height, size.width); - return point; + return size; } /** @@ -233,7 +267,7 @@ export class UIElement { public async getRectangle() { const location = await this.location(); const size = await this.size(); - const rect = { x: location.x, y: location.y, width: size.y, height: size.x }; + const rect = { x: location.x, y: location.y, width: size.width, height: size.height }; return rect; } @@ -242,7 +276,7 @@ export class UIElement { */ public async getActualRectangle() { const actRect = await this.getRectangle(); - const density = this._args.device.config.density; + const density = this._args.device.deviceScreenDensity; if (this._args.isIOS) { if (density) { actRect.x *= density; @@ -263,40 +297,34 @@ export class UIElement { * @param xOffset */ public async scroll(direction: Direction, yOffset: number = 0, xOffset: number = 0) { - //await this._driver.execute("mobile: scroll", [{direction: 'up'}]) - //await this._driver.execute('mobile: scroll', { direction: direction === 0 ? "down" : "up", element: this._element.ELEMENT }); const location = await this.location(); const size = await this.size(); - const x = location.x === 0 ? 10 : location.x; - let y = (location.y + 15); - if (yOffset === 0) { - yOffset = location.y + size.y - 15; - } - if (direction === Direction.down) { - y = (location.y + size.y) - 15; - - if (!this._webio.isIOS) { - if (yOffset === 0) { - yOffset = location.y + size.y - 15; - } + if (direction === Direction.down || direction === Direction.up) { + if (xOffset > 0) { + location.x += xOffset; } - } - if (direction === Direction.up) { if (yOffset === 0) { - yOffset = size.y - 15; + yOffset = location.y + size.height - 5; } } - const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, this._webio.isIOS, false); - if (direction === Direction.down) { - //endPoint.point.y += location.y; + if (direction === Direction.left || direction === Direction.right) { + if (yOffset > 0) { + location.y += yOffset; + } + if (xOffset === 0) { + xOffset = location.x + size.width - 5; + } } - let action = new this._wd.TouchAction(this._driver); + + const endPoint = calculateOffset(direction, location.y, yOffset, location.x, xOffset, this._args.isIOS); + + const action = new this._wd.TouchAction(this._driver); action - .press({ x: x, y: y }) + .press({ x: endPoint.startPoint.x, y: endPoint.startPoint.y }) .wait(endPoint.duration) - .moveTo({ x: endPoint.point.x, y: endPoint.point.y }) + .moveTo({ x: endPoint.endPoint.x, y: endPoint.endPoint.y }) .release(); await action.perform(); await this._driver.sleep(150); @@ -316,7 +344,7 @@ export class UIElement { while (el === null && retries >= 0) { try { el = await elementToSearch(); - if (!el || el === null || !(await el.isDisplayed())) { + if (!el || el === null || !(el && await el.isDisplayed())) { el = null; await this.scroll(direction, yOffset, xOffset); } @@ -329,41 +357,6 @@ export class UIElement { return el; } - /** - * Drag element with specific offset - * @param direction - * @param yOffset - * @param xOffset - default value 0 - */ - public async drag(direction: Direction, yOffset: number, xOffset: number = 0) { - const location = await this.location(); - - const x = location.x === 0 ? 10 : location.x; - const y = location.y === 0 ? 10 : location.y; - - const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, this._webio.isIOS, false); - - if (this._args.isAndroid) { - let action = new this._wd.TouchAction(this._driver); - action - .longPress({ x: x, y: y }) - .wait(endPoint.duration) - .moveTo({ x: yOffset, y: yOffset }) - .release(); - await action.perform(); - } else { - await this._wd.execute(`mobile: dragFromToForDuration`, { - duration: endPoint.duration, - fromX: x, - fromY: y, - toX: xOffset, - toY: yOffset - }); - } - - await this._driver.sleep(150); - } - /** * Click and hold over an element * @param time in milliseconds to increase the default press period. @@ -391,7 +384,7 @@ export class UIElement { if (shouldClearText) { await this.adbDeleteText(adbDeleteCharsCount); } - text = text.replace(" ","%s"); + text = text.replace(" ", "%s"); await this.click(); await adbShellCommand(this._driver, "input", ["text", text]); } else { @@ -474,35 +467,161 @@ export class UIElement { * @param direction */ public async swipe(direction: Direction) { - const rectangle = await this.getRectangle(); - const centerX = rectangle.x + rectangle.width / 2; - const centerY = rectangle.y + rectangle.height / 2; - let swipeX; - if (direction == Direction.right) { - const windowSize = await this._driver.getWindowSize(); - swipeX = windowSize.width - 10; - } else if (direction == Direction.left) { - swipeX = 10; + logInfo(`Swipe direction: `, Direction[direction]); + if (this._args.isIOS) { + await this._driver + .execute('mobile: scroll', { + element: this._element.value, + direction: Direction[direction] + }); } else { - console.log("Provided direction must be left or right !"); + try { + await this.scroll(direction); + } catch (error) { + console.log("", error); + } } + logInfo(`End swipe`); + } + + /** + * Drag element with specific offset + * @experimental + * @param direction + * @param yOffset + * @param xOffset - default value 0 + */ + public async drag(direction: Direction, yOffset: number, xOffset: number = 0, duration?: number) { + direction = direction === Direction.up ? Direction.down : Direction.up; + + const location = await this.location(); + + const x = location.x === 0 ? 10 : location.x; + const y = location.y === 0 ? 10 : location.y; + + const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, this._args.isIOS); + duration = duration || endPoint.duration; if (this._args.isAndroid) { - const action = new this._wd.TouchAction(this._driver); - action.press({ x: centerX, y: centerY }) - .wait(200) - .moveTo({ x: swipeX, y: centerY }) + let action = new this._wd.TouchAction(this._driver); + action + .longPress({ x: x, y: y }) + .wait(duration) + .moveTo({ x: yOffset, y: yOffset }) .release(); await action.perform(); - } - else { - await this._driver.execute('mobile: dragFromToForDuration', { - duration: 2.0, - fromX: centerX, - fromY: centerY, - toX: swipeX, - toY: centerY + } else { + await this._driver.execute(`mobile: dragFromToForDuration`, { + duration: duration, + fromX: x, + fromY: y, + toX: xOffset, + toY: yOffset }); } + + await this._driver.sleep(150); } -} + + /** + *@experimental + * Pan element with specific offset + * @param offsets where the finger should move to. + * @param initPointOffset element.getRectangle() is used as start point. In case some additional offset should be provided use this param. + */ + public async pan(offsets: { x: number, y: number }[], initPointOffset: { x: number, y: number } = { x: 0, y: 0 }) { + logInfo("Start pan gesture!"); + const rect = await this.getRectangle(); + const action = new this._wd.TouchAction(this._driver); + await action.press({ x: rect.x + initPointOffset.x, y: rect.y + initPointOffset.y }).wait(200); + if (offsets.length > 1) { + for (let index = 1; index < offsets.length; index++) { + const p = offsets[index]; + action.moveTo({ x: rect.x + p.x, y: rect.y + p.y }); + } + } + + await action.release().perform(); + logInfo("End pan gesture!"); + } + + /** + * @experimental + * This method will try to move two fingers from opposite corners. + * One finger starts from + * { x: elementRect.x + offset.x, y: elementRect.y + offset.y } + * and ends to + * { x: elementRect.x + elementRect.width - offset.x, y: elementRect.height + elementRect.y - offset.y } + * and the other finger starts from + * { x: elementRect.width + elementRect.x - offset.x, y: elementRect.height + elementRect.y - offset.y } + * and ends to + * { x: elementRect.x + offset.x, y: elementRect.y + offset.y } + */ + public async rotate(offset: { x: number, y: number } = { x: 10, y: 10 }) { + logInfo("Start rotate gesture!"); + const elementRect = await this.getRectangle(); + + const startPoint = { x: elementRect.x + offset.x, y: elementRect.y + offset.y }; + const endPoint = { x: elementRect.x + elementRect.width - offset.x, y: elementRect.height + elementRect.y - offset.y }; + + const multiAction = new this._wd.MultiAction(this._driver); + const action1 = new this._wd.TouchAction(this._driver); + action1 + .press(startPoint) + .wait(100) + .moveTo(endPoint) + .wait(1000) + .release(); + multiAction.add(action1); + + const action2 = new this._wd.TouchAction(this._driver); + action2 + .press(endPoint) + .wait(100) + .moveTo({ x: startPoint.x, y: startPoint.y - 1 }) + .wait(1000) + .release(); + multiAction.add(action2); + + await multiAction.perform(); + logInfo("End rotate gesture!"); + } + + /** + * @experimental + * @param zoomFactory - zoomIn or zoomOut. Only zoomIn action is implemented + * @param offset + */ + public async pinch(zoomType: "in" | "out", offset?: { x: number, y: number }) { + logInfo("Start pinch gesture!"); + const elementRect = await this.getRectangle(); + + offset = offset || { x: elementRect.width / 2, y: elementRect.height / 2 }; + elementRect.y = elementRect.y + elementRect.height / 2; + + const endPoint = { x: offset.x, y: offset.y }; + + const startPointOne = { x: elementRect.x + 20, y: elementRect.y }; + const action1 = new this._wd.TouchAction(this._driver); + action1 + .press(startPointOne) + .wait(100) + .moveTo(endPoint) + .release() + + const multiAction = new this._wd.MultiAction(this._driver); + multiAction.add(action1); + + const startPointTwo = { x: elementRect.x + elementRect.width, y: elementRect.y }; + const action2 = new this._wd.TouchAction(this._driver); + action2 + .press(startPointTwo) + .wait(500) + .moveTo(endPoint) + .release() + multiAction.add(action2); + + await multiAction.perform(); + logInfo("End pinch gesture!"); + } +} \ No newline at end of file diff --git a/lib/utils.d.ts b/lib/utils.d.ts index b1be4f2..2cd64f7 100644 --- a/lib/utils.d.ts +++ b/lib/utils.d.ts @@ -21,8 +21,9 @@ export declare const getStorage: (args: INsCapabilities) => string; export declare function getReportPath(args: INsCapabilities): string; export declare const getRegexResultsAsArray: (regex: any, str: any) => any[]; export declare function getAppPath(caps: INsCapabilities): string; -export declare function calculateOffset(direction: any, y: number, yOffset: number, x: number, xOffset: number, isIOS: boolean, verbose: any): { - point: Point; +export declare function calculateOffset(direction: any, y: number, yOffset: number, x: number, xOffset: number, isIOS: boolean): { + startPoint: Point; + endPoint: Point; duration: number; }; /** @@ -32,7 +33,7 @@ export declare function calculateOffset(direction: any, y: number, yOffset: numb * @param yOffset * @param xOffset */ -export declare function scroll(wd: any, driver: any, direction: Direction, isIOS: boolean, y: number, x: number, yOffset: number, xOffset: number, verbose: any): Promise; +export declare function scroll(wd: any, driver: any, direction: Direction, isIOS: boolean, y: number, x: number, yOffset: number, xOffset: number): Promise; export declare const addExt: (fileName: string, ext: string) => string; export declare const isPortAvailable: (port: any) => Promise<{}>; export declare const findFreePort: (retries?: number, port?: number) => Promise; diff --git a/lib/utils.ts b/lib/utils.ts index aeda908..75985e3 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -242,7 +242,7 @@ export function getStorageByDeviceName(args: INsCapabilities) { storage = createStorageFolder(storage, getDeviceName(args)); logWarn(`Images storage set to: ${storage}!`); - + return storage; } @@ -381,48 +381,55 @@ export function getAppPath(caps: INsCapabilities) { return appFullPath; } -export function calculateOffset(direction, y: number, yOffset: number, x: number, xOffset: number, isIOS: boolean, verbose) { +export function calculateOffset(direction, y: number, yOffset: number, x: number, xOffset: number, isIOS: boolean) { let speed = 10; - let yEnd = Math.abs(yOffset); - let xEnd = Math.abs(xOffset); + let yEnd = y; + let xEnd = x; let duration = Math.abs(yEnd) * speed; - if (isIOS) { - speed = 100; - if (direction === Direction.down) { - direction = -1; - yEnd = direction * yEnd; - } - if (direction === Direction.right) { - direction = -1; - xEnd = direction * xEnd; - } - } else { - if (direction === Direction.down) { - yEnd = Math.abs(yOffset - y); - } - if (direction === Direction.up) { - yEnd = direction * Math.abs((Math.abs(yOffset) + y)); - } - + if (direction === Direction.down) { + yEnd = Math.abs(y); + y = Math.abs(yOffset - y); + duration = Math.abs(yOffset) * speed; + } + if (direction === Direction.up) { + yEnd = Math.abs((Math.abs(y - yOffset))); duration = Math.abs(yOffset) * speed; + } - if (direction === Direction.right) { - xEnd = Math.abs(xOffset - x); - } + if (direction === Direction.right) { + xEnd = Math.abs(x); + x = Math.abs(xOffset - x); + duration = Math.abs(xOffset) * speed; + } - if (direction === Direction.left) { - xEnd = Math.abs(xOffset + x); + if (direction === Direction.left) { + xEnd = Math.abs(xOffset + x); + duration = Math.abs(xOffset) * speed; + const addToX = isIOS ? 50 : 5; + if (isIOS) { + x = x === 0 ? 50 : x; + } else { + x = x === 0 ? 5 : x; } - - if (yOffset < xOffset && x) { - duration = Math.abs(xOffset) * speed; + if (x === 0) { + logInfo(`Changing x to x + ${addToX}, since this will perform navigate back for ios or rise exception in android!`); } + } + if (yOffset < xOffset) { + duration = Math.abs(xOffset) * speed; } - log({ point: new Point(xEnd, yEnd), duration: duration }, verbose); - return { point: new Point(xEnd, yEnd), duration: duration }; + logInfo("Start point: ", new Point(x, y)); + logInfo("End point: ", new Point(xEnd, yEnd)); + logInfo("Scrolling speed: ", duration); + + return { + startPoint: new Point(x, y), + endPoint: new Point(xEnd, yEnd), + duration: duration + }; } /** @@ -432,19 +439,19 @@ export function calculateOffset(direction, y: number, yOffset: number, x: number * @param yOffset * @param xOffset */ -export async function scroll(wd, driver, direction: Direction, isIOS: boolean, y: number, x: number, yOffset: number, xOffset: number, verbose) { +export async function scroll(wd, driver, direction: Direction, isIOS: boolean, y: number, x: number, yOffset: number, xOffset: number) { if (x === 0) { x = 20; } if (y === 0) { y = 20; } - const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, isIOS, verbose); + const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, isIOS); const action = new wd.TouchAction(driver); action .press({ x: x, y: y }) .wait(endPoint.duration) - .moveTo({ x: endPoint.point.x, y: endPoint.point.y }) + .moveTo({ x: endPoint.endPoint.x, y: endPoint.endPoint.y }) .release(); await action.perform(); await driver.sleep(150); @@ -689,7 +696,7 @@ export const logColorized = (bgColor: ConsoleColor, frontColor: ConsoleColor, in export async function adbShellCommand(wd: any, command: string, args: Array) { - await wd.execute('mobile: shell', {"command": command, "args": args}); + await wd.execute('mobile: shell', { "command": command, "args": args }); } enum ConsoleColor { diff --git a/package.json b/package.json index 9535a46..4f0b86e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nativescript-dev-appium", - "version": "6.1.0", + "version": "6.1.3", "description": "A NativeScript plugin to help integrate and run Appium tests", "author": "NativeScript", "license": "MIT", @@ -36,7 +36,7 @@ "frame-comparer": "^2.0.1", "glob": "^7.1.0", "inquirer": "^6.2.0", - "mobile-devices-controller": "^5.2.0", + "mobile-devices-controller": "~5.2.0", "wd": "~1.11.3", "webdriverio": "~4.14.0", "yargs": "~12.0.5" diff --git a/test/device-manager.spec.ts b/test/device-manager.spec.ts index bec3568..163b998 100644 --- a/test/device-manager.spec.ts +++ b/test/device-manager.spec.ts @@ -206,8 +206,8 @@ describe("start-appium-server-ios", async () => { appPath: iosApp, appiumCaps: { platformName: Platform.IOS, - deviceName: /^iPhone XR$/, - platformVersion: /12/, + deviceName: /^iPhone XR 12$/, + platformVersion: "12.2", fullReset: true }, verbose: false @@ -275,7 +275,7 @@ describe("dev-mode-options", async () => { before("start devices", async () => { await DeviceController.startDevice({ platform: Platform.ANDROID, apiLevel: "23" }); - await DeviceController.startDevice({ platform: Platform.IOS, apiLevel: /12./, name: "iPhone XR" }); + await DeviceController.startDevice({ platform: Platform.IOS, apiLevel: "12.2", name: "iPhone XR 12" }); appiumServer = new AppiumServer({}); await appiumServer.start(8399); });