From 7e18f98d101df72cdeb1ffb8f753bb0170cdfb8a Mon Sep 17 00:00:00 2001 From: rosen-vladimirov <rosen.vladimirov@telerik.com> Date: Thu, 14 Jul 2016 10:54:25 +0300 Subject: [PATCH 1/2] Implement new events for debuggable WebViews detection Implement `debuggableViewFound`, `debuggableViewLost` and `debuggableViewChanged` events that will be raised when there's a change in application's debuggable views. Add new method to Public API: `getDebuggableViews` which will return debuggable views per application on device. --- README.md | 210 +++++++++++++++++- appbuilder/device-emitter.ts | 12 + definitions/mobile.d.ts | 57 ++++- mobile/android/android-application-manager.ts | 25 +++ mobile/application-manager-base.ts | 51 ++++- mobile/ios/device/ios-application-manager.ts | 5 + .../ios-simulator-application-manager.ts | 5 + mobile/mobile-core/android-process-service.ts | 129 +++++++---- mobile/mobile-core/devices-service.ts | 10 + package.json | 2 +- test/unit-tests/appbuilder/device-emitter.ts | 57 ++++- .../mobile/application-manager-base.ts | 196 +++++++++++++++- 12 files changed, 709 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 1606723d..c0a0289b 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,14 @@ require("mobile-cli-lib").deviceEmitter.on("applicationInstalled", function(ide }); ``` +* `applicationUninstalled` - Raised when application is removed from device. The callback has two arguments - `deviceIdentifier` and `applicationIdentifier`. <br/><br/> +Sample usage: +```JavaScript +require("mobile-cli-lib").deviceEmitter.on("applicationUninstalled", function(identifier, applicationIdentifier) { + console.log("Application " + applicationIdentifier + " has been uninstalled from device with id: " + identifier); +}); +``` + * `debuggableAppFound` - Raised when application on a device becomes available for debugging. The callback has one argument - `applicationInfo`. <br/><br/> Sample usage: ```JavaScript @@ -327,13 +335,71 @@ Sample result for `applicationInfo` will be: } ``` -* `applicationUninstalled` - Raised when application is removed from device. The callback has two arguments - `deviceIdentifier` and `applicationIdentifier`. <br/><br/> +* `debuggableViewFound` - Raised when a new debuggable WebView is found. The callback has three arguments - `deviceIdentifier`, `appIdentifier` and `webViewInfo`. + Sample usage: ```JavaScript -require("mobile-cli-lib").deviceEmitter.on("applicationUninstalled", function(identifier, applicationIdentifier) { - console.log("Application " + applicationIdentifier + " has been uninstalled from device with id: " + identifier); +require("mobile-cli-lib") + .deviceEmitter.on("debuggableViewFound", function(deviceIdentifier, appIdentifier, debuggableViewInfo) { + console.log("On device " + deviceIdentifier + " the application " + appIdentifier + " now has new WebView: " + debuggableViewInfo); }); ``` +Sample result for `debuggableViewInfo` will be: +```JSON +{ + "description": "", + "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@211d45a5b74b06d12bb016f3c4d54095faf2646f/inspector.html?ws=127.0.0.1:53213/devtools/page/4050", + "id": "4050", + "title": "New tab", + "type": "page", + "url": "chrome-native://newtab/", + "webSocketDebuggerUrl": "ws://127.0.0.1:53213/devtools/page/4050" +} +``` + +* `debuggableViewLost` - Raised when a debuggable WebView is lost. The callback has three arguments - `deviceIdentifier`, `appIdentifier` and `webViewInfo`. + +Sample usage: +```JavaScript +require("mobile-cli-lib") + .deviceEmitter.on("debuggableViewLost", function(deviceIdentifier, appIdentifier, debuggableViewInfo) { + console.log("On device " + deviceIdentifier + " the application " + appIdentifier + " now cannot debug WebView: " + debuggableViewInfo); +}); +``` +Sample result for `debuggableViewInfo` will be: +```JSON +{ + "description": "", + "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@211d45a5b74b06d12bb016f3c4d54095faf2646f/inspector.html?ws=127.0.0.1:53213/devtools/page/4050", + "id": "4050", + "title": "New tab", + "type": "page", + "url": "chrome-native://newtab/", + "webSocketDebuggerUrl": "ws://127.0.0.1:53213/devtools/page/4050" +} +``` + +* `debuggableViewChanged` - Raised when a property of debuggable WebView is changed, for example it's title. The callback has three arguments - `deviceIdentifier`, `appIdentifier` and `webViewInfo`. + +Sample usage: +```JavaScript +require("mobile-cli-lib") + .deviceEmitter.on("debuggableViewChanged", function(deviceIdentifier, appIdentifier, debuggableViewInfo) { + console.log("On device " + deviceIdentifier + " the application " + appIdentifier + " has changes in WebView: " + debuggableViewInfo); +}); +``` +Sample result for `debuggableViewInfo` will be: +```JSON +{ + "description": "", + "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@211d45a5b74b06d12bb016f3c4d54095faf2646f/inspector.html?ws=127.0.0.1:53213/devtools/page/4050", + "id": "4050", + "title": "New tab 2", + "type": "page", + "url": "chrome-native://newtab/", + "webSocketDebuggerUrl": "ws://127.0.0.1:53213/devtools/page/4050" +} +``` * `companionAppInstalled` - Raised when application is removed from device. The callback has two arguments - `deviceIdentifier` and `framwork`. <br/><br/> Sample usage: @@ -548,6 +614,144 @@ Sample result will be: }]] ``` +* `getDebuggableApps(deviceIdentifiers: string[]): Promise<IDeviceApplicationInformation[]>[]` - This function checks the proc/net/unix file of each device from the deviceIdentifiers argument for web views connected to abstract ports and returns information about the applications. +```JavaScript +/** + * Describes basic information about application on device. + */ +interface IDeviceApplicationInformation { + /** + * The device identifier. + */ + deviceIdentifier: string; + + /** + * The application identifier. + */ + appIdentifier: string; + + /** + * The framework of the project (Cordova or NativeScript). + */ + framework: string; +} +``` + +Sample usage: +```JavaScript +Promise.all(require("mobile-cli-lib").devicesService.getDebuggableApps(["4df18f307d8a8f1b", "JJY5KBTW75TCHQUK"])) + .then(function(data) { + data.forEach(function(apps) { + console.log(apps); + }); + }, function(err) { + console.log(err); + }); +``` +Sample result will be: +```JSON +[[{ + "deviceIdentifier": "4df18f307d8a8f1b", + "appIdentifier": "com.telerik.Fitness", + "framework": "NativeScript" +}, { + "deviceIdentifier": "4df18f307d8a8f1b", + "appIdentifier": "com.telerik.livesynctest", + "framework": "Cordova" +}], [{ + "deviceIdentifier": "JJY5KBTW75TCHQUK", + "appIdentifier": "com.telerik.PhotoAlbum", + "framework": "NativeScript" +}]] +``` + +* `getDebuggableViews(deviceIdentifier: string, appIdentifier: string): Promise<IDebugWebViewInfo[]>` - This function returns WebViews that can be debugged for specified application on specified device. +> NOTE: This method works only for Cordova based applications. DO NOT pass appIdentifier of NativeScript application. + +```JavaScript +/** + * Describes information for WebView that can be debugged. + */ +interface IDebugWebViewInfo { + /** + * Short description of the view. + */ + description: string; + + /** + * Url to the devtools. + * @example http://chrome-devtools-frontend.appspot.com/serve_rev/@211d45a5b74b06d12bb016f3c4d54095faf2646f/inspector.html?ws=127.0.0.1:53213/devtools/page/4024 + */ + devtoolsFrontendUrl: string; + + /** + * Unique identifier of the web view. Could be number or GUID. + * @example 4027 + */ + id: string; + + /** + * Title of the WebView. + * @example https://bit.ly/12345V is not available + */ + title: string; + + /** + * Type of the WebView. + * @example page + */ + type: string; + + /** + * URL loaded in the view. + * @example https://bit.ly/12345V + */ + url: string; + + /** + * Debugger URL. + * @example ws://127.0.0.1:53213/devtools/page/4027 + */ + webSocketDebuggerUrl: string; +} +``` + +Sample usage: +```JavaScript +require("mobile-cli-lib") + .devicesService + .getDebuggableViews("4df18f307d8a8f1b", "com.telerik.cordovaApp") + .then(function(data) { + console.log(data); + }, function(err) { + console.log(err); + }); +``` + +Sample result will be: +```JSON +[{ + "description": "", + "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@211d45a5b74b06d12bb016f3c4d54095faf2646f/inspector.html?ws=127.0.0.1:53213/devtools/page/4050", + "id": "4050", + "title": "New tab", + "type": "page", + "url": "chrome-native://newtab/", + "webSocketDebuggerUrl": "ws://127.0.0.1:53213/devtools/page/4050" + }, + + { + "description": "", + "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@211d45a5b74b06d12bb016f3c4d54095faf2646f/inspector.html?ws=127.0.0.1:53213/devtools/page/4032", + "id": "4032", + "title": "New tab", + "type": "page", + "url": "chrome-native://newtab/", + "webSocketDebuggerUrl": "ws://127.0.0.1:53213/devtools/page/4032" + } +] +``` + ### Module liveSyncService > Stability: 1 - Could be changed due to some new requirments. diff --git a/appbuilder/device-emitter.ts b/appbuilder/device-emitter.ts index 17fd18e5..bb1c075e 100644 --- a/appbuilder/device-emitter.ts +++ b/appbuilder/device-emitter.ts @@ -85,6 +85,18 @@ export class DeviceEmitter extends EventEmitter { device.applicationManager.on("debuggableAppLost", (debuggableAppInfo: Mobile.IDeviceApplicationInformation) => { this.emit("debuggableAppLost", debuggableAppInfo); }); + + device.applicationManager.on("debuggableViewFound", (appIdentifier: string, debuggableWebViewInfo: Mobile.IDebugWebViewInfo) => { + this.emit("debuggableViewFound", device.deviceInfo.identifier, appIdentifier, debuggableWebViewInfo); + }); + + device.applicationManager.on("debuggableViewLost", (appIdentifier: string, debuggableWebViewInfo: Mobile.IDebugWebViewInfo) => { + this.emit("debuggableViewLost", device.deviceInfo.identifier, appIdentifier, debuggableWebViewInfo); + }); + + device.applicationManager.on("debuggableViewChanged", (appIdentifier: string, debuggableWebViewInfo: Mobile.IDebugWebViewInfo) => { + this.emit("debuggableViewChanged", device.deviceInfo.identifier, appIdentifier, debuggableWebViewInfo); + }); } private checkCompanionAppChanged(device: Mobile.IDevice, applicationName: string, eventName: string): void { diff --git a/definitions/mobile.d.ts b/definitions/mobile.d.ts index 3b514751..30369de6 100644 --- a/definitions/mobile.d.ts +++ b/definitions/mobile.d.ts @@ -197,6 +197,7 @@ declare module Mobile { getApplicationInfo(applicationIdentifier: string): IFuture<Mobile.IApplicationInfo>; tryStartApplication(appIdentifier: string, framework?: string): IFuture<void>; getDebuggableApps(): IFuture<Mobile.IDeviceApplicationInformation[]>; + getDebuggableAppViews(appIdentifiers: string[]): IFuture<IDictionary<Mobile.IDebugWebViewInfo[]>>; } /** @@ -300,7 +301,7 @@ declare module Mobile { getDeviceByIdentifier(identifier: string): Mobile.IDevice; mapAbstractToTcpPort(deviceIdentifier: string, appIdentifier: string): IFuture<string>; detectCurrentlyAttachedDevices(): IFuture<void>; - startEmulator(platform?: string): IFuture<void>; + startEmulator(platform?: string): IFuture<void>; } /** @@ -321,6 +322,60 @@ declare module Mobile { * @return {Mobile.IDeviceApplicationInformation[]} Returns array of applications information for the applications which are available for debugging. */ getDebuggableApps(deviceIdentifier: string): IFuture<Mobile.IDeviceApplicationInformation[]>; + + /** + * Gets all mapped abstract to tcp ports for specified device id and application identifiers. + * @param deviceIdentifier {string} The identifier of the device. + * @param appIdentifiers {string[]} Application identifiers that will be checked. + * @return {IFuture<IDictionary<number>>} Dictionary, where the keys are app identifiers and the values are local ports. + */ + getMappedAbstractToTcpPorts(deviceIdentifier: string, appIdentifiers: string[]): IFuture<IDictionary<number>>; + } + + /** + * Describes information for WebView that can be debugged. + */ + interface IDebugWebViewInfo { + /** + * Short description of the view. + */ + description: string; + + /** + * Url to the devtools. + * @example http://chrome-devtools-frontend.appspot.com/serve_rev/@211d45a5b74b06d12bb016f3c4d54095faf2646f/inspector.html?ws=127.0.0.1:53213/devtools/page/4024 + */ + devtoolsFrontendUrl: string; + + /** + * Unique identifier of the web view. Could be number or GUID. + * @example 4027 + */ + id: string; + + /** + * Title of the WebView. + * @example https://bit.ly/12345V is not available + */ + title: string; + + /** + * Type of the WebView. + * @example page + */ + type: string; + + /** + * URL loaded in the view. + * @example https://bit.ly/12345V + */ + url: string; + + /** + * Debugger URL. + * @example ws://127.0.0.1:53213/devtools/page/4027 + */ + webSocketDebuggerUrl: string; } interface IiTunesValidator { diff --git a/mobile/android/android-application-manager.ts b/mobile/android/android-application-manager.ts index da4479fa..72868f76 100644 --- a/mobile/android/android-application-manager.ts +++ b/mobile/android/android-application-manager.ts @@ -11,6 +11,7 @@ export class AndroidApplicationManager extends ApplicationManagerBase { private $options: ICommonOptions, private $logcatHelper: Mobile.ILogcatHelper, private $androidProcessService: Mobile.IAndroidProcessService, + private $httpClient: Server.IHttpClient, $logger: ILogger) { super($logger); } @@ -83,6 +84,30 @@ export class AndroidApplicationManager extends ApplicationManagerBase { return this.$androidProcessService.getDebuggableApps(this.identifier); } + public getDebuggableAppViews(appIdentifiers: string[]): IFuture<IDictionary<Mobile.IDebugWebViewInfo[]>> { + return ((): IDictionary<Mobile.IDebugWebViewInfo[]> => { + let mappedAppIdentifierPorts = this.$androidProcessService.getMappedAbstractToTcpPorts(this.identifier, appIdentifiers).wait(), + applicationViews: IDictionary<Mobile.IDebugWebViewInfo[]> = {}; + + _.each(mappedAppIdentifierPorts, (port: number, appIdentifier: string) => { + applicationViews[appIdentifier] = []; + let localAddress = `http://127.0.0.1:${port}/json`; + + try { + if (port) { + let apps = this.$httpClient.httpRequest(localAddress).wait().body; + applicationViews[appIdentifier] = JSON.parse(apps); + } + } catch (err) { + this.$logger.trace(`Error while checking ${localAddress}. Error is: ${err.message}`); + } + }); + + return applicationViews; + + }).future<IDictionary<Mobile.IDebugWebViewInfo[]>>()(); + } + private getStartPackageActivity(framework?: string): string { framework = framework || ""; return startPackageActivityNames[framework.toLowerCase()] || this.$staticConfig.START_PACKAGE_ACTIVITY_NAME; diff --git a/mobile/application-manager-base.ts b/mobile/application-manager-base.ts index 65e4ba2e..12dd95ff 100644 --- a/mobile/application-manager-base.ts +++ b/mobile/application-manager-base.ts @@ -1,8 +1,10 @@ import { EventEmitter } from "events"; +import {TARGET_FRAMEWORK_IDENTIFIERS} from "../constants"; export abstract class ApplicationManagerBase extends EventEmitter implements Mobile.IDeviceApplicationManager { private lastInstalledAppIdentifiers: string[]; private lastAvailableDebuggableApps: Mobile.IDeviceApplicationInformation[]; + private lastAvailableDebuggableAppViews: IDictionary<Mobile.IDebugWebViewInfo[]> = {}; constructor(protected $logger: ILogger) { super(); @@ -82,6 +84,7 @@ export abstract class ApplicationManagerBase extends EventEmitter implements Mob public abstract getApplicationInfo(applicationIdentifier: string): IFuture<Mobile.IApplicationInfo>; public abstract canStartApplication(): boolean; public abstract getDebuggableApps(): IFuture<Mobile.IDeviceApplicationInformation[]>; + public abstract getDebuggableAppViews(appIdentifiers: string[]): IFuture<IDictionary<Mobile.IDebugWebViewInfo[]>>; private checkForAvailableDebuggableAppsChanges(): IFuture<void> { return (() => { @@ -93,8 +96,52 @@ export abstract class ApplicationManagerBase extends EventEmitter implements Mob this.lastAvailableDebuggableApps = currentlyAvailableDebuggableApps; - _.each(newAvailableDebuggableApps, (appInfo: Mobile.IDeviceApplicationInformation) => this.emit("debuggableAppFound", appInfo)); - _.each(notAvailableAppsForDebugging, (appInfo: Mobile.IDeviceApplicationInformation) => this.emit("debuggableAppLost", appInfo)); + _.each(newAvailableDebuggableApps, (appInfo: Mobile.IDeviceApplicationInformation) => { + this.emit("debuggableAppFound", appInfo); + }); + + _.each(notAvailableAppsForDebugging, (appInfo: Mobile.IDeviceApplicationInformation) => { + this.emit("debuggableAppLost", appInfo); + + if (_.has(this.lastAvailableDebuggableAppViews, appInfo.appIdentifier)) { + // Prevent emitting debuggableViewLost when application cannot be debugged anymore. + delete this.lastAvailableDebuggableAppViews[appInfo.appIdentifier]; + } + }); + + let cordovaDebuggableAppIdentifiers = _(currentlyAvailableDebuggableApps) + .filter(c => c.framework === TARGET_FRAMEWORK_IDENTIFIERS.Cordova) + .map(c => c.appIdentifier) + .value(); + + let currentlyAvailableAppViews = this.getDebuggableAppViews(cordovaDebuggableAppIdentifiers).wait(); + + _.each(currentlyAvailableAppViews, (currentlyAvailableViews, appIdentifier) => { + let previouslyAvailableViews = this.lastAvailableDebuggableAppViews[appIdentifier]; + + let newAvailableViews = _.differenceBy(currentlyAvailableViews, previouslyAvailableViews, "id"); + let notAvailableViews = _.differenceBy(previouslyAvailableViews, currentlyAvailableViews, "id"); + + _.each(notAvailableViews, debugWebViewInfo => { + this.emit("debuggableViewLost", appIdentifier, debugWebViewInfo); + }); + + _.each(newAvailableViews, debugWebViewInfo => { + this.emit("debuggableViewFound", appIdentifier, debugWebViewInfo); + }); + + // Determine which of the views had changed since last check and raise debuggableViewChanged event for them: + let keptViews = _.differenceBy(currentlyAvailableViews, newAvailableViews, "id"); + _.each(keptViews, view => { + let previousTimeViewInfo = _.find(previouslyAvailableViews, previousView => previousView.id === view.id); + if (!_.isEqual(view, previousTimeViewInfo)) { + this.emit("debuggableViewChanged", appIdentifier, view); + } + }); + + this.lastAvailableDebuggableAppViews[appIdentifier] = currentlyAvailableViews; + }); + }).future<void>()(); } } diff --git a/mobile/ios/device/ios-application-manager.ts b/mobile/ios/device/ios-application-manager.ts index 1778097d..6a9930cb 100644 --- a/mobile/ios/device/ios-application-manager.ts +++ b/mobile/ios/device/ios-application-manager.ts @@ -231,6 +231,11 @@ export class IOSApplicationManager extends ApplicationManagerBase { return Future.fromResult([]); } + public getDebuggableAppViews(appIdentifiers: string[]): IFuture<IDictionary<Mobile.IDebugWebViewInfo[]>> { + // Implement when we can find debuggable applications for iOS. + return Future.fromResult(null); + } + private lookupApplications(): IDictionary<Mobile.IDeviceApplication> { let func = () => { let dictionaryPointer = ref.alloc(CoreTypes.cfDictionaryRef); diff --git a/mobile/ios/simulator/ios-simulator-application-manager.ts b/mobile/ios/simulator/ios-simulator-application-manager.ts index 09e3149a..8dba3b66 100644 --- a/mobile/ios/simulator/ios-simulator-application-manager.ts +++ b/mobile/ios/simulator/ios-simulator-application-manager.ts @@ -95,4 +95,9 @@ export class IOSSimulatorApplicationManager extends ApplicationManagerBase { public getDebuggableApps(): IFuture<Mobile.IDeviceApplicationInformation[]> { return Future.fromResult([]); } + + public getDebuggableAppViews(appIdentifiers: string[]): IFuture<IDictionary<Mobile.IDebugWebViewInfo[]>> { + // Implement when we can find debuggable applications for iOS. + return Future.fromResult(null); + } } diff --git a/mobile/mobile-core/android-process-service.ts b/mobile/mobile-core/android-process-service.ts index ec6bbc14..2a486638 100644 --- a/mobile/mobile-core/android-process-service.ts +++ b/mobile/mobile-core/android-process-service.ts @@ -8,7 +8,6 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { constructor(private $errors: IErrors, private $staticConfig: Config.IStaticConfig, private $injector: IInjector, - private $httpClient: Server.IHttpClient, private $net: INet) { this._devicesAdbs = {}; } @@ -27,14 +26,15 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { public mapAbstractToTcpPort(deviceIdentifier: string, appIdentifier: string): IFuture<string> { return (() => { let adb = this.getAdb(deviceIdentifier); - let processId = this.getProcessId(adb, appIdentifier).wait(); + let processId = this.getProcessIds(adb, [appIdentifier]).wait()[appIdentifier]; let applicationNotStartedErrorMessage = `The application is not started on the device with identifier ${deviceIdentifier}.`; if (!processId) { this.$errors.failWithoutHelp(applicationNotStartedErrorMessage); } - let abstractPort = this.getAbstractPortForApplication(adb, processId, appIdentifier).wait(); + let abstractPortsInformation = this.getAbstractPortsInformation(adb).wait(); + let abstractPort = this.getAbstractPortForApplication(adb, processId, appIdentifier, abstractPortsInformation).wait(); if (!abstractPort) { this.$errors.failWithoutHelp(applicationNotStartedErrorMessage); @@ -51,6 +51,39 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { }).future<string>()(); } + public getMappedAbstractToTcpPorts(deviceIdentifier: string, appIdentifiers: string[]): IFuture<IDictionary<number>> { + return ((): IDictionary<number> => { + let adb = this.getAdb(deviceIdentifier), + abstractPortsInformation = this.getAbstractPortsInformation(adb).wait(), + processIds = this.getProcessIds(adb, appIdentifiers).wait(), + adbForwardList = adb.executeCommand(["forward", "--list"]).wait(), + localPorts: IDictionary<number> = {}; + + _.each(appIdentifiers, appIdentifier => { + localPorts[appIdentifier] = null; + let processId = processIds[appIdentifier]; + + if (!processId) { + return; + } + + let abstractPort = this.getAbstractPortForApplication(adb, processId, appIdentifier, abstractPortsInformation).wait(); + + if (!abstractPort) { + return; + } + + let localPort = this.getAlreadyMappedPort(adb, deviceIdentifier, abstractPort, adbForwardList).wait(); + + if (localPort) { + localPorts[appIdentifier] = localPort; + } + }); + + return localPorts; + }).future<IDictionary<number>>()(); + } + public getDebuggableApps(deviceIdentifier: string): IFuture<Mobile.IDeviceApplicationInformation[]> { return ((): Mobile.IDeviceApplicationInformation[] => { let adb = this.getAdb(deviceIdentifier); @@ -58,9 +91,11 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { // TODO: Add tests and make sure only unique names are returned. return _(androidWebViewPortInformation) - .map((line: string) => this.getApplicationInfoFromWebViewPortInformation(adb, deviceIdentifier, line).wait()) - .filter(appIdentifier => !!appIdentifier) - .uniqBy("appIdentifier") + .map(line => this.getApplicationInfoFromWebViewPortInformation(adb, deviceIdentifier, line).wait() + || this.getNativeScriptApplicationInformation(adb, deviceIdentifier, line).wait() + ) + .filter(deviceAppInfo => !!deviceAppInfo) + .uniqBy(deviceAppInfo => deviceAppInfo.appIdentifier) .value(); }).future<Mobile.IDeviceApplicationInformation[]>()(); } @@ -68,29 +103,23 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { private getApplicationInfoFromWebViewPortInformation(adb: Mobile.IDeviceAndroidDebugBridge, deviceIdentifier: string, information: string): IFuture<Mobile.IDeviceApplicationInformation> { return ((): Mobile.IDeviceApplicationInformation => { // Need to search by processId to check for old Android webviews (@webview_devtools_remote_<processId>). - let processIdRegExp = /@webview_devtools_remote_(.+)/g; - let processIdMatches = processIdRegExp.exec(information); - let oldAndroidWebViewAppIdentifier: string; + let processIdRegExp = /@webview_devtools_remote_(.+)/g, + processIdMatches = processIdRegExp.exec(information), + cordovaAppIdentifier: string; + if (processIdMatches) { let processId = processIdMatches[1]; - // Process information will look like this (without the columns names): - // USER PID PPID VSIZE RSS WCHAN PC NAME - // u0_a63 25512 1334 1519560 96040 ffffffff f76a8f75 S com.telerik.appbuildertabstest - let processIdInformation: string = adb.executeShellCommand(["ps", "|grep", processId]).wait(); - oldAndroidWebViewAppIdentifier = _.last(processIdInformation.trim().split(/[ \t]/)); - } - - // Search for appIdentifier (@<appIdentifier>_devtools_remote). - let chromeAppIdentifierRegExp = /@(.+)_devtools_remote\s?/g; - let chromeAppIdentifierMatches = chromeAppIdentifierRegExp.exec(information); - let chromeAppIdentifier: string; - - if (chromeAppIdentifierMatches && chromeAppIdentifierMatches.length > 0) { - chromeAppIdentifier = chromeAppIdentifierMatches[1]; + cordovaAppIdentifier = this.getApplicationIdentifierFromPid(adb, processId).wait(); + } else { + // Search for appIdentifier (@<appIdentifier>_devtools_remote). + let chromeAppIdentifierRegExp = /@(.+)_devtools_remote\s?/g; + let chromeAppIdentifierMatches = chromeAppIdentifierRegExp.exec(information); + + if (chromeAppIdentifierMatches && chromeAppIdentifierMatches.length > 0) { + cordovaAppIdentifier = chromeAppIdentifierMatches[1]; + } } - let cordovaAppIdentifier = oldAndroidWebViewAppIdentifier || chromeAppIdentifier; - if (cordovaAppIdentifier) { return { deviceIdentifier: deviceIdentifier, @@ -99,6 +128,12 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { }; } + return null; + }).future<Mobile.IDeviceApplicationInformation>()(); + } + + private getNativeScriptApplicationInformation(adb: Mobile.IDeviceAndroidDebugBridge, deviceIdentifier: string, information: string): IFuture<Mobile.IDeviceApplicationInformation> { + return ((): Mobile.IDeviceApplicationInformation => { // Search for appIdentifier (@<appIdentifier-debug>). let nativeScriptAppIdentifierRegExp = /@(.+)-debug/g; let nativeScriptAppIdentifierMatches = nativeScriptAppIdentifierRegExp.exec(information); @@ -116,13 +151,12 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { }).future<Mobile.IDeviceApplicationInformation>()(); } - private getAbstractPortForApplication(adb: Mobile.IDeviceAndroidDebugBridge, processId: string, appIdentifier: string): IFuture<string> { + private getAbstractPortForApplication(adb: Mobile.IDeviceAndroidDebugBridge, processId: string | number, appIdentifier: string, abstractPortsInformation: string): IFuture<string> { return (() => { // The result will look like this (without the columns names): // Num RefCount Protocol Flags Type St Inode Path // 0000000000000000: 00000002 00000000 00010000 0001 01 189004 @webview_devtools_remote_25512 // The Path column is the abstract port. - let abstractPortsInformation = this.getAbstractPortsInformation(adb).wait(); return this.getPortInformation(abstractPortsInformation, processId) || this.getPortInformation(abstractPortsInformation, `${appIdentifier}_devtools_remote`) || this.getPortInformation(abstractPortsInformation, `${appIdentifier}-debug`); @@ -133,36 +167,41 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { return adb.executeShellCommand(["cat", "/proc/net/unix"]); } - private getPortInformation(abstractPortsInformation: string, searchedInfo: string): string { + private getPortInformation(abstractPortsInformation: string, searchedInfo: string | number): string { let processRegExp = new RegExp(`\\w+:\\s+(?:\\w+\\s+){1,6}@(.*?${searchedInfo})$`, "gm"); let match = processRegExp.exec(abstractPortsInformation); return match && match[1]; } - private getProcessId(adb: Mobile.IDeviceAndroidDebugBridge, appIdentifier: string): IFuture<string> { + private getProcessIds(adb: Mobile.IDeviceAndroidDebugBridge, appIdentifiers: string[]): IFuture<IDictionary<number>> { return (() => { // Process information will look like this (without the columns names): // USER PID PPID VSIZE RSS WCHAN PC NAME // u0_a63 25512 1334 1519560 96040 ffffffff f76a8f75 S com.telerik.appbuildertabstest - let processIdRegExp = new RegExp(`^\\w*\\s*(\\d+).*?${appIdentifier}$`); + let result: IDictionary<number> = {}; let processIdInformation: string = adb.executeShellCommand(["ps"]).wait(); + _.each(appIdentifiers, appIdentifier => { + let processIdRegExp = new RegExp(`^\\w*\\s*(\\d+).*?${appIdentifier}$`); + result[appIdentifier] = this.getFirstMatchingGroupFromMultilineResult<number>(processIdInformation, processIdRegExp); + }); - return this.parseMultilineResult(processIdInformation, processIdRegExp); - }).future<string>()(); + return result; + }).future<IDictionary<number>>()(); } - private getAlreadyMappedPort(adb: Mobile.IDeviceAndroidDebugBridge, deviceIdentifier: string, abstractPort: string): IFuture<number> { + private getAlreadyMappedPort(adb: Mobile.IDeviceAndroidDebugBridge, deviceIdentifier: string, abstractPort: string, adbForwardList?: any): IFuture<number> { return ((): number => { - let allForwardedPorts: string = adb.executeCommand(["forward", "--list"]).wait() || ""; + let allForwardedPorts: string = adbForwardList || adb.executeCommand(["forward", "--list"]).wait() || ""; + // Sample output: // 5e2e580b tcp:62503 localabstract:webview_devtools_remote_7985 // 5e2e580b tcp:62524 localabstract:webview_devtools_remote_7986 // 5e2e580b tcp:63160 localabstract:webview_devtools_remote_7987 // 5e2e580b tcp:57577 localabstract:com.telerik.nrel-debug - let regex = new RegExp(`${deviceIdentifier}\\s+?tcp:(\\d+?)\\s+?.*?${abstractPort}.*$`); + let regex = new RegExp(`${deviceIdentifier}\\s+?tcp:(\\d+?)\\s+?.*?${abstractPort}$`); - return this.parseMultilineResult(allForwardedPorts, regex); + return this.getFirstMatchingGroupFromMultilineResult<number>(allForwardedPorts, regex); }).future<number>()(); } @@ -174,8 +213,18 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { return this._devicesAdbs[deviceIdentifier]; } - private parseMultilineResult(input: string, regex: RegExp): number { - let selectedNumber: number; + private getApplicationIdentifierFromPid(adb: Mobile.IDeviceAndroidDebugBridge, pid: string, psData?: string): IFuture<string> { + return ((): string => { + psData = psData || adb.executeShellCommand(["ps"]).wait(); + // Process information will look like this (without the columns names): + // USER PID PPID VSIZE RSS WCHAN PC NAME + // u0_a63 25512 1334 1519560 96040 ffffffff f76a8f75 S com.telerik.appbuildertabstest + return this.getFirstMatchingGroupFromMultilineResult<string>(psData, new RegExp(`\\s+${pid}(?:\\s+\\d+){3}\\s+.*\\s+(.*?)$`)); + }).future<string>()(); + } + + private getFirstMatchingGroupFromMultilineResult<T>(input: string, regex: RegExp): T { + let result: T; _((input || "").split('\n')) .map(line => line.trim()) @@ -183,12 +232,12 @@ export class AndroidProcessService implements Mobile.IAndroidProcessService { .each(line => { let matches = line.match(regex); if (matches && matches[1]) { - selectedNumber = +matches[1]; + result = <any>matches[1]; return false; } }); - return selectedNumber; + return result; } } diff --git a/mobile/mobile-core/devices-service.ts b/mobile/mobile-core/devices-service.ts index 7b9bd6e0..31d45587 100644 --- a/mobile/mobile-core/devices-service.ts +++ b/mobile/mobile-core/devices-service.ts @@ -407,6 +407,16 @@ export class DevicesService implements Mobile.IDevicesService { return _.map(deviceIdentifiers, (deviceIdentifier: string) => this.getDebuggableAppsCore(deviceIdentifier)); } + @exportedPromise("devicesService") + public getDebuggableViews(deviceIdentifier: string, appIdentifier: string): IFuture<Mobile.IDebugWebViewInfo[]> { + return ((): Mobile.IDebugWebViewInfo[] => { + let device = this.getDeviceByIdentifier(deviceIdentifier), + debuggableViewsPerApp = device.applicationManager.getDebuggableAppViews([appIdentifier]).wait(); + + return debuggableViewsPerApp && debuggableViewsPerApp[appIdentifier]; + }).future<Mobile.IDebugWebViewInfo[]>()(); + } + private getDebuggableAppsCore(deviceIdentifier: string): IFuture<Mobile.IDeviceApplicationInformation[]> { return ((): Mobile.IDeviceApplicationInformation[] => { let device = this.getDeviceByIdentifier(deviceIdentifier); diff --git a/package.json b/package.json index 05f12518..86206914 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "gaze": "1.0.0", "iconv-lite": "0.4.3", "inquirer": "0.8.2", - "ios-sim-portable": "1.1.1", + "ios-sim-portable": "~1.1.4", "lodash": "4.13.1", "log4js": "0.6.9", "marked": "0.3.3", diff --git a/test/unit-tests/appbuilder/device-emitter.ts b/test/unit-tests/appbuilder/device-emitter.ts index 5489d77c..57847db8 100644 --- a/test/unit-tests/appbuilder/device-emitter.ts +++ b/test/unit-tests/appbuilder/device-emitter.ts @@ -252,7 +252,6 @@ describe("deviceEmitter", () => { _.each(["debuggableAppFound", "debuggableAppLost"], (applicationEvent: string) => { describe(applicationEvent, () => { - // deviceIdentifier, appIdentifier, framework let attachDebuggableEventVerificationHandler = (expectedDebuggableAppInfo: Mobile.IDeviceApplicationInformation, done: mocha.Done) => { deviceEmitter.on(applicationEvent, (debuggableAppInfo: Mobile.IDeviceApplicationInformation) => { @@ -301,6 +300,62 @@ describe("deviceEmitter", () => { }); }); + _.each(["debuggableViewFound", "debuggableViewLost", "debuggableViewChanged"], (applicationEvent: string) => { + describe(applicationEvent, () => { + + let createDebuggableWebView = (uniqueId: string) => { + return { + description: `description_${uniqueId}`, + devtoolsFrontendUrl: `devtoolsFrontendUrl_${uniqueId}`, + id: `${uniqueId}`, + title: `title_${uniqueId}`, + type: `type_${uniqueId}`, + url: `url_${uniqueId}`, + webSocketDebuggerUrl: `webSocketDebuggerUrl_${uniqueId}`, + }; + }; + + let appId = "appId"; + + let attachDebuggableEventVerificationHandler = (expectedDeviceIdentifier: string, expectedAppIdentifier: string, expectedDebuggableViewInfo: Mobile.IDebugWebViewInfo, done: mocha.Done) => { + deviceEmitter.on(applicationEvent, (deviceIdentifier: string, appIdentifier: string, debuggableViewInfo: Mobile.IDebugWebViewInfo) => { + assert.deepEqual(deviceIdentifier, expectedDeviceIdentifier); + + assert.deepEqual(appIdentifier, expectedAppIdentifier); + + assert.deepEqual(debuggableViewInfo, expectedDebuggableViewInfo); + + // Wait for all operations to be completed and call done after that. + setTimeout(done, 0); + }); + }; + + it("is raised when working with android device", (done) => { + let expectedDebuggableViewInfo: Mobile.IDebugWebViewInfo = createDebuggableWebView("test1"); + + attachDebuggableEventVerificationHandler(androidDevice.deviceInfo.identifier, appId, expectedDebuggableViewInfo, done); + androidDeviceDiscovery.emit("deviceFound", androidDevice); + androidDevice.applicationManager.emit(applicationEvent, appId, expectedDebuggableViewInfo); + }); + + it("is raised when working with iOS device", (done) => { + let expectedDebuggableViewInfo: Mobile.IDebugWebViewInfo = createDebuggableWebView("test1"); + + attachDebuggableEventVerificationHandler(iOSDevice.deviceInfo.identifier, appId, expectedDebuggableViewInfo, done); + iOSDeviceDiscovery.emit("deviceFound", iOSDevice); + iOSDevice.applicationManager.emit(applicationEvent, appId, expectedDebuggableViewInfo); + }); + + it("is raised when working with iOS simulator", (done) => { + let expectedDebuggableViewInfo: Mobile.IDebugWebViewInfo = createDebuggableWebView("test1"); + + attachDebuggableEventVerificationHandler(iOSSimulator.deviceInfo.identifier, appId, expectedDebuggableViewInfo, done); + iOSSimulatorDiscovery.emit("deviceFound", iOSSimulator); + iOSSimulator.applicationManager.emit(applicationEvent, appId, expectedDebuggableViewInfo); + }); + }); + }); + _.each(["companionAppInstalled", "companionAppUninstalled"], (applicationEvent: string) => { describe(applicationEvent, () => { _.each(companionAppIdentifiers, (companionAppIdentifersForPlatform: any, applicationFramework: string) => { diff --git a/test/unit-tests/mobile/application-manager-base.ts b/test/unit-tests/mobile/application-manager-base.ts index cb2c4d46..0394ce8d 100644 --- a/test/unit-tests/mobile/application-manager-base.ts +++ b/test/unit-tests/mobile/application-manager-base.ts @@ -5,7 +5,8 @@ import { ApplicationManagerBase } from "../../../mobile/application-manager-base import Future = require("fibers/future"); let currentlyAvailableAppsForDebugging: Mobile.IDeviceApplicationInformation[], - currentlyInstalledApps: string[]; + currentlyInstalledApps: string[], + currentlyAvailableAppWebViewsForDebugging: IDictionary<Mobile.IDebugWebViewInfo[]>; class ApplicationManager extends ApplicationManagerBase { constructor($logger: ILogger) { @@ -47,6 +48,10 @@ class ApplicationManager extends ApplicationManagerBase { public getDebuggableApps(): IFuture<Mobile.IDeviceApplicationInformation[]> { return Future.fromResult(currentlyAvailableAppsForDebugging); } + + public getDebuggableAppViews(appIdentifiers: string[]): IFuture<IDictionary<Mobile.IDebugWebViewInfo[]>> { + return Future.fromResult(_.cloneDeep(currentlyAvailableAppWebViewsForDebugging)); + } } function createTestInjector(): IInjector { @@ -64,6 +69,27 @@ function createAppsAvailableForDebugging(count: number): Mobile.IDeviceApplicati })); } +function createDebuggableWebView(uniqueId: string) { + return { + description: `description_${uniqueId}`, + devtoolsFrontendUrl: `devtoolsFrontendUrl_${uniqueId}`, + id: `${uniqueId}`, + title: `title_${uniqueId}`, + type: `type_${uniqueId}`, + url: `url_${uniqueId}`, + webSocketDebuggerUrl: `webSocketDebuggerUrl_${uniqueId}`, + }; +} + +function createDebuggableWebViews(appInfos: Mobile.IDeviceApplicationInformation[], numberOfViews: number): IDictionary<Mobile.IDebugWebViewInfo[]> { + let result: IDictionary<Mobile.IDebugWebViewInfo[]> = {}; + _.each(appInfos, (appInfo, index) => { + result[appInfo.appIdentifier] = _.times(numberOfViews, (currentViewIndex: number) => createDebuggableWebView(`${index}_${currentViewIndex}`)); + }); + + return result; +} + describe("ApplicationManagerBase", () => { let applicationManager: ApplicationManager, testInjector: IInjector; @@ -71,6 +97,7 @@ describe("ApplicationManagerBase", () => { beforeEach(() => { testInjector = createTestInjector(); currentlyAvailableAppsForDebugging = null; + currentlyAvailableAppWebViewsForDebugging = null; applicationManager = testInjector.resolve("applicationManager"); }); @@ -205,6 +232,171 @@ describe("ApplicationManagerBase", () => { applicationManager.checkForApplicationUpdates().wait(); Future.wait(futures); }); + + it("emits debuggableViewFound when new views are available for debug", (done) => { + currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(2); + let numberOfViewsPerApp = 2; + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews(currentlyAvailableAppsForDebugging, numberOfViewsPerApp); + let currentDebuggableViews: IDictionary<Mobile.IDebugWebViewInfo[]> = {}; + applicationManager.on("debuggableViewFound", (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + currentDebuggableViews[appIdentifier] = currentDebuggableViews[appIdentifier] || []; + currentDebuggableViews[appIdentifier].push(d); + let numberOfFoundViewsPerApp = _.uniq(_.values(currentDebuggableViews).map(arr => arr.length)); + if (_.keys(currentDebuggableViews).length === currentlyAvailableAppsForDebugging.length + && numberOfFoundViewsPerApp.length === 1 // for all apps we've found exactly two apps. + && numberOfFoundViewsPerApp[0] === numberOfViewsPerApp) { + _.each(currentDebuggableViews, (webViews, appId) => { + _.each(webViews, webView => { + let expectedWebView = _.find(currentlyAvailableAppWebViewsForDebugging[appId], c => c.id === webView.id); + assert.isTrue(_.isEqual(webView, expectedWebView)); + }); + }); + setTimeout(done, 0); + } + }); + + applicationManager.checkForApplicationUpdates().wait(); + }); + + it("emits debuggableViewLost when views for debug are removed", (done) => { + currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(2); + let numberOfViewsPerApp = 2; + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews(currentlyAvailableAppsForDebugging, numberOfViewsPerApp); + let expectedResults = _.cloneDeep(currentlyAvailableAppWebViewsForDebugging); + let currentDebuggableViews: IDictionary<Mobile.IDebugWebViewInfo[]> = {}; + + applicationManager.checkForApplicationUpdates().wait(); + + applicationManager.on("debuggableViewLost", (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + currentDebuggableViews[appIdentifier] = currentDebuggableViews[appIdentifier] || []; + currentDebuggableViews[appIdentifier].push(d); + let numberOfFoundViewsPerApp = _.uniq(_.values(currentDebuggableViews).map(arr => arr.length)); + if (_.keys(currentDebuggableViews).length === currentlyAvailableAppsForDebugging.length + && numberOfFoundViewsPerApp.length === 1 // for all apps we've found exactly two apps. + && numberOfFoundViewsPerApp[0] === numberOfViewsPerApp) { + _.each(currentDebuggableViews, (webViews, appId) => { + _.each(webViews, webView => { + let expectedWebView = _.find(expectedResults[appId], c => c.id === webView.id); + assert.isTrue(_.isEqual(webView, expectedWebView)); + }); + }); + setTimeout(done, 0); + } + }); + + currentlyAvailableAppWebViewsForDebugging = _.mapValues(currentlyAvailableAppWebViewsForDebugging, (a) => []); + + applicationManager.checkForApplicationUpdates().wait(); + }); + + it("emits debuggableViewFound when new views are available for debug", (done) => { + currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(2); + let numberOfViewsPerApp = 2; + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews(currentlyAvailableAppsForDebugging, numberOfViewsPerApp); + + applicationManager.checkForApplicationUpdates().wait(); + + let expectedViewToBeFound = createDebuggableWebView("uniqueId"), + expectedAppIdentifier = currentlyAvailableAppsForDebugging[0].appIdentifier, + isLastCheck = false; + + applicationManager.on("debuggableViewFound", (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + assert.deepEqual(appIdentifier, expectedAppIdentifier); + assert.isTrue(_.isEqual(d, expectedViewToBeFound)); + + if (isLastCheck) { + setTimeout(done, 0); + } + }); + + currentlyAvailableAppWebViewsForDebugging[expectedAppIdentifier].push(_.cloneDeep(expectedViewToBeFound)); + applicationManager.checkForApplicationUpdates().wait(); + + expectedViewToBeFound = createDebuggableWebView("uniqueId1"); + currentlyAvailableAppWebViewsForDebugging[expectedAppIdentifier].push(_.cloneDeep(expectedViewToBeFound)); + applicationManager.checkForApplicationUpdates().wait(); + + expectedViewToBeFound = createDebuggableWebView("uniqueId2"); + expectedAppIdentifier = currentlyAvailableAppsForDebugging[1].appIdentifier; + isLastCheck = true; + + currentlyAvailableAppWebViewsForDebugging[expectedAppIdentifier].push(_.cloneDeep(expectedViewToBeFound)); + applicationManager.checkForApplicationUpdates().wait(); + }); + + it("emits debuggableViewLost when views for debug are not available anymore", (done) => { + currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(2); + let numberOfViewsPerApp = 2; + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews(currentlyAvailableAppsForDebugging, numberOfViewsPerApp); + + applicationManager.checkForApplicationUpdates().wait(); + + let expectedAppIdentifier = currentlyAvailableAppsForDebugging[0].appIdentifier, + expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[expectedAppIdentifier].splice(0, 1)[0], + isLastCheck = false; + + applicationManager.on("debuggableViewLost", (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + assert.deepEqual(appIdentifier, expectedAppIdentifier); + assert.isTrue(_.isEqual(d, expectedViewToBeLost)); + + if (isLastCheck) { + setTimeout(done, 0); + } + }); + + applicationManager.checkForApplicationUpdates().wait(); + + expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[expectedAppIdentifier].splice(0, 1)[0]; + applicationManager.checkForApplicationUpdates().wait(); + + expectedAppIdentifier = currentlyAvailableAppsForDebugging[1].appIdentifier; + expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[expectedAppIdentifier].splice(0, 1)[0]; + + isLastCheck = true; + applicationManager.checkForApplicationUpdates().wait(); + }); + + it("emits debuggableViewChanged when view's property is modified (each one except id)", (done) => { + currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(1); + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews(currentlyAvailableAppsForDebugging, 2); + let viewToChange = currentlyAvailableAppWebViewsForDebugging[currentlyAvailableAppsForDebugging[0].appIdentifier][0]; + let expectedView = _.cloneDeep(viewToChange); + expectedView.title = "new title"; + + applicationManager.on("debuggableViewChanged", (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + assert.isTrue(_.isEqual(d, expectedView)); + setTimeout(done, 0); + }); + + applicationManager.checkForApplicationUpdates().wait(); + viewToChange.title = "new title"; + applicationManager.checkForApplicationUpdates().wait(); + }); + + it("does not emit debuggableViewChanged when id is modified", (done) => { + currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(1); + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews(currentlyAvailableAppsForDebugging, 2); + let viewToChange = currentlyAvailableAppWebViewsForDebugging[currentlyAvailableAppsForDebugging[0].appIdentifier][0]; + let expectedView = _.cloneDeep(viewToChange); + + applicationManager.checkForApplicationUpdates().wait(); + applicationManager.on("debuggableViewChanged", (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + setTimeout(() => done(new Error("When id is changed, debuggableViewChanged must not be emitted.")), 0); + }); + + applicationManager.on("debuggableViewLost", (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + assert.isTrue(_.isEqual(d, expectedView)); + }); + + applicationManager.on("debuggableViewFound", (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + expectedView.id = "new id"; + assert.isTrue(_.isEqual(d, expectedView)); + setTimeout(done, 0); + }); + + viewToChange.id = "new id"; + applicationManager.checkForApplicationUpdates().wait(); + }); }); describe("installed and uninstalled apps", () => { @@ -556,7 +748,7 @@ describe("ApplicationManagerBase", () => { it("when startApplications throws", () => { applicationManager.canStartApplication = () => true; applicationManager.isApplicationInstalled = (appId: string) => Future.fromResult(true); - assertDoesNotThrow({shouldStartApplicatinThrow: true}); + assertDoesNotThrow({ shouldStartApplicatinThrow: true }); }); }); From 198ae8eee48833bcdf3a6ca64e58afb5b04e3821 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov <rosen.vladimirov@telerik.com> Date: Thu, 14 Jul 2016 16:50:01 +0300 Subject: [PATCH 2/2] Set versiont to 0.18.0 --- README.md | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c0a0289b..197624c7 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Contains common infrastructure for CLIs - mainly AppBuilder and NativeScript. Installation === -Latest version: 0.17.3 +Latest version: 0.18.0 -Release date: 2016, July 12 +Release date: 2016, July 14 ### System Requirements diff --git a/package.json b/package.json index 86206914..4922d35a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mobile-cli-lib", "preferGlobal": false, - "version": "0.17.3", + "version": "0.18.0", "author": "Telerik <support@telerik.com>", "description": "common lib used by different CLI", "bin": {