From 5d7cc06ae84e246bf0dff7114874518ae32d42f5 Mon Sep 17 00:00:00 2001 From: Eric Plaster Date: Tue, 28 May 2013 10:45:40 -0500 Subject: [PATCH] Added changes to notify websocket listeners of alert dialogs. One thing to keep in mind: iOS and Android treat dialogs differently. iOS "system" dialogs can be triggered from the application under test as well as from external sources (the system). However, Android treats dialogs as any other window. So, to make this work, the bootstrap app looks for windows with the package name of "com.android.systemui". These are dialogs that the system has instantiated, but not the app. So, for example, if we had the same application written on both platforms, and they both had a login screen, and you click on the login button without putting in a username. On iOS, you would be notified if they used a standard system dialog, and you would not be notified on Android. However, if the system put up a low battery dialog, it would be detected on both systems. So... The main reason for implementing this is to stop failing tests from unintentional dialogs appearing during an overnight test. Updated call signiture to instruments. --- android/adb.js | 8 +- .../android/bootstrap/SocketServer.java | 22 ++++-- .../android/bootstrap/utils/TheWatchers.java | 48 ++++++++++++ app/android.js | 1 + app/appium.js | 11 +++ app/ios.js | 2 + instruments/instruments.js | 13 +++- package.json | 8 +- sample-code/examples/python/alerts.py | 41 ++++++++++ server.js | 22 +++++- test/functional/apidemos/alerts.js | 60 +++++++++++++++ test/functional/uicatalog/alerts.js | 77 +++++++++++++++++++ 12 files changed, 299 insertions(+), 14 deletions(-) create mode 100644 android/bootstrap/src/io/appium/android/bootstrap/utils/TheWatchers.java create mode 100644 sample-code/examples/python/alerts.py create mode 100644 test/functional/apidemos/alerts.js create mode 100644 test/functional/uicatalog/alerts.js diff --git a/android/adb.js b/android/adb.js index ac46eb681..e1ed18b76 100644 --- a/android/adb.js +++ b/android/adb.js @@ -29,6 +29,7 @@ var ADB = function(opts, android) { } this.sdkRoot = opts.sdkRoot; this.udid = opts.udid; + this.webSocket = opts.webSocket; // Don't uninstall if using fast reset. // Uninstall if reset is set and fast reset isn't. this.skipUninstall = opts.fastReset || !(opts.reset || false); @@ -719,7 +720,6 @@ ADB.prototype.runBootstrap = function(readyCb, exitCb) { }; ADB.prototype.checkForSocketReady = function(output) { - logger.info("Checking for appium socket server to be ready"); if (/Appium Socket Server Ready/.test(output)) { this.requirePortForwarded(); this.debug("Connecting to server on device..."); @@ -811,6 +811,12 @@ ADB.prototype.handleBootstrapOutput = function(output) { match = re.exec(line); if (match) { logger.info("[ANDROID] " + match[1]); + + var alertRe = /Emitting system alert message/; + if (alertRe.test(line)) { + logger.info("Emiting alert message..."); + me.webSocket.sockets.emit('alert', {message: line}); + } } else { // The dump command will always disconnect UiAutomation. // Detect the crash then restart UiAutomation. diff --git a/android/bootstrap/src/io/appium/android/bootstrap/SocketServer.java b/android/bootstrap/src/io/appium/android/bootstrap/SocketServer.java index f99997413..3f9e1a11b 100644 --- a/android/bootstrap/src/io/appium/android/bootstrap/SocketServer.java +++ b/android/bootstrap/src/io/appium/android/bootstrap/SocketServer.java @@ -1,8 +1,6 @@ package io.appium.android.bootstrap; -import io.appium.android.bootstrap.exceptions.AndroidCommandException; -import io.appium.android.bootstrap.exceptions.CommandTypeException; -import io.appium.android.bootstrap.exceptions.SocketServerException; +import org.json.JSONException; import java.io.BufferedReader; import java.io.IOException; @@ -11,7 +9,10 @@ import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; -import org.json.JSONException; +import io.appium.android.bootstrap.exceptions.AndroidCommandException; +import io.appium.android.bootstrap.exceptions.CommandTypeException; +import io.appium.android.bootstrap.exceptions.SocketServerException; +import io.appium.android.bootstrap.utils.TheWatchers; /** * The SocketServer class listens on a specific port for commands from Appium, @@ -26,6 +27,7 @@ class SocketServer { PrintWriter out; boolean keepListening; private final AndroidCommandExecutor executor; + private final TheWatchers watchers = TheWatchers.getInstance(); /** * Constructor @@ -108,7 +110,11 @@ class SocketServer { in = new BufferedReader(new InputStreamReader(client.getInputStream())); out = new PrintWriter(client.getOutputStream(), true); while (keepListening) { - handleClientData(); + if (in.ready()) { + handleClientData(); + } + Thread.sleep(100); + watchers.check(); } in.close(); out.close(); @@ -116,6 +122,8 @@ class SocketServer { Logger.info("Closed client connection"); } catch (final IOException e) { throw new SocketServerException("Error when client was trying to connect"); + } catch (InterruptedException e) { + throw new SocketServerException("The socket server was interupted"); } } @@ -123,8 +131,8 @@ class SocketServer { * When {@link #handleClientData()} has valid data, this method delegates the * command. * - * @param cmd - * @return + * @param cmd AndroidCommand + * @return Result */ private String runCommand(final AndroidCommand cmd) { AndroidCommandResult res; diff --git a/android/bootstrap/src/io/appium/android/bootstrap/utils/TheWatchers.java b/android/bootstrap/src/io/appium/android/bootstrap/utils/TheWatchers.java new file mode 100644 index 000000000..6e8c0f885 --- /dev/null +++ b/android/bootstrap/src/io/appium/android/bootstrap/utils/TheWatchers.java @@ -0,0 +1,48 @@ +package io.appium.android.bootstrap.utils; + +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiSelector; + +import io.appium.android.bootstrap.Logger; + +public class TheWatchers { + private static TheWatchers ourInstance = new TheWatchers(); + private long start = System.currentTimeMillis(); + private long delta = 1; + private boolean alerted = false; + + public static TheWatchers getInstance() { + return ourInstance; + } + + private TheWatchers() { + start = System.currentTimeMillis(); + } + + public void setDelta(long seconds) { + delta = seconds * 1000; + } + + public boolean check() { + if(start + delta < System.currentTimeMillis()) { + // Send only one alert message... + if (isDialogPresent() && (!alerted)) { + Logger.info("Emitting system alert message"); + alerted = true; + } + + // if the dialog went away, make sure we can send an alert again + if (!isDialogPresent() && alerted) { + alerted = false; + } + + start = System.currentTimeMillis(); + } + return false; + } + + public boolean isDialogPresent() { + UiObject alertDialog = new UiObject(new UiSelector().packageName("com.android.systemui")); + return alertDialog.exists(); + } +} diff --git a/app/android.js b/app/android.js index 921af0ba2..be0ffeb9b 100644 --- a/app/android.js +++ b/app/android.js @@ -19,6 +19,7 @@ var errors = require('./errors') var Android = function(opts) { this.rest = opts.rest; + this.webSocket = opts.webSocket; this.opts = opts; this.apkPath = opts.apkPath; this.udid = opts.udid; diff --git a/app/appium.js b/app/appium.js index b8002fd5d..35029affe 100644 --- a/app/appium.js +++ b/app/appium.js @@ -43,6 +43,7 @@ var Appium = function(args) { setWebhook(logger, port, host); } this.rest = null; + this.webSocket = null; this.devices = {}; this.deviceType = null; this.device = null; @@ -72,6 +73,14 @@ Appium.prototype.attachTo = function(rest, cb) { } }; +Appium.prototype.attachSocket = function(webSocket, cb) { + this.webSocket = webSocket; + + if (cb) { + cb(); + } +}; + Appium.prototype.registerConfig = function(configObj) { this.serverConfig = configObj; }; @@ -392,6 +401,7 @@ Appium.prototype.invoke = function() { if (this.isIos()) { var iosOpts = { rest: this.rest + , webSocket: this.webSocket , app: this.args.app , ipa: this.args.ipa , bundleId: this.args.bundleId @@ -410,6 +420,7 @@ Appium.prototype.invoke = function() { } else if (this.isAndroid()) { var androidOpts = { rest: this.rest + , webSocket: this.webSocket , apkPath: this.args.app , verbose: !this.args.quiet , udid: this.args.udid diff --git a/app/ios.js b/app/ios.js index 9f741495b..b100fdb5e 100644 --- a/app/ios.js +++ b/app/ios.js @@ -28,6 +28,7 @@ var path = require('path') var IOS = function(args) { this.rest = args.rest; + this.webSocket = args.webSocket; this.app = args.app; this.ipa = args.ipa; this.bundleId = args.bundleId || null; @@ -212,6 +213,7 @@ IOS.prototype.start = function(cb, onDie) { , path.resolve(__dirname, 'uiauto/Automation.tracetemplate') , sock , me.withoutDelay + , me.webSocket , onLaunch , onExit ); diff --git a/instruments/instruments.js b/instruments/instruments.js index ca0d187fa..2c45b09a1 100644 --- a/instruments/instruments.js +++ b/instruments/instruments.js @@ -11,12 +11,13 @@ var spawn = require('child_process').spawn , path = require('path') , codes = require('../app/uiauto/lib/status.js').codes; -var Instruments = function(app, udid, bootstrap, template, sock, withoutDelay, cb, exitCb) { +var Instruments = function(app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb) { this.app = app; this.udid = udid; this.bootstrap = bootstrap; this.template = template; this.withoutDelay = withoutDelay; + this.webSocket = webSocket; this.commandQueue = []; this.curCommand = null; this.resultHandler = this.defaultResultHandler; @@ -277,6 +278,12 @@ Instruments.prototype.outputStreamHandler = function(output) { Instruments.prototype.errorStreamHandler = function(output) { logger.info(("[INST STDERR] " + output).yellow); + var re = /Call to onAlert returned 'YES'/; + var match = re.exec(output); + if (match) { + logger.info("Emiting alert message..."); + this.webSocket.sockets.emit('alert', {message: output}); + } }; Instruments.prototype.lookForShutdownInfo = function(output) { @@ -327,7 +334,7 @@ Instruments.prototype.debug = function(msg) { /* NODE EXPORTS */ -module.exports = function(server, app, udid, bootstrap, template, sock, withoutDelay, cb, exitCb) { - return new Instruments(server, app, udid, bootstrap, template, sock, withoutDelay, cb, exitCb); +module.exports = function(server, app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb) { + return new Instruments(server, app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb); }; diff --git a/package.json b/package.json index f720beb5d..200aec02f 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "node-idevice": "~0.1.2", "win-spawn" : "~1.1.1", "adm-zip" : "~0.4.3", - "ws": "0.4.25" + "ws": "0.4.25", + "socket.io" : "~0.9.14" }, "scripts": { "test": "grunt travis" @@ -71,6 +72,9 @@ "assert": "~0.4.9", "grunt-mocha-test": "~0.2.0", "grunt-contrib-jshint": "~0.1.1", - "saucelabs": "~0.0.7" + "saucelabs": "~0.0.7", + "socket.io-client": "~0.9.11", + "socks": "~0.0.1", + "telnetclient": "~0.0.1" } } diff --git a/sample-code/examples/python/alerts.py b/sample-code/examples/python/alerts.py new file mode 100644 index 000000000..d6bca5611 --- /dev/null +++ b/sample-code/examples/python/alerts.py @@ -0,0 +1,41 @@ +from socketIO_client import SocketIO + +""" +Please note that you will need to install the socketIO-client python module. + +This app will block on the wait() method until messages are recieved. It's +advised to read the documentation for socketIO-client and listen for alerts +in a seperate thread. + +This should not be considered production quality. +""" + + +class Alerts(object): + def __init__(self): + self.socket = SocketIO('localhost', 4723) + self.connect_events() + self.socket.wait() + + def connect_events(self): + self.socket.on('connect', self.on_connect) + self.socket.on('disconnect', self.on_disconnect) + self.socket.on('alert', self.on_alert) + self.socket.on('error', self.on_error) + + def on_connect(self): + print "[connected]" + + def on_alert(self, *args): + message = args[0]['message'] + print 'on_alert', str(message) + + def on_disconnect(self): + print "[disconnected]" + + def on_error(self, name, message): + print('Error: %s: %s' % (name, message)) + + +if __name__ == "__main__": + alerts = Alerts() diff --git a/server.js b/server.js index 994536d34..fa9fc84bd 100644 --- a/server.js +++ b/server.js @@ -12,6 +12,7 @@ var http = require('http') , async = require('async') , _ = require("underscore") , parser = require('./app/parser') + , io = require('socket.io') , gridRegister = require('./grid_register'); var allowCrossDomain = function(req, res, next) { @@ -49,7 +50,8 @@ var main = function(args, readyCb, doneCb) { doneCb = function() {}; } var rest = express() - , server = http.createServer(rest); + , server = http.createServer(rest) + , alerts = io.listen(server, {'flash policy port': -1}); rest.configure(function() { rest.use(express.favicon()); @@ -73,6 +75,24 @@ var main = function(args, readyCb, doneCb) { // Hook up REST http interface appiumServer.attachTo(rest); + alerts.configure(function() { + alerts.set('log level', 1); + alerts.set("polling duration", 10); + alerts.set("transports", ['websocket', 'flashsocket']); + }); + + alerts.sockets.on("connection", function (socket) { + socket.set('log level', 1); + logger.info("Client connected: " + (socket.id).toString()); + + socket.on('disconnect', function(data) { + logger.info("Client disconnected: " + data); + }); + }); + + // add web socket so we can emit events + appiumServer.attachSocket(alerts); + var checkSetup = function(cb) { var configFile = path.resolve(__dirname, ".appiumconfig"); fs.readFile(configFile, function(err, data) { diff --git a/test/functional/apidemos/alerts.js b/test/functional/apidemos/alerts.js new file mode 100644 index 000000000..509e4a76a --- /dev/null +++ b/test/functional/apidemos/alerts.js @@ -0,0 +1,60 @@ +/*global it:true */ +"use strict"; + +var path = require('path') + , appPath = path.resolve(__dirname, "../../../sample-code/apps/ApiDemos/bin/ApiDemos-debug.apk") + , appPkg = "com.example.android.apis" + , appAct = "ApiDemos" + , describeWd = require("../../helpers/driverblock.js").describeForApp(appPath, + "android", appPkg, appAct) + , should = require('should') + , telnet = require('telnetclient') + , io = require('socket.io-client'); + + +describeWd('alert dialog detection', function(h) { + // setup websocket client... + var options ={ + transports: ['websocket'], + 'force new connection': true + }; + var client = io.connect('http://127.0.0.1:4723', options); + + // set up telnet... + var runCommands = function(cmds, cb) { + var client = new telnet.Client('127.0.0.1', 5554, {}, function(socket) { + }) + .on('data',function(d,e) {}) + .on('end',function() {}) + for (var i=0; i