diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index dabe2b2..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); @@ -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) { @@ -491,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); } /** @@ -509,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--; @@ -864,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; @@ -881,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 251766d..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,7 +197,7 @@ 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 { @@ -209,6 +209,7 @@ export class DeviceManager implements IDeviceManager { 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 ... 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/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 0ae8c62..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; } @@ -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 7bc0aef..4f0b86e 100644 --- a/package.json +++ b/package.json @@ -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"