diff --git a/CHANGELOG.md b/CHANGELOG.md
index f60915b..b89928a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,43 @@
+## [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)
+
+
+### Bug Fixes
+
+* deep clone of cropRect from options ([a10689e](https://github.com/NativeScript/nativescript-dev-appium/commit/a10689e))
+* prevent crashing in case device density is missing ([#256](https://github.com/NativeScript/nativescript-dev-appium/issues/256)) ([0d7d101](https://github.com/NativeScript/nativescript-dev-appium/commit/0d7d101))
+* respect appium capabilities provided by the user in the json file ([9485d32](https://github.com/NativeScript/nativescript-dev-appium/commit/9485d32))
+* saving every image when LogImageType.everyImage is provided ([0147399](https://github.com/NativeScript/nativescript-dev-appium/commit/0147399))
+* sendKeys command to input properly string with intervals. Add parameter for chars count to be deleted ([efc0932](https://github.com/NativeScript/nativescript-dev-appium/commit/efc0932))
+* **ios:** ios13 statusbar height ([#265](https://github.com/NativeScript/nativescript-dev-appium/issues/265)) ([5844f86](https://github.com/NativeScript/nativescript-dev-appium/commit/5844f86))
+
+
+### Features
+
+* add the ability to run adb shell commands ([#251](https://github.com/NativeScript/nativescript-dev-appium/issues/251)) ([7246bce](https://github.com/NativeScript/nativescript-dev-appium/commit/7246bce))
+
+
# [6.0.0](https://github.com/NativeScript/nativescript-dev-appium/compare/5.3.0...6.0.0) (2019-08-08)
diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts
index 3563261..c32d76a 100644
--- a/lib/appium-driver.d.ts
+++ b/lib/appium-driver.d.ts
@@ -1,4 +1,3 @@
-export declare const should: any;
import { ElementHelper } from "./element-helper";
import { SearchOptions } from "./search-options";
import { UIElement } from "./ui-element";
@@ -11,6 +10,7 @@ import { ImageHelper } from "./image-helper";
import { ImageOptions } from "./image-options";
import { LogType } from "./log-types";
import { DeviceOrientation } from "./enums/device-orientation";
+import { AndroidKeyEvent } from "mobile-devices-controller";
export declare class AppiumDriver {
private _driver;
private _wd;
@@ -37,7 +37,7 @@ export declare class AppiumDriver {
readonly isIOS: boolean;
readonly driver: any;
/**
- * Get the storage where test results from image comparison is logged It will be reports/app nam/device name
+ * Get the storage where test results from image comparison is logged. The path should be reports/app nam/device name
*/
readonly reportsPath: string;
/**
@@ -265,8 +265,27 @@ export declare class AppiumDriver {
*/
getScreenActualViewPort(): IRectangle;
/**
- * Get screen view port
- * This is convenient to use for some gestures on the screen
+ * Get screen view port.
+ * Provides the view port that is needed for some gestures like swipe etc.
*/
getScreenViewPort(): IRectangle;
+ /**
+ * Android ONLY! Input key event via ADB.
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param keyEvent The event number
+ */
+ adbKeyEvent(keyEvent: number | AndroidKeyEvent): Promise;
+ /**
+ * Android ONLY! Send text via ADB.
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param text The string to send
+ */
+ adbSendText(text: string): Promise;
+ /**
+ * Android ONLY! Execute shell command via ADB.
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param command The command name
+ * @param args Additional arguments
+ */
+ adbShellCommand(command: string, args: Array): Promise;
}
diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts
index fabc4a2..f4615cc 100644
--- a/lib/appium-driver.ts
+++ b/lib/appium-driver.ts
@@ -1,10 +1,5 @@
import * as wd from "wd";
import * as webdriverio from "webdriverio";
-const chai = require("chai");
-const chaiAsPromised = require("chai-as-promised");
-chai.use(chaiAsPromised);
-export const should = chai.should();
-chaiAsPromised.transferPromiseness = wd.transferPromiseness;
import { ElementHelper } from "./element-helper";
import { SearchOptions } from "./search-options";
@@ -16,7 +11,8 @@ import {
DeviceController,
IDevice,
DeviceType,
- AndroidController
+ AndroidController,
+ IOSController
} from "mobile-devices-controller";
import {
addExt,
@@ -33,7 +29,9 @@ import {
getStorage,
encodeImageToBase64,
ensureReportsDirExists,
- checkImageLogType
+ checkImageLogType,
+ adbShellCommand,
+ logWarn
} from "./utils";
import { INsCapabilities } from "./interfaces/ns-capabilities";
@@ -50,6 +48,8 @@ import { LogImageType } from "./enums/log-image-type";
import { DeviceOrientation } from "./enums/device-orientation";
import { NsCapabilities } from "./ns-capabilities";
import { AutomationName } from "./automation-name";
+import { AndroidKeyEvent } from "mobile-devices-controller";
+import { setInterval } from "timers";
export class AppiumDriver {
private _defaultWaitTime: number = 5000;
@@ -120,7 +120,7 @@ export class AppiumDriver {
}
/**
- * Get the storage where test results from image comparison is logged It will be reports/app nam/device name
+ * Get the storage where test results from image comparison is logged. The path should be reports/app nam/device name
*/
get reportsPath() {
return this._args.reportsPath;
@@ -168,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();
}
@@ -218,10 +223,11 @@ export class AppiumDriver {
let hasStarted = false;
let retries = 10;
+ let sessionInfoDetails;
+
while (retries > 0 && !hasStarted) {
try {
let sessionInfo;
- let sessionInfoDetails;
try {
if (args.sessionId || args.attachToDebug) {
const sessionInfos = JSON.parse(((await getSessions(args.port)) || "{}") + '');
@@ -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) {
@@ -314,7 +322,28 @@ export class AppiumDriver {
await driver.updateSettings(appiumCapsFromConfig.settings);
}
- return new AppiumDriver(driver, wd, webio, args.driverConfig, args);
+ if (+sessionInfoDetails.statBarHeight === 0
+ && sessionInfoDetails.platformName.toLowerCase() === "ios"
+ && sessionInfoDetails.platformVersion.startsWith("13")) {
+ try {
+ 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) { }
+ }
+
+ const appiumDriver = new AppiumDriver(driver, wd, webio, args.driverConfig, args);
+ return appiumDriver;
}
public async updateSettings(settings: any) {
@@ -470,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);
}
/**
@@ -488,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--;
@@ -586,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,
@@ -604,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,
@@ -843,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;
@@ -860,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}`);
@@ -1004,22 +1040,55 @@ export class AppiumDriver {
* Useful for image comparison
*/
public getScreenActualViewPort(): IRectangle {
- return (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect;
+ return (this._args.device.viewportRect || this.imageHelper.options.cropRectangle);
}
/**
- * Get screen view port
- * This is convenient to use for some gestures on the screen
+ * Get screen view port.
+ * Provides the view port that is needed for some gestures like swipe etc.
*/
public getScreenViewPort(): IRectangle {
- const rect = (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect;
- if (rect && Object.getOwnPropertyNames(rect).length > 0) {
+ const rect = this.getScreenActualViewPort();
+ if (rect
+ && this.isIOS
+ && Object.getOwnPropertyNames(rect).length > 0
+ && this._args.device.deviceScreenDensity) {
return {
- x: rect.x / this._args.appiumCaps.device.deviceScreenDensity,
- y: rect.y / this._args.appiumCaps.device.deviceScreenDensity,
- width: rect.x / this._args.appiumCaps.device.deviceScreenDensity,
- height: rect.x / this._args.appiumCaps.device.deviceScreenDensity,
+ x: rect.x / this._args.device.deviceScreenDensity,
+ y: rect.y / this._args.device.deviceScreenDensity,
+ width: rect.width / this._args.device.deviceScreenDensity,
+ height: rect.height / this._args.device.deviceScreenDensity,
}
+ } else {
+ return rect;
}
}
+
+ /**
+ * Android ONLY! Input key event via ADB.
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param keyEvent The event number
+ */
+ public async adbKeyEvent(keyEvent: number | AndroidKeyEvent) {
+ await this.adbShellCommand("input", ["keyevent", keyEvent]);
+ }
+
+ /**
+ * Android ONLY! Send text via ADB.
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param text The string to send
+ */
+ public async adbSendText(text: string) {
+ await this.adbShellCommand("input", ["text", text]);
+ }
+
+ /**
+ * Android ONLY! Execute shell command via ADB.
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param command The command name
+ * @param args Additional arguments
+ */
+ public async adbShellCommand(command: string, args: Array) {
+ await adbShellCommand(this._driver, command, args);
+ }
}
\ No newline at end of file
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.d.ts b/lib/device-manager.d.ts
index e535392..c6c8916 100644
--- a/lib/device-manager.d.ts
+++ b/lib/device-manager.d.ts
@@ -13,13 +13,18 @@ export declare class DeviceManager implements IDeviceManager {
static getInstalledApps(device: IDevice): Promise;
static getDefaultDevice(args: INsCapabilities, deviceName?: string, token?: string, type?: DeviceType, platformVersion?: number): IDevice;
private static convertViewportRectToIRectangle;
- static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): IDevice;
+ static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): any;
static setDontKeepActivities(args: INsCapabilities, driver: any, value: any): Promise;
static executeShellCommand(driver: any, commandArgs: {
command: string;
"args": Array;
}): Promise;
- static getDensity(args: INsCapabilities, driver: any): Promise;
+ /**
+ * Android only
+ * @param args
+ * @param driver
+ */
+ static setDensity(args: INsCapabilities, driver: any): Promise;
static applyDeviceAdditionsSettings(driver: any, args: INsCapabilities, sessionInfo: any): Promise;
getPackageId(device: IDevice, appPath: string): string;
private static cleanUnsetProperties;
diff --git a/lib/device-manager.ts b/lib/device-manager.ts
index 604e67a..385f0ab 100644
--- a/lib/device-manager.ts
+++ b/lib/device-manager.ts
@@ -171,12 +171,10 @@ 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 }
}
- delete args.appiumCaps.density;
- delete args.appiumCaps.offsetPixels;
DeviceManager.cleanUnsetProperties(device);
return device;
@@ -199,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;
}
@@ -219,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 ...
@@ -242,7 +241,12 @@ export class DeviceManager implements IDeviceManager {
return output;
}
- public static async getDensity(args: INsCapabilities, driver) {
+ /**
+ * Android only
+ * @param args
+ * @param driver
+ */
+ public static async setDensity(args: INsCapabilities, driver) {
args.device.config = args.device.config || {};
if (args.appiumCaps.platformName.toLowerCase() === "android") {
if (!args.ignoreDeviceController) {
@@ -258,15 +262,6 @@ export class DeviceManager implements IDeviceManager {
if (args.device.config.density) {
args.device.config['offsetPixels'] = AndroidController.calculateScreenOffset(args.device.config.density);
}
- } else {
- IOSController.getDevicesScreenInfo().forEach((v, k, m) => {
- if (args.device.name.includes(k)) {
- args.device.config = {
- density: args.device.config['density'] || v.density,
- offsetPixels: v.actionBarHeight
- };
- }
- });
}
}
@@ -278,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) {
@@ -291,8 +286,8 @@ export class DeviceManager implements IDeviceManager {
args.device.config['offsetPixels'] = AndroidController.calculateScreenOffset(args.device.config.density);
}
- if (!density) {
- await DeviceManager.getDensity(args, driver);
+ if (!density && !args.isIOS) {
+ await DeviceManager.setDensity(args, driver);
density = args.device.config.density
args.device.config['offsetPixels'] = AndroidController.calculateScreenOffset(args.device.config.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 f499009..3842db1 100644
--- a/lib/image-helper.d.ts
+++ b/lib/image-helper.d.ts
@@ -17,8 +17,8 @@ export interface IImageCompareOptions {
*/
toleranceType?: ImageOptions;
/**
- * Wait miliseconds before capture creating image
- * Default value is 2000
+ * Wait milliseconds before capture creating image
+ * Default value is 5000
*/
waitBeforeCreatingInitialImageCapture?: number;
/**
@@ -43,14 +43,14 @@ export interface IImageCompareOptions {
keepOriginalImageSize?: boolean;
/**
* Default value is set to false. nativescript-dev-appium will recalculate view port for iOS
- * so that the top/y will start from the end of status bar
+ * so that the top/y will start from the end of the status bar
* So far appium calculates it even more and some part of safe areas are missed
*/
keepAppiumViewportRect?: boolean;
/**
- * Defines if an image is device specific or only by platform.
- * Default value is true and the image will be saved in device specific directory.
- * If value is set to false, image will be saved under ios or android folder.
+ * Defines if an image is device-specific or only by the platform.
+ * Default value is true and the image will be saved in device-specific directory.
+ * If the value is set to false, the image will be saved under ios or android folder.
*/
isDeviceSpecific?: boolean;
/**
@@ -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 7ead1ae..139024d 100644
--- a/lib/image-helper.ts
+++ b/lib/image-helper.ts
@@ -29,8 +29,8 @@ export interface IImageCompareOptions {
toleranceType?: ImageOptions;
/**
- * Wait miliseconds before capture creating image
- * Default value is 2000
+ * Wait milliseconds before capture creating image
+ * Default value is 5000
*/
waitBeforeCreatingInitialImageCapture?: number;
@@ -53,7 +53,7 @@ export interface IImageCompareOptions {
/**
* Default value is set to true which means that nativescript-dev-appium will save the image
- * in original size and compare only the part which cropRectangle specifies.
+ * in original size and compare only the part which cropRectangle specifies.
* If false, the image size will be reduced and saved by the dimensions of cropRectangle.
*/
keepOriginalImageSize?: boolean;
@@ -61,15 +61,15 @@ export interface IImageCompareOptions {
/**
* Default value is set to false. nativescript-dev-appium will recalculate view port for iOS
- * so that the top/y will start from the end of status bar
+ * so that the top/y will start from the end of the status bar
* So far appium calculates it even more and some part of safe areas are missed
*/
keepAppiumViewportRect?: boolean;
/**
- * Defines if an image is device specific or only by platform.
- * Default value is true and the image will be saved in device specific directory.
- * If value is set to false, image will be saved under ios or android folder.
+ * Defines if an image is device-specific or only by the platform.
+ * Default value is true and the image will be saved in device-specific directory.
+ * If the value is set to false, the image will be saved under ios or android folder.
*/
isDeviceSpecific?: boolean;
@@ -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,
@@ -98,11 +100,13 @@ export class ImageHelper {
};
constructor(private _args: INsCapabilities, private _driver: AppiumDriver) {
- this._defaultOptions.cropRectangle = (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect;
+ if (this._args.device.viewportRect) {
+ ImageHelper.fullClone(this._args.device.viewportRect, this._defaultOptions.cropRectangle)
+ }
if (!this._defaultOptions.cropRectangle
- || !isNumber(this._defaultOptions.cropRectangle.y)) {
+ || !isNumber(this._defaultOptions.cropRectangle.y) || this._args.appiumCaps.offsetPixels > 0) {
this._defaultOptions.cropRectangle = this._defaultOptions.cropRectangle || {};
- this._defaultOptions.cropRectangle.y = this._args.device.config.offsetPixels || 0;
+ this._defaultOptions.cropRectangle.y = this._args.appiumCaps.offsetPixels || this._args.device.config.offsetPixels || 0;
this._defaultOptions.cropRectangle.x = 0;
if (this._args.device.deviceScreenSize && this._args.device.deviceScreenSize.width && this._args.device.deviceScreenSize.height) {
this._defaultOptions.cropRectangle.height = this._args.device.deviceScreenSize.height - this._defaultOptions.cropRectangle.y;
@@ -146,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);
@@ -268,8 +288,12 @@ export class ImageHelper {
const eventStartTime = Date.now().valueOf();
let counter = 1;
options.timeOutSeconds *= 1000;
+ let pathActualImageCounter = resolvePath(this._args.reportsPath, imageName.replace(".", "_actual."));
+ const shouldLogEveryImage = checkImageLogType(this._args.testReporter, LogImageType.everyImage);
while ((Date.now().valueOf() - eventStartTime) <= options.timeOutSeconds && !result) {
- const pathActualImageCounter = resolvePath(this._args.reportsPath, imageName.replace(".", "_actual_" + counter + "."));
+ if (shouldLogEveryImage) {
+ pathActualImageCounter = resolvePath(this._args.reportsPath, imageName.replace(".", "_actual_" + counter + "."));
+ }
pathActualImage = await this._driver.saveScreenshot(pathActualImageCounter);
if (!options.keepOriginalImageSize) {
await this.clipRectangleImage(options.cropRectangle, pathActualImage);
@@ -308,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 386ae5d..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: import("mobile-devices-controller/lib/device").IDevice, 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 167d580..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,23 +125,19 @@ 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.
*/
hold(time?: number): Promise;
/**
* Send keys to field or other UI component
- * @param text
- * @param shouldClearText, default value is true
+ * @param text The string to input
+ * @param shouldClearText Clears existing input before send new one - default value is 'true'
+ * @param useAdb default value is false. Usable for Android ONLY !
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param adbDeleteCharsCount default value is 10. Usable for Android ONLY when 'useAdb' and 'shouldClearText' are True!
*/
- sendKeys(text: string, shouldClearText?: boolean): Promise;
+ sendKeys(text: string, shouldClearText?: boolean, useAdb?: boolean, adbDeleteCharsCount?: number): Promise;
/**
* Type text to field or other UI component
* @param text
@@ -142,9 +150,15 @@ export declare class UIElement {
*/
pressKeycode(keyCode: number): Promise;
/**
- * Clears text form ui element
+ * Clears text from ui element
*/
clearText(): Promise;
+ /**
+ * Clears text from ui element with ADB. Android ONLY !
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param charactersCount Characters count to delete. (Optional - default value 10)
+ */
+ adbDeleteText(charactersCount?: number): Promise;
log(): Promise;
refetch(): Promise;
/**
@@ -156,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 4b940e2..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 } 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);
}
@@ -49,7 +54,7 @@ export class UIElement {
* This method is not working very good with UiAutomator2
* It is better to use click instead.
*/
-
+
public async tap() {
if (this._args.automationName == AutomationName.UiAutomator2) {
return await this.tapCenter();
@@ -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,15 +276,15 @@ export class UIElement {
*/
public async getActualRectangle() {
const actRect = await this.getRectangle();
+ const density = this._args.device.deviceScreenDensity;
if (this._args.isIOS) {
- const density = this._args.device.config.density;
if (density) {
actRect.x *= density;
actRect.y *= density;
actRect.width *= density;
actRect.height *= density;
} else {
- throw new Error("Device's density is undefined!");
+ logError("Device's density is undefined!");
}
}
return actRect;
@@ -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.
@@ -380,14 +373,26 @@ export class UIElement {
/**
* Send keys to field or other UI component
- * @param text
- * @param shouldClearText, default value is true
+ * @param text The string to input
+ * @param shouldClearText Clears existing input before send new one - default value is 'true'
+ * @param useAdb default value is false. Usable for Android ONLY !
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param adbDeleteCharsCount default value is 10. Usable for Android ONLY when 'useAdb' and 'shouldClearText' are True!
*/
- public async sendKeys(text: string, shouldClearText: boolean = true) {
- if (shouldClearText) {
- await this.clearText();
+ public async sendKeys(text: string, shouldClearText: boolean = true, useAdb: boolean = false, adbDeleteCharsCount: number = 10) {
+ if (useAdb && this._args.isAndroid) {
+ if (shouldClearText) {
+ await this.adbDeleteText(adbDeleteCharsCount);
+ }
+ text = text.replace(" ", "%s");
+ await this.click();
+ await adbShellCommand(this._driver, "input", ["text", text]);
+ } else {
+ if (shouldClearText) {
+ await this.clearText();
+ }
+ await this._element.sendKeys(text);
}
- await this._element.sendKeys(text);
}
/**
@@ -407,17 +412,30 @@ export class UIElement {
* @param key code
*/
public async pressKeycode(keyCode: number) {
- await this._driver.pressKeycode(keyCode);
+ await this._driver.pressKeyCode(keyCode);
}
/**
- * Clears text form ui element
+ * Clears text from ui element
*/
public async clearText() {
await this.click();
await this._element.clear();
}
+ /**
+ * Clears text from ui element with ADB. Android ONLY !
+ * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too.
+ * @param charactersCount Characters count to delete. (Optional - default value 10)
+ */
+ public async adbDeleteText(charactersCount: number = 10) {
+ await this.click();
+ for (let index = 0; index < charactersCount; index++) {
+ // Keyevent 67 Delete (backspace)
+ await adbShellCommand(this._driver, "input", ["keyevent", AndroidKeyEvent.KEYCODE_DEL]);
+ }
+ }
+
public async log() {
const el = await this.element();
console.dir(el);
@@ -449,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 764e2ca..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;
@@ -51,6 +52,7 @@ export declare function logWarn(info: any, obj?: any): void;
export declare function logError(info: any, obj?: any): void;
export declare function log(message: any, verbose: any): void;
export declare const logColorized: (bgColor: ConsoleColor, frontColor: ConsoleColor, info: any) => void;
+export declare function adbShellCommand(wd: any, command: string, args: Array): Promise;
declare enum ConsoleColor {
Reset = "\u001B[0m",
Bright = "\u001B[1m",
diff --git a/lib/utils.ts b/lib/utils.ts
index 7df007e..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);
@@ -687,6 +694,11 @@ export const logColorized = (bgColor: ConsoleColor, frontColor: ConsoleColor, in
console.log(`${ConsoleColor.BgYellow}${ConsoleColor.FgBlack}%s${ConsoleColor.Reset}`, info);
}
+
+export async function adbShellCommand(wd: any, command: string, args: Array) {
+ await wd.execute('mobile: shell', { "command": command, "args": args });
+}
+
enum ConsoleColor {
Reset = "\x1b[0m",
Bright = "\x1b[1m",
diff --git a/package.json b/package.json
index 8988766..4f0b86e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nativescript-dev-appium",
- "version": "6.0.0",
+ "version": "6.1.3",
"description": "A NativeScript plugin to help integrate and run Appium tests",
"author": "NativeScript",
"license": "MIT",
@@ -33,12 +33,10 @@
"dependencies": {
"app-root-path": "~2.1.0",
"blink-diff": "~1.0.13",
- "chai": "~4.2.0",
- "chai-as-promised": "~7.1.0",
"frame-comparer": "^2.0.1",
"glob": "^7.1.0",
"inquirer": "^6.2.0",
- "mobile-devices-controller": "~5.0.0",
+ "mobile-devices-controller": "~5.2.0",
"wd": "~1.11.3",
"webdriverio": "~4.14.0",
"yargs": "~12.0.5"
diff --git a/samples/e2e-ts/tsconfig.json b/samples/e2e-ts/tsconfig.json
index 8f790fe..b6946e2 100644
--- a/samples/e2e-ts/tsconfig.json
+++ b/samples/e2e-ts/tsconfig.json
@@ -5,6 +5,7 @@
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"importHelpers": false,
+ "sourceMap": true,
"types": [ ],
"lib": [
"es2015",
diff --git a/samples/e2e-ts/typescript.mocha.sample.e2e-spec.mochawesome.ts b/samples/e2e-ts/typescript.mocha.sample.e2e-spec.mochawesome.ts
index 0d1a2ac..ccd4a08 100644
--- a/samples/e2e-ts/typescript.mocha.sample.e2e-spec.mochawesome.ts
+++ b/samples/e2e-ts/typescript.mocha.sample.e2e-spec.mochawesome.ts
@@ -2,7 +2,7 @@ import { AppiumDriver, createDriver, SearchOptions, nsCapabilities } from "nativ
import { assert } from "chai";
const addContext = require('mochawesome/addContext');
-describe("sample scenario", () => {
+describe("sample scenario", async function(){
let driver: AppiumDriver;
before(async function(){
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);
});