Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,264 @@
/* global Marionette */
(function(module, ns) {
'use strict';
/**
* Actions is used to denote a set of actions that
* have to be executed in particular order.
*
* @class Marionette.Actions
* @param {Marionette.Client} context of a client.
*/
function Actions(client) {
this.client = client;
this.actionChain = [];
this.currentId = null;
}
Actions.prototype = {
/**
* Send a 'touchstart' event to this element. If no coordinates are given,
* it will be targeted at the center of the element.
* If given, it will be targeted at the (x,y) coordinates
* relative to the top-left corner of the element.
*
* @method press
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* to press on.
* @param {Number} x optional, x-coordinate to tap,
* relative to the top-left corner of the element.
* @param {Number} y optional, y-coordinate to tap,
* relative to the top-left corner of the element.
* @return {Object} self.
*/
press: function press(element, x, y) {
this.actionChain.push(['press', element.id, x, y]);
return this;
},
/**
* release() can only be called
* if press() has already be called on this element.
* Send 'touchend' event to whenever the finger is.
*
* @method release
*
* @return {Object} self.
*/
release: function release() {
this.actionChain.push(['release']);
return this;
},
/**
* move() can only be called
* if press() has already be called on this element.
* move() send 'touchmove' event which moves the finger to target element.
*
* @method move
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* of the move gesture.
*
* @return {Object} self.
*/
move: function move(element) {
this.actionChain.push(['move', element.id]);
return this;
},
/**
* moveByOffset() can only be called
* if press() has already be called on this element.
* moveByOffset() send 'touchmove' event.
*
* @method moveByOffset
*
* @param {Number} x x-coordinate relative to the top-left corner of
* the target element of the last touch.
*
* @param {Number} y y-coordinate relative to the top-left corner of
* the target element of the last touch.
*
* @return {Object} self.
*/
moveByOffset: function moveByOffset(x, y) {
this.actionChain.push(['moveByOffset', x, y]);
return this;
},
/**
* wait() waits for specified time period.
*
* @method wait
* @param {Number} time wait for "time" seconds.
*
* @return {Object} self.
*/
wait: function wait(time) {
this.actionChain.push(['wait', time]);
return this;
},
/**
* cancel() can only be called
* if press() has already be called on this element.
* cancel() send 'touchcancel' event to whenever the finger is.
*
* @method cancel
*
* @return {Object} self.
*/
cancel: function cancel() {
this.actionChain.push(['cancel']);
return this;
},
/**
* tap() performs a quick tap on the target.
* action.tap() is essentially action.press().release().
*
* @method tap
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* to press/release on.
*
* @param {Number} x optional, x-coordinate relative to the top-left corner
* of the element of the last touch.
*
* @param {Number} y optional, y-coordinate relative to the top-left corner
* of the element of the last touch.
*
* @return {Object} self.
*/
tap: function tap(element, x, y) {
this.actionChain.push(['press', element.id, x, y]);
this.actionChain.push(['release']);
return this;
},
/**
* doubleTap() performs a double
* {{#crossLink "Marionette.Actions/tap"}}{{/crossLink}} on the element.
*
* @method doubleTap
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* to double tap on.
*
* @param {Number} x optional, x-coordinate relative to
* the top-left corner of the element of the last touch.
*
* @param {Number} y optional, y-coordinate relative to
* the top-left corner of the element of the last touch.
*
* @return {Object} self.
*/
doubleTap: function doubleTap(element, x, y) {
this.actionChain.push(['press', element.id, x, y]);
this.actionChain.push(['release']);
this.actionChain.push(['press', element.id, x, y]);
this.actionChain.push(['release']);
return this;
},
/**
* flick() sends a sequence of touch events:
* touchstart, touchmove, touchend.
* It scrolls the page in any direction
* within period of time provided(duration).
*
* @method flick
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* to double tap on.
*
* @param {Number} x1 starting x-coordinates of
* the finger relative to the element.
*
* @param {Number} y1 starting y-coordinates of
* the finger relative to the element.
*
* @param {Number} x2 ending x-coordinates of
* the finger relative to the element.
*
* @param {Number} y2 ending y-coordinates of
* the finger relative to the element.
*
* @param {Number} duration optional,
* time needed for the flick gesture for complete.
*
* @return {Object} self.
*/
flick: function flick(element, x1, y1, x2, y2, duration) {
var time = 0;
var timeIncrement = 10.0;
if (duration === undefined) {
duration = 200;
}
if (timeIncrement >= duration) {
timeIncrement = duration;
}
var moveX = timeIncrement / duration * (x2 - x1);
var moveY = timeIncrement / duration * (y2 - y1);
this.actionChain.push(['press', element.id, x1, y1]);
while (time < duration) {
time += timeIncrement;
this.actionChain.push(['moveByOffset', moveX, moveY]);
this.actionChain.push(['wait', timeIncrement / 1000]);
}
this.actionChain.push(['release']);
return this;
},
/**
* Sends 'touchstart',
* then wait for 'time' seconds, and send 'touchend' eventually.
* longPress() cannot follow any active touch,
* i.e. the finger must leave the screen before longPress() gets called.
*
* @method longPress
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* of the long press.
*
* @param {Number} time the waiting time between touchstart and touchend.
*
* @return {Object} self.
*/
longPress: function longPress(element, time) {
this.actionChain.push(['press', element.id]);
this.actionChain.push(['wait', time]);
this.actionChain.push(['release']);
return this;
},
/**
* perform() will send the whole action chain built
* so far to the server side for execution.
*
* @method perform
* @param {Function} callback callback when the perform completes.
*
* @return {Object} self.
*/
perform: function perform(callback) {
var cmd = {
name: 'actionChain',
parameters: {
chain: this.actionChain,
nextId: this.currentId
}
};
this.currentId = this.client._sendCommand(cmd, callback, 'value');
this.actionChain = [];
return this;
}
};
module.exports = Actions;
}.apply(
this,
(this.Marionette) ?
[Marionette('actions'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
/* global Marionette */
(function(module, ns) {
'use strict';
var debug = require('debug')('marionette:command-stream');
var Responder = ns.require('responder');
var wire = require('json-wire-protocol');
/**
* Command stream accepts a socket or any event
* emitter that will emit data events
*
* @class Marionette.CommandStream
* @param {EventEmitter} socket socket instance.
* @constructor
*/
function CommandStream(socket) {
this.socket = socket;
this._handler = new wire.Stream();
this._handler.on('data', this.emit.bind(this, this.commandEvent));
Responder.apply(this);
socket.on('data', this._handler.write.bind(this._handler));
socket.on('error', function(err) {
this.emit('error', err);
}.bind(this));
}
var proto = CommandStream.prototype = Object.create(
Responder.prototype
);
/**
* name of the event this class
* will emit when a response to a
* command is received.
*
* @property commandEvent
* @type String
*/
proto.commandEvent = 'command';
/**
* Parses command into a string to
* be sent over a tcp socket to marionette.
*
*
* @method stringify
* @param {Object} command marionette command.
* @return {String} command as a string.
*/
proto.stringify = function stringify(command) {
return wire.stringify(command);
};
/**
* Writes a command to the socket.
* Handles conversion and formatting of object.
*
* @method send
* @param {Object} data marionette command.
*/
proto.send = function send(data) {
debug('writing ', data, 'to socket');
if (this.socket.write) {
//nodejs socket
this.socket.write(this.stringify(data), 'utf8');
} else {
//moztcp socket
this.socket.send(this.stringify(data));
}
};
/**
* Adds a chunk (string or buffer) to the
* total buffer of this instance.
*
* @this
* @param {String|Buffer} buffer buffer or string to add.
*/
proto.add = function add(buffer) {
this._handler.write(buffer);
};
module.exports = exports = CommandStream;
}.apply(
this,
(this.Marionette) ?
[Marionette('command-stream'), Marionette] :
[module, require('./marionette')]
));

View file

@ -0,0 +1,231 @@
/* global Marionette */
(function(module, ns) {
'use strict';
var Command = require('../message').Command;
var Response = require('../message').Response;
/**
*
* Abstract driver that will handle
* all common tasks between implementations.
* Such as error handling, request/response queuing
* and timeouts.
*
* @constructor
* @class Marionette.Drivers.Abstract
* @param {Object} options set options on prototype.
*/
function Abstract(options) {
this._sendQueue = [];
this._responseQueue = [];
this._lastId = 0;
}
Abstract.prototype = {
/**
* Duration in milliseconds to wait before timing out a command
* request.
*
* @property timeout
* @type Number
*/
timeout: 10000,
/**
* Waiting for a command to finish?
*
* @private
* @property _waiting
* @type Boolean
*/
_waiting: true,
/**
* Is system ready for commands?
*
* @property ready
* @type Boolean
*/
ready: false,
/**
* A unique identifier for the current connection.
*
* @property connectionId
* @type Number
*/
connectionId: null,
/**
* Duration in milliseconds defining when to abort an attempt to
* execute script in the remote end.
*
* @method setScriptTiemout
* @param {Number} the timeout value.
*/
setScriptTimeout: function setScriptTimeout(timeout) {
},
/**
* Sends a JSON data structure across the wire to the remote end.
*
* Each command is queued whilst waiting for any pending commands.
* This ensures order of response is correct, but also means the client
* implements a synchronous interface for Marionette.
*
* @method send
*
* @param {(Object|Command|Response)} obj
* An object that can be dumped into a JSON data structure,
* or a message that can be marshaled.
* @param {Function} callback
* Callback to be invoked when a response for the request is
* received.
*/
send: function send(obj, callback) {
if (!this.ready) {
throw new Error('Connection is not ready');
}
if (typeof callback == 'undefined') {
throw new Error('Callback is required');
}
if (obj instanceof Command || obj instanceof Response) {
this.sendMessage(obj, callback);
} else {
this.sendRaw(obj, callback);
}
return this;
},
/**
* Sends a message across the wire to the remote end.
*
* @param {(Command|Response)} message
* The message to send.
* @param {Function} callback
* Callback to be invoked when a response for the request is
* received.
*/
sendMessage: function(message, callback) {
message.id = ++this._lastId;
this.sendRaw(message.toMsg(), callback);
},
/**
* Sends a JSON data structure across the wire to the remote end.
*
* @param {Object} packet
* An object that can be marshaled into a JSON data structure.
* @param {Function} callback
* Callback to be invoked when a response for the request is
* received.
*/
sendRaw: function(packet, callback) {
this._responseQueue.push(callback);
this._sendQueue.push(packet);
this._nextCommand();
},
/**
* Connects to the remote end.
*
* Requires the _connect function to be defined in the concrete class.
*
* MyConcreteClass.prototype._connect = function _connect(){
* // open a socket to marrionete accept response
* // you *must* call _onDeviceResponse with the first
* // response from Marionette it looks like this:
* // {applicationType: "gecko", marionetteProtocol: 2}
* this.connectionId = result.id;
* }
*
* @method connect
* @param {Function} callback executes
* after successfully connecting to the server.
*/
connect: function connect(callback) {
this.ready = true;
this._responseQueue.push(function(data) {
this.marionetteProtocol = data.marionetteProtocol || 1;
this.traits = data.traits;
this.applicationType = data.applicationType;
callback();
}.bind(this));
this._connect();
},
/**
* Destroys connection to server
*
* Will immediately close connection to server
* closing any pending responses.
*
* @method close
*/
close: function() {
this.ready = false;
this._responseQueue.length = 0;
if (this._close) {
this._close();
}
},
/**
* Checks queue if not waiting for a response
* Sends command to websocket server
*
* @private
* @method _nextCommand
*/
_nextCommand: function _nextCommand() {
if (!this._waiting && this._sendQueue.length) {
this._waiting = true;
var next = this._sendQueue.shift();
this._sendCommand(next);
}
},
/**
* Handles responses from devices.
* Will only respond to the event if the connectionId
* is equal to the event id and the client is ready.
*
* @param {Object} data response from server.
* @private
* @method _onDeviceResponse
*/
_onDeviceResponse: function _onDeviceResponse(data) {
if (this.ready && data.id === this.connectionId) {
this._waiting = false;
var resp;
if (this.marionetteProtocol >= 3) {
var msg = Response.fromMsg(data.response);
resp = msg.result;
} else {
resp = data.response;
}
var cb = this._responseQueue.shift();
cb(resp);
this._nextCommand();
}
}
};
module.exports = Abstract;
}.apply(
this,
(this.Marionette) ?
[Marionette('drivers/abstract'), Marionette] :
[module, require('../marionette')]
));

View file

@ -0,0 +1,23 @@
/* global Marionette */
(function(module, ns) {
'use strict';
module.exports = {
Abstract: ns.require('drivers/abstract')
};
if (typeof(window) === 'undefined') {
module.exports.Tcp = require('./tcp');
module.exports.TcpSync = require('./tcp-sync');
} else {
if (typeof(window.TCPSocket) !== 'undefined') {
module.exports.MozTcp = ns.require('drivers/moz-tcp');
}
}
}.apply(
this,
(this.Marionette) ?
[Marionette('drivers'), Marionette] :
[module, require('../marionette')]
));

View file

@ -0,0 +1,233 @@
'use strict';
var debug = require('debug')('marionette:tcp-sync');
var wire = require('json-wire-protocol');
var sockittome = require('sockit-to-me');
var Command = require('../message').Command;
var Response = require('../message').Response;
var DEFAULT_HOST = 'localhost';
var DEFAULT_PORT = 2828;
var SOCKET_TIMEOUT_EXTRA = 500;
function TcpSync(options) {
if (!options) {
options = {};
}
this.host = options.host || DEFAULT_HOST;
this.port = options.port || DEFAULT_PORT;
this.connectionTimeout = options.connectionTimeout || 2000;
this.isSync = true;
this.retryInterval = 300;
this.sockit = new sockittome.Sockit();
this.sockit.setPollTimeout(this.connectionTimeout + SOCKET_TIMEOUT_EXTRA);
this._lastId = 0;
}
TcpSync.prototype._createSocketConfig = function() {
return {host: this.host, port: this.port};
};
TcpSync.prototype.setScriptTimeout = function(timeout) {
this.sockit.setPollTimeout(timeout + SOCKET_TIMEOUT_EXTRA);
};
/**
* Utility to wait for the marionette socket to be ready.
*
* @method waitForSocket
* @param {Object} [options] for timeout.
* @param {Number} [options.interval] time between running test.
* @param {Number} [options.timeout] maximum wallclock time before
* failing test.
* @param {Function} [callback] callback.
*/
TcpSync.prototype.waitForSocket = function(options, callback) {
// the logic is lifted from
// mozilla-central/source/testing/marionette/client/marionette/marionette.py
if (typeof(options) === 'function') {
callback = options;
options = null;
}
options = options || {};
var interval = options.interval || 100;
var timeout = options.timeout || 300000;
var socketTimeout = 1000;
var sockit = new sockittome.Sockit();
var start = Date.now();
var lastDebugMessage = '';
var self = this;
// Use sockittome's built in polling timeout during calls to connect
// to avoid socket misuse.
sockit.setPollTimeout(socketTimeout);
var probeSocket = function() {
try {
sockit.connect(this._createSocketConfig());
var s = sockit.read(16).toString();
sockit.close();
if (s.indexOf(':') != -1) {
callback();
return;
}
} catch (e) {
// This may seem ridiculous, but we have to keep these errors showing up
// but, they often repeat like CRAZY, so, quiet them down by only showing
// each exception we encounter once.
if (lastDebugMessage != e.message) {
lastDebugMessage = e.message;
debug('exception when probing socket', lastDebugMessage);
}
// Above read _may_ fail so it is important to close the socket...
sockit.close();
}
// timeout. Abort.
if ((Date.now() - start) > timeout) {
console.error('Timeout connecting to B2G');
return;
}
// interval delay for the next iteration.
setTimeout(probeSocket.bind(self), interval);
}.bind(this);
debug('probing socket');
probeSocket();
};
TcpSync.prototype.connect = function(callback) {
this.waitForSocket(function _connect() {
try {
this.sockit.connect(this._createSocketConfig());
} catch(err) {
if (Date.now() - this._beginConnect >= this.connectionTimeout) {
callback(err);
}
setTimeout(_connect.bind(this, callback), this.retryInterval);
return;
}
if (!this._beginConnect) {
this._beginConnect = Date.now();
}
// Ensure this method's resolution is asynchronous in all cases
process.nextTick(function() {
debug('socket connected');
delete this._beginConnect;
var resp = this._readResponse();
this.marionetteProtocol = resp.marionetteProtocol || 1;
this.traits = resp.traits;
this.applicationType = resp.applicationType;
callback();
}.bind(this));
}.bind(this));
};
TcpSync.prototype.defaultCallback = function(err, result) {
if (err) {
throw err;
}
return result;
};
// Following the json-wire-protocol implementation, read one byte at a time
// until the 'separator' character is received. The data preceding the
// delimiter describes the length of the JSON string.
TcpSync.prototype._readResponse = function() {
var stream = new wire.Stream();
var data, error;
stream.on('data', function(parsed) {
data = parsed;
});
stream.on('error', function(err) {
error = err;
});
while (!data && !error) {
stream.write(this.sockit.read(1));
}
if (error) {
throw error;
}
if (this.marionetteProtocol >= 3) {
var resp = Response.fromMsg(data);
return resp.error || resp.result;
}
return data;
};
/**
* Sends a JSON data structure across the wire to the remote end.
*
* @param {(Object|Command|Response)} obj
* An object that can be dumped into a JSON data structure,
* or a message that can be marshaled.
* @param {Function} cb
* Callback to be invoked when a response for the request is
* received.
*
* @return {?}
* The return value from the passed in callback.
*/
TcpSync.prototype.send = function(obj, cb) {
if (obj instanceof Command || obj instanceof Response) {
return this.sendMessage(obj, cb);
} else {
return this.sendRaw(obj, cb);
}
};
/**
* Sends a message across the wire to the remote end.
*
* @param {(Command|Response)} msg
* The message to send.
* @param {Function} cb
* Callback to be invoked when a response for the request is received.
*
* @return {?}
* The return value from the passed in callback.
*/
TcpSync.prototype.sendMessage = function(msg, cb) {
msg.id = ++this._lastId;
return this.sendRaw(msg.toMsg(), cb);
};
/**
* Sends a JSON data structure across the wire to the remote end.
*
* @param {Object} data
* An object that can be marshaled into a JSON data structure.
* @param {Function} cb
* Callback to be invoked when a response for the request is received.
*
* @return {?}
* The return value from the passed in callback.
*/
TcpSync.prototype.sendRaw = function(data, cb) {
debug('write', data);
this.sockit.write(wire.stringify(data));
return cb(this._readResponse());
};
TcpSync.prototype.close = function() {
this.sockit.close();
};
module.exports = TcpSync;

View file

@ -0,0 +1,106 @@
'use strict';
var Abstract = require('./abstract');
var CommandStream = require('../../marionette/command-stream');
var debug = require('debug')('marionette:tcp');
var net = require('net');
var retrySocket = require('socket-retry-connect');
/** TCP **/
Tcp.Socket = net.Socket;
/**
* NodeJS only tcp socket driver for marionette.
* See {{#crossLink "Marionette.Drivers.MozTcp"}}{{/crossLink}}
* for the gecko/xpcom vesion of this driver.
*
* @class Marionette.Drivers.Tcp
* @extends Marionette.Drivers.Abstract
* @constructor
* @param {Options} options connection options.
* @param {String} [options.host="localhost"] host.
* @param {Numeric} [options.port="2828"] port.
*/
function Tcp(options) {
if (!options) {
options = {};
}
Abstract.call(this, options);
/**
* The unique identifier of the current connection.
*
* @property connectionId
* @type Number
*/
this.connectionId = 0;
/**
* The remote hostname.
*
* @property host
* @type String
*/
this.host = options.host || 'localhost';
/**
* The remote port number.
*
* @property port
* @type Number
*/
this.port = options.port || 2828;
}
Tcp.prototype = Object.create(Abstract.prototype);
/**
* Sends a command to the server.
*
* @param {Object} cmd remote marionette command.
*/
Tcp.prototype._sendCommand = function _sendCommand(cmd) {
this.client.send(cmd);
};
/**
* Opens TCP socket for marionette client.
*/
Tcp.prototype._connect = function connect() {
var options = {
port: this.port,
host: this.host,
tries: this.tries
};
retrySocket.waitForSocket(options, function(err, socket) {
debug('got socket starting command stream');
this.socket = socket;
this.client = new CommandStream(this.socket);
this.client.on('command', this._onClientCommand.bind(this));
}.bind(this));
};
/**
* Receives command from server.
*
* @param {Object} data response from marionette server.
*/
Tcp.prototype._onClientCommand = function(data) {
this._onDeviceResponse({
id: this.connectionId,
response: data
});
};
/**
* Closes connection to marionette.
*/
Tcp.prototype._close = function close() {
if (this.socket && this.socket.destroy) {
this.socket.destroy();
}
};
/** export */
module.exports = exports = Tcp;

View file

@ -0,0 +1,361 @@
/* global Marionette */
/**
@namespace
*/
(function(module, ns) {
'use strict';
/**
* Creates an element reference
* based on an id and a client instance.
* You should never need to manually create
* an instance of element.
*
* Use {{#crossLink "Marionette.Client/findElement"}}{{/crossLink}} or
* {{#crossLink "Marionette.Client/findElements"}}{{/crossLink}} to create
* instance(s) of this class.
*
* @class Marionette.Element
* @param {String} id id of element.
* @param {Marionette.Client} client client instance.
*/
function Element(id, client) {
this.id = id;
this.client = client;
}
Element.prototype = {
/**
* Sends remote command processes the result.
* Appends element id to each command.
*
* @method _sendCommand
* @chainable
* @private
* @param {Object} command marionette request.
* @param {Function} callback callback function receives the result of
* response[key] as its first argument.
* @param {String} key key in the response to pass to callback.
*
* @return {Object} self.
*/
_sendCommand: function(command, callback, key) {
if (!command.parameters) {
command.parameters = {};
}
if (typeof command.parameters.id == 'undefined') {
command.parameters.id = this.id;
}
var isSync = this.client.isSync;
var result = this.client._sendCommand(command, callback, key);
if (isSync)
return result;
return this;
},
/**
* Finds a single child of this element.
*
* @method findElement
* @param {String} query search string.
* @param {String} method search method.
* @param {Function} callback element callback.
* @return {Object} self.
*/
findElement: function findElement(query, method, callback) {
var result = this.client.findElement(query, method, this.id, callback);
if (this.client.isSync) {
return result;
}
return this;
},
/**
* Finds a all children of this element that match a pattern.
*
* @method findElements
* @param {String} query search string.
* @param {String} method search method.
* @param {Function} callback element callback.
* @return {Array} elements matched the query string.
*/
findElements: function findElement(query, method, callback) {
var result = this.client.findElements(query, method, this.id, callback);
if (this.client.isSync) {
return result;
}
return this;
},
/**
* Shortcut method to execute
* a function with this element as first argument.
* @method scriptWith
* @param {Function|String} script remote script.
* @param {Array} [args] optional arguments for script.
* @param {Function} callback callback when script completes.
* @return {String|Number} return value of the script.
*/
scriptWith: function scriptWith(script, args, callback) {
if (!Array.isArray(args)) {
callback = args;
args = [];
}
return this.client.executeScript(script, [this].concat(args), callback);
},
/**
* Checks to see if two elements are equal
*
* @method equals
* @param {Marionette.Element} element element to test.
* @return {Boolean} true when equal.
*/
equals: function equals(element) {
if (!(element instanceof Element)) {
throw new Error('must pass an element');
}
return this.id === element.id;
},
/**
* Gets attribute value for element.
*
* @method getAttribute
* @param {String} attr attribtue name.
* @param {Function} callback gets called with attribute's value.
* @return {String} the value of the Attribute.
*/
getAttribute: function getAttribute(attr, callback) {
var body = {
name: 'getElementAttribute',
parameters: {
name: attr,
},
};
return this._sendCommand(body, callback, 'value');
},
/**
* Sends typing event keys to element.
*
*
* @method sendKeys
* @param {String|Array} input message to type.
* @param {Function} callback boolean success.
* @return {Object} self.
*/
sendKeys: function sendKeys(input, callback) {
if (!Array.isArray(input)) {
input = [input];
}
var body = {
name: 'sendKeysToElement',
parameters: {
value: input,
},
};
return this._sendCommand(body, callback);
},
/**
* Clicks element.
*
* @method click
* @param {Function} callback boolean result.
* @return {Object} self.
*/
click: function click(callback) {
var body = {name: 'clickElement'};
return this._sendCommand(body, callback);
},
/**
* Gets text of element
*
* @method text
* @param {Function} callback text of element.
* @return {String} text of element.
*/
text: function text(callback) {
var body = {name: 'getElementText'};
return this._sendCommand(body, callback, 'value');
},
/**
* Returns tag name of element.
*
* @method tagName
* @param {Function} callback node style [err, tagName].
* @return {String} tag name of element.
*/
tagName: function tagName(callback) {
var body = {name: 'getElementTagName'};
return this._sendCommand(body, callback, 'value');
},
/**
* Taps an element at given x and y coordinates.
* If no offsets are given, it will be tapped at the center of the element.
*
* @method tap
* @param {Number} [x] offset for the tap.
* @param {Number} [y] offset for the tap.
* @param {Function} [callback] [Error err]
* @return {object} self
*/
tap: function(x, y, callback) {
var body = {
name: 'singleTap',
parameters: {},
};
if (typeof x == 'number') {
body.parameters.x = x;
}
if (typeof y == 'number') {
body.parameters.y = y;
}
return this._sendCommand(body, callback, 'value');
},
/**
* Clears element.
*
* @method clear
* @param {Function} callback value of element.
* @return {Object} self.
*/
clear: function clear(callback) {
var body = {name: 'clearElement'};
return this._sendCommand(body, callback);
},
/**
* Checks if element is selected.
*
* @method selected
* @param {Function} callback boolean argument.
* @return {Object} self.
*/
selected: function selected(callback) {
var body = {name: 'isElementSelected'};
return this._sendCommand(body, callback, 'value');
},
/**
* Checks if element is enabled.
*
* @method enabled
* @param {Function} callback boolean argument.
* @return {Object} self.
*/
enabled: function enabled(callback) {
var body = {name: 'isElementEnabled'};
return this._sendCommand(body, callback, 'value');
},
/**
* Checks if element is displayed.
*
* @method displayed
* @param {Function} callback boolean argument.
* @return {Object} self.
*/
displayed: function displayed(callback) {
var body = {name: 'isElementDisplayed'};
return this._sendCommand(body, callback, 'value');
},
/**
* Returns the size of a given element.
*
* The returned size will be in the following format:
*
* // returned in callback or result of this call in the sync driver.
* {
* width: Number,
* height: Number
* }
*
* @method size
* @param {Function} callback [Error err, Object size].
* @return {Object} self.
*/
size: function size(callback) {
var body = {name: 'getElementRect'};
return this._sendCommand(
body, callback, this.client.protocol == 1 ? 'value' : undefined);
},
/**
* Returns the dictionary with x and y location of an element.
*
* The returned location will be in the following format:
*
* // returned in callback or result of this call in the sync driver.
* {
* x: Number,
* y: Number
* }
*
* @method location
* @param {Function} callback [Error err, Object location].
* @return {Object} self.
*/
location: function location(callback) {
var body = {name: 'getElementRect'};
return this._sendCommand(
body, callback, this.client.protocol == 1 ? 'value' : undefined);
},
/**
* Returns the object with:
* x and y location of the element
* height and width of the element
*
* @method rect
* @param {Function} callback [Error err, Object rect]
* @return {Object} self.
*/
rect: function rect (callback) {
var body = {name: 'getElementRect'};
return this._sendCommand(
body, callback, this.client.protocol == 1 ? 'value' : undefined);
},
/**
* Returns the value of the specified CSS property name.
*
* @method cssProperty
* @param {String} property css property.
* @param {Function} callback [Error err, String value].
* @return {Object} self.
*/
cssProperty: function cssProperty(property, callback) {
var body = {
name: 'getElementValueOfCssProperty',
parameters: {
propertyName: property,
},
};
return this._sendCommand(body, callback, 'value');
}
};
module.exports = Element;
}.apply(
this,
(this.Marionette) ?
[Marionette('element'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,131 @@
/* jshint -W069 */
/* global Marionette */
(function(module, ns) {
'use strict';
var ERRORS = Object.freeze({
'element not accessible': 'ElementNotAccessibleError',
'element not selectable': 'ElementIsNotSelectable',
'element not visible': 'ElementNotVisible',
'invalid cookie domain': 'InvalidCookieDomain',
'invalid element coordinates': 'InvalidElementCoordinates',
'invalid element state': 'InvalidElementState',
'invalid selector': 'InvalidSelector',
'invalid xpath selector': 'XPathLookupError',
'javascript error': 'JavaScriptError',
'no such alert': 'NoAlertOpenError',
'no such element': 'NoSuchElement',
'no such frame': 'NoSuchFrame',
'no such window': 'NoSuchWindow',
'script timeout': 'ScriptTimeout',
'stale element reference': 'StaleElementReference',
'timeout': 'Timeout',
'unable to set cookie': 'UnableToSetCookie',
'unexpected alert open': 'UnexpectedAlertOpen',
'unknown command': 'UnknownCommand',
'unknown error': 'UnknownError',
'webdriver error': 'GenericError',
});
var CODES = Object.freeze({
7: ERRORS['no such element'],
8: ERRORS['no such frame'],
9: ERRORS['unknown command'],
10: ERRORS['stale element reference'],
11: ERRORS['element not visible'],
12: ERRORS['invalid element state'],
13: ERRORS['unknown error'],
15: ERRORS['element not selectable'],
17: ERRORS['javascript error'],
19: ERRORS['invalid xpath selector'],
21: ERRORS['timeout'],
23: ERRORS['no such window'],
24: ERRORS['invalid cookie domain'],
25: ERRORS['unable to set cookie'],
26: ERRORS['unexpected alert open'],
27: ERRORS['no such alert'],
28: ERRORS['script timeout'],
29: ERRORS['invalid element coordinates'],
32: ERRORS['invalid selector'],
56: ERRORS['element not accessible'],
500: ERRORS['webdriver error']
});
var DEFAULT_ERROR = ERRORS['webdriver error'];
/**
* Returns an error object given an error object from the Marionette client.
*
* Codes are from:
* http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
*
* Status strings are from:
* https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
*
* The expected input for protocol version 2 and higher:
*
* {
* error: "javascript error",
* message: "Something",
* stacktrace: "wentwrong@line",
* }
*
* The expected input for protocol version 1:
*
* {
* message: "Something",
* stacktrace: "wentwrong@line",
* status: "javascript error",
* }
*
* @param {Client} client which the error originates from.
* @param {Object} options for error (see above).
*/
function MarionetteError(client, options) {
var error = DEFAULT_ERROR;
if (options.status in CODES) {
error = CODES[options.status];
} else if (options.status in ERRORS) {
error = ERRORS[options.status];
} else if (options.error in ERRORS) {
error = ERRORS[options.error];
}
this.client = client;
this.type = error;
this.name = this.type;
this.message = this.name;
if (options.message)
this.message += ': ' + options.message;
this.message += '\nRemote Stack:\n';
this.message += options.stacktrace || '<none>';
if (Error.captureStackTrace) {
Error.captureStackTrace(this);
} else {
// elsewhere we do horrible try/catch
try {
throw new Error();
} catch (e) {
this.stack = e.stack;
}
}
}
MarionetteError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error
}
});
MarionetteError.ERRORS = ERRORS;
MarionetteError.CODES = CODES;
module.exports = MarionetteError;
}.apply(
this,
(this.Marionette) ?
[Marionette('error'), Marionette] :
[module, require('./marionette')]
));

View file

@ -0,0 +1,128 @@
/* global Marionette */
(function(module, ns) {
'use strict';
function deepmerge(d) {
for (var i = 1; i < arguments.length; ++i) {
var o = arguments[i];
for (var k in o) {
var v = o[k];
if (typeof v == 'object') {
d[k] = deepmerge(d[k], v);
} else {
if (Array.isArray(d)) {
if (d.indexOf(v) < 0) {
d.push(v);
}
} else {
d[k] = v;
}
}
}
}
return d;
}
function cmd(defaults) {
return function(override) {
if (typeof(override) === 'undefined') {
override = {};
}
return deepmerge(defaults, override);
};
}
module.exports = {
connectProto1: cmd({from: 'root', applicationType: 'gecko', traits: []}),
connectProto2: cmd({applicationType: 'gecko', marionetteProtocol: 2}),
getMarionetteID: cmd({type: 'getMarionetteID'}),
getMarionetteIDResponse: cmd({from: 'root', id: 'con1'}),
newSession: cmd({type: 'newSession'}),
newSessionResponseProto1: cmd({
from: 'actor',
value: 'b2g-7',
}),
newSessionResponseProto2: cmd({
sessionId: '{2dddca75-7f78-415f-a7de-63eb2bc9412b}',
capabilities: {browserName: 'firefox'},
}),
getWindow: cmd({name: 'getWindow'}),
getWindowResponse: cmd({value: '3-b2g'}),
getWindows: cmd({name: 'getWindows'}),
getWindowsResponseProto1: cmd({from: 'actor', value: ['1-b2g', '2-b2g']}),
getWindowsResponseProto2: cmd(['1-b2g', '2-b2g']),
getUrl: cmd({name: 'getUrl'}),
getUrlResponse: cmd({value: 'http://localhost/'}),
getLogsResponseProto1: cmd({
from: 'actor',
value: [
['debug', 'wow', 'Fri Apr 27 2012 11:00:32 GMT-0700 (PDT)'],
],
}),
getLogsResponseProto2: cmd([
['debug', 'wow', 'Fri Apr 27 2012 11:00:32 GMT-0700 (PDT)'],
]),
screenshotResponse: cmd({
value: 'data:image/png;base64,iVBOgoAAAANSUhEUgAAAUAAAAHMCAYAAACk4nEJA',
}),
elementEqualsResponse: cmd({value: false}),
findElementResponse: cmd(
{value: {ELEMENT: '{8056e6f7-2213-41d4-9db6-ad77ac7a96d3}'}}),
findElementsResponseProto1: cmd({
from: 'actor',
value: [
{ELEMENT: '{46982c9e-bb0c-486e-a514-d1cf20b42641}'},
{ELEMENT: '{585e70ee-e088-43cc-9b07-a4b078c1b8db}'},
],
}),
findElementsResponseProto2: cmd([
{ELEMENT: '{46982c9e-bb0c-486e-a514-d1cf20b42641}'},
{ELEMENT: '{585e70ee-e088-43cc-9b07-a4b078c1b8db}'},
]),
numberError: cmd({
from: 'actor',
error: {
message: 'you fail',
status: 7,
stacktrace: 'fail@url\nother:300',
},
}),
stringError: cmd({
from: 'actor',
error: {
message: 'you fail',
status: 'no such element',
stacktrace: 'fail@url\nother:300',
},
}),
modernError: cmd({
error: 'no such element',
message: 'you fail',
stacktrace: 'fail@url\nother:300',
}),
//valueProto1: cmd({from: 'actor', value: 'zomg'}),
value: cmd({value: 'zomg'}),
capabilities: cmd({capabilities: {browserName: 'firefox'}}),
//okProto1: cmd({from: 'actor', ok: true}),
ok: cmd({}),
};
}.apply(
this,
(this.Marionette) ?
[Marionette('example-commands'), Marionette] :
[module, require('./marionette')]
));

View file

@ -0,0 +1,20 @@
/* global Marionette */
(function(module, ns) {
'use strict';
var exports = module.exports;
exports.Element = ns.require('element');
exports.Error = ns.require('error');
exports.Client = ns.require('client');
exports.Drivers = ns.require('drivers');
exports.CommandStream = ns.require('command-stream');
exports.Actions = ns.require('actions');
exports.MultiActions = ns.require('multi-actions');
}.apply(
this,
(this.Marionette) ?
[Marionette, Marionette] :
[module, require('./marionette')]
));

View file

@ -0,0 +1,101 @@
(function(global, module) {
'use strict';
/**
* Define a list of paths
* this will only be used in the browser.
*/
var paths = {};
/**
* Exports object is a shim
* we use in the browser to
* create an object that will behave much
* like module.exports
*/
function Exports(path) {
this.path = path;
}
Exports.prototype = {
/**
* Unified require between browser/node.
* Path is relative to this file so you
* will want to use it like this from any depth.
*
*
* var Leaf = ns.require('sub/leaf');
*
*
* @param {String} path path lookup relative to this file.
*/
require: function exportRequire(path) {
if (typeof(window) === 'undefined') {
return require(require('path').join(__dirname, path));
} else {
return paths[path];
}
},
/**
* Maps exports to a file path.
*/
set exports(val) {
return (paths[this.path] = val);
},
get exports() {
return paths[this.path];
}
};
/**
* Module object constructor.
*
*
* var module = Module('sub/leaf');
* module.exports = function Leaf(){}
*
*
* @constructor
* @param {String} path file path.
*/
function Module(path) {
return new Exports(path);
}
Module.require = Exports.prototype.require;
Module.exports = Module;
Module._paths = paths;
/**
* Reference self as exports
* which also happens to be the constructor
* so you can assign items to the namespace:
*
* //assign to Module.X
* //assume module.exports is Module
* module.exports.X = Foo; //Module.X === Foo;
* Module.exports('foo'); //creates module.exports object.
*
*/
module.exports = Module;
/**
* In the browser assign
* to a global namespace
* obviously 'Module' would
* be whatever your global namespace is.
*/
if (typeof(window) !== 'undefined')
window.Marionette = Module;
}(
this,
(typeof(module) === 'undefined') ?
{} :
module
));

View file

@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
function Command(body) {
this.id = null;
this.name = body.name;
this.params = body.parameters || {};
}
Command.prototype.toString = function() {
return 'Command ' + JSON.stringify({
id: this.id,
name: this.name,
params: this.params
});
};
Command.prototype.toMsg = function() {
if (!this.id) {
throw new RangeError('Command ID must be specified');
}
return [Command.TYPE, this.id, this.name, this.params];
};
Command.fromMsg = function(msg) {
return new Command(msg[1], msg[2], msg[3]);
};
Command.TYPE = 0;
function Response(msgId, obj) {
this.id = msgId;
this.error = null;
this.result = null;
if (obj instanceof Error) {
this.error = obj;
} else {
this.result = obj;
}
}
Response.prototype.toString = function() {
return 'Response ' + JSON.stringify({
id: this.id,
error: this.error,
result: this.result
});
};
Response.prototype.toMsg = function() {
return [Response.TYPE, this.id, this.error, this.result];
};
Response.fromMsg = function(msg) {
return new Response(msg[1], msg[2] || msg[3]);
};
Response.TYPE = 1;
module.exports = {
Command: Command,
Response: Response,
};

View file

@ -0,0 +1,64 @@
/* global Marionette */
(function(module, ns) {
'use strict';
/**
* For a multifinger gesture, we can use MultiActions.
* For example, one finger to hold down
* while the other finger moves from one element to another.
*
* @class Marionette.MultiActions
* @param {Marionette.Client} context of a client.
*/
function MultiActions(client) {
this.client = client;
this.multiActions = [];
this.maxLength = 0;
}
MultiActions.prototype = {
/**
* Adds a action chain for execution.
*
* @method add
* @param {Object} action {{#crossLink "Marionette.Actions"}}{{/crossLink}}.
*
* @return {Object} self.
*/
add: function add(action) {
this.multiActions.push(action.actionChain);
if (action.actionChain.length > this.maxLength) {
this.maxLength = action.actionChain.length;
}
return this;
},
/**
* Send multiple action chains that have been added
* to the server side for execution.
*
* @method perform
* @param {Function} callback callback when the perform completes.
*/
perform: function perform(callback) {
var cmd = {
type: 'multiAction',
value: this.multiActions,
max_length: this.maxLength
};
this.client._sendCommand(cmd, callback);
this.multiActions = [];
return this;
}
};
module.exports = MultiActions;
}.apply(
this,
(this.Marionette) ?
[Marionette('multi-actions'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,212 @@
/* global Marionette */
(function(module, ns) {
'use strict';
/**
* Constructor
*
* @param {Object} list of events to add onto responder.
*/
function Responder(events) {
this._$events = {};
if (typeof(events) !== 'undefined') {
this.addEventListener(events);
}
}
/**
* Stringifies request to websocket
*
*
* @param {String} command command name.
* @param {Object} data object to be sent over the wire.
* @return {String} json object.
*/
Responder.stringify = function stringify(command, data) {
return JSON.stringify([command, data]);
};
/**
* Parses request from WebSocket.
*
* @param {String} json json string to translate.
* @return {Object} ex: { event: 'test', data: {} }.
*/
Responder.parse = function parse(json) {
var data;
try {
data = (json.forEach) ? json : JSON.parse(json);
} catch (e) {
throw new Error('Could not parse json: "' + json + '"');
}
return {event: data[0], data: data[1]};
};
Responder.prototype = {
parse: Responder.parse,
stringify: Responder.stringify,
/**
* Events on this instance
*
* @type Object
*/
_$events: null,
/**
* Recieves json string event and dispatches an event.
*
* @param {String|Object} json data object to respond to.
* @param {String} json.event event to emit.
* @param {Object} json.data data to emit with event.
* @param {Object} [params] option number of params to pass to emit.
* @return {Object} result of WebSocketCommon.parse.
*/
respond: function respond(json) {
var event = Responder.parse(json),
args = Array.prototype.slice.call(arguments).slice(1);
args.unshift(event.data);
args.unshift(event.event);
this.emit.apply(this, args);
return event;
},
//TODO: Extract event emitter logic
/**
* Adds an event listener to this object.
*
*
* @param {String} type event name.
* @param {Function} callback event callback.
*/
addEventListener: function addEventListener(type, callback) {
var event;
if (typeof(callback) === 'undefined' && typeof(type) === 'object') {
for (event in type) {
if (type.hasOwnProperty(event)) {
this.addEventListener(event, type[event]);
}
}
return this;
}
if (!(type in this._$events)) {
this._$events[type] = [];
}
this._$events[type].push(callback);
return this;
},
/**
* Adds an event listener which will
* only fire once and then remove itself.
*
*
* @param {String} type event name.
* @param {Function} callback fired when event is emitted.
*/
once: function once(type, callback) {
var self = this;
function onceCb() {
self.removeEventListener(type, onceCb);
callback.apply(this, arguments);
}
this.addEventListener(type, onceCb);
return this;
},
/**
* Emits an event.
*
* Accepts any number of additional arguments to pass unto
* event listener.
*
* @param {String} eventName name of the event to emit.
* @param {Object} [arguments] additional arguments to pass.
*/
emit: function emit() {
var args = Array.prototype.slice.call(arguments),
event = args.shift(),
eventList,
self = this;
if (event in this._$events) {
eventList = this._$events[event];
eventList.forEach(function(callback) {
callback.apply(self, args);
});
}
return this;
},
/**
* Removes all event listeners for a given event type
*
*
* @param {String} event event type to remove.
*/
removeAllEventListeners: function removeAllEventListeners(name) {
if (name in this._$events) {
//reuse array
this._$events[name].length = 0;
}
return this;
},
/**
* Removes a single event listener from a given event type
* and callback function.
*
*
* @param {String} eventName event name.
* @param {Function} callback same instance of event handler.
*/
removeEventListener: function removeEventListener(name, callback) {
var i, length, events;
if (!(name in this._$events)) {
return false;
}
events = this._$events[name];
for (i = 0, length = events.length; i < length; i++) {
if (events[i] && events[i] === callback) {
events.splice(i, 1);
return true;
}
}
return false;
}
};
Responder.prototype.on = Responder.prototype.addEventListener;
Responder.prototype.removeListener = Responder.prototype.removeEventListener;
module.exports = Responder;
}.apply(
this,
(this.Marionette) ?
[Marionette('responder'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,85 @@
'use strict';
var CommandStream = require('../marionette/command-stream');
var debug = require('debug')('marionette:connection-manager');
/**
* Socket class
*/
ConnectionManger.Socket = require('net').Socket;
/**
* @class
* @constructor
*/
function ConnectionManger() {
this.connections = {};
}
ConnectionManger.prototype = {
currentId: 0,
defaultPort: 2828,
/**
* Opens a connection and returns the id and connection
*
* manager.open()
* // => {id: 0, connection: new CommandStream() }
*
* @this
* @param {Numeric} port port number.
* @return {Object} { id: connectionId, connection: commandStream }.
*/
open: function open(port) {
var socket,
stream,
id = this.currentId++,
closeFn = this.close.bind(this, id);
port = port || this.defaultPort;
socket = new ConnectionManger.Socket();
socket.on('close', closeFn);
socket.on('end', closeFn);
stream = new CommandStream(socket);
socket.on('error', function() {
debug(arguments);
});
socket.on('end', function() {
debug('socket close');
});
socket.connect(port);
this.connections[id] = stream;
return { id: id, connection: stream };
},
/**
* Returns connection by id
*
* @this
* @param {Number} id connection id.
* @return {CommandStream} command stream for connection id.
*/
get: function get(id) {
return this.connections[id];
},
/**
* Closes and removes a connection.
*
* @this
* @param {Number} id connection id to close.
*/
close: function close(id) {
delete this.connections[id];
}
};
module.exports = exports = ConnectionManger;

View file

@ -0,0 +1,20 @@
{
"name": "marionette-client",
"version": "1.9.4",
"author": "The Gaia Team <dev-gaia@lists.mozilla.org>",
"description": "Marionette Javascript Client",
"main": "lib/marionette/index",
"dependencies": {
"debug": "~0.6",
"json-wire-protocol": "file:../json-wire-protocol",
"socket-retry-connect": "file:../socket-retry-connect",
"sockit-to-me": "file:../sockit-to-me"
},
"devDependencies": {
"yuidocjs": "~0.3",
"node-static": "~0.6",
"chai": "~1.7.2"
}
}

View file

@ -0,0 +1,103 @@
'use strict';
var assert = require('chai').assert;
var helper = global.helper = {};
global.assert = assert;
var deepEqual = assert.deepEqual.bind(assert);
assert.deepEqual = function(one, other, msg) {
// Hack so that we pass when comparing
// { '0': foo } with [foo]
deepEqual(argsToArrays(one), argsToArrays(other), msg);
};
function argsToArrays(obj) {
if (typeof obj !== 'object') {
return obj;
}
var proto;
try {
proto = Object.getPrototypeOf(obj);
if (proto && Object.keys(proto).length) {
// This is something fancy...
return obj;
}
} catch (err) {
// uh wtf
return obj;
}
if (!Array.isArray(obj) && isArrayLike(obj)) {
obj = Array.prototype.slice.call(obj);
}
for (var key in obj) {
obj[key] = argsToArrays(obj[key]);
}
return obj;
}
function isArrayLike(obj) {
return Object.keys(obj).every(function(value) {
var representation = parseInt(value);
return typeof representation === 'number' && !isNaN(representation);
});
}
helper.requireLib = function(path) {
return require('../lib/' + path);
};
helper.nsFind = function(obj, string) {
var result = obj;
var parts = string.split('.');
var part;
while ((part = parts.shift())) {
if (!result[part]) {
throw new Error('Cannot find ' + string + ' in object ');
}
result = result[part];
}
return result;
};
helper.require = function(path, component, cb) {
if (typeof(component) === 'function') {
//new module pattern
cb = component;
component = path;
if (/^support/.test(path)) {
path = '/test/' + path;
} else {
path = '/lib/marionette/' + path;
}
} else {
//old system
path = '/lib/' + path;
}
cb(require('..' + path));
};
helper.require('responder', function(obj) {});
helper.require('example-commands', function(obj) {
global.exampleCmds = obj;
});
helper.require('support/device-interaction', function(obj) {
global.DeviceInteraction = obj;
});
helper.require('support/socket', function(obj) {
global.FakeSocket = obj;
});
helper.require('support/mock-driver', function(obj) {
global.MockDriver = obj;
});

View file

@ -0,0 +1,71 @@
/* global DeviceInteraction, exampleCmds, helper */
'use strict';
require('../helper');
suite('scope', function() {
var client = marionette.client(),
Actions,
device,
subject;
helper.require('actions', function(obj) {
Actions = obj;
});
device = new DeviceInteraction(exampleCmds, function() {
return subject;
});
suite('actions', function() {
setup(function() {
subject = new Actions(client);
});
test('can move the page', function() {
var bodyElement;
client.goUrl('http://yahoo.com');
client.waitFor(function() {
client.findElement('body', function(error, element) {
if (error) {
console.log('cannot find');
return;
}
bodyElement = element;
});
if (bodyElement !== undefined) {
return true;
}
});
subject.
press(bodyElement, 100, 100).
moveByOffset(0, 10).
perform();
// TODO: Bug 965385 - Use `Actions#wait` in integration tests
// Model this pause with an invocation of `Actions#wait` once "Bug 962645
// - Action chains only handle one press->release chain" is resolved.
client.executeAsyncScript(function() {
setTimeout(marionetteScriptFinished, 50);
});
subject.
release().
perform();
// TODO: Bug 965385 - Use `Actions#wait` in integration tests
// Model this pause with an invocation of `Actions#wait` once "Bug 962645
// - Action chains only handle one press->release chain" is resolved.
client.executeAsyncScript(function() {
setTimeout(marionetteScriptFinished, 50);
});
subject.
flick(bodyElement, 0, 0, 0, 200).
perform();
});
});
});

View file

@ -0,0 +1,29 @@
/* global marionette, setup */
'use strict';
var querystring = require('querystring');
suite('client methods', function() {
var client = marionette.client();
setup(function() {
client.goUrl('data:text/html,' + querystring.escape(
'<div id="host"></div>'));
client.executeScript(function() {
var host = document.getElementById('host');
var root = host.createShadowRoot();
var button = document.createElement('button');
button.id = 'button';
button.textContent = 'Foo';
root.appendChild(button);
});
});
test('#switchToShadowRoot', function() {
var host = client.findElement('#host');
client.switchToShadowRoot(host);
var button = client.findElement('#button');
button.click();
});
});

View file

@ -0,0 +1,18 @@
/* global assert */
'use strict';
require('../../helper');
suite('drivers/tcp-sync', function() {
var client = marionette.client();
test('can execute sync commands', function() {
client.goUrl('http://yahoo.com');
client.goUrl('http://google.com');
client.goUrl('http://yahoo.com');
var location = client.executeScript(function() {
return window.location.href;
});
assert.ok(location.indexOf('yahoo.com') !== -1);
});
});

View file

@ -0,0 +1,84 @@
/* global assert */
'use strict';
var querystring = require('querystring');
require('../helper');
suite('element methods', function() {
var client = marionette.client();
setup(function() {
client.goUrl('data:text/html,' + querystring.escape(
'<div id="el" style="width: 50px; height: 50px">cheese</div>'));
});
test('#displayed', function() {
var element = client.findElement('html');
assert.strictEqual(element.displayed(), true);
});
test('#scriptWith', function() {
var element = client.findElement('html');
var evaled = element.scriptWith(function(el, arg) {
return el.tagName + ' ' + arg;
}, ['FTW!']);
assert.equal(evaled, 'HTML FTW!');
});
test('#findElement', function() {
var el = client.findElement('html');
assert.instanceOf(el, client.Element);
});
test('#findElements', function() {
var els = client.findElements('html');
assert.isArray(els);
els.forEach(function(el) { assert.instanceOf(el, client.Element); });
});
test('#findElement - missing', function() {
var err;
try {
client.findElement('#fooobaramazingmissing');
} catch (e) {
err = e;
}
if (!err) throw new Error('missing element did not trigger an error');
assert.equal(err.type, 'NoSuchElement');
});
test('#cssProperty', function() {
var body = client.findElement('body');
var font = body.cssProperty('font-size');
assert.ok(font, 'returns a css property value');
});
test('#size', function () {
var element = client.findElement('#el');
var size = element.size();
assert.property(size, 'width');
assert.property(size, 'height');
assert.equal(size.width, 50);
assert.equal(size.height, 50);
});
test('#location', function () {
var element = client.findElement('#el');
var location = element.location();
assert.property(location, 'x');
assert.property(location, 'y');
});
test('#rect', function () {
var element = client.findElement('#el');
var rect = element.rect();
assert.property(rect, 'x');
assert.property(rect, 'y');
assert.property(rect, 'width');
assert.property(rect, 'height');
assert.equal(rect.width, 50);
assert.equal(rect.height, 50);
});
});

View file

@ -0,0 +1,43 @@
/* global assert, helper */
'use strict';
require('../helper');
suite('error handling', function() {
var MarionetteError;
helper.require('error', function(obj) {
MarionetteError = obj;
});
var client = marionette.client();
test('catch error', function() {
try {
client.executeScript(function() {
throw new Error('e');
});
} catch (e) {
e.client.screenshot();
}
});
test('throws no such element error', function() {
try {
client.findElement('cheese');
assert.fail('expected no such element error to be thrown');
} catch (e) {
assert.include(e.toString(), 'NoSuchElement');
assert.property(e, 'type');
assert.equal(e.type, 'NoSuchElement');
}
});
test('throws javascript error', function() {
try {
client.executeScript(function() { throw Error('rochefort'); });
} catch (e) {
assert.include(e.toString(), 'JavaScriptError');
assert.property(e, 'type');
assert.equal(e.type, 'JavaScriptError');
}
});
});

View file

@ -0,0 +1,18 @@
'use strict';
module.exports.skipInitialError = function (client) {
setup(function(done) {
client.executeAsyncScript(function() {
setTimeout(marionetteScriptFinished, 10);
}, function(err) {
// hacked work around for gaia exception at startup.
if (
err &&
err.message &&
err.message.indexOf('Expected IccHelper') !== -1
) {
return done();
}
done(err);
});
});
};

View file

@ -0,0 +1,36 @@
/* global assert */
'use strict';
require('../helper');
suite('scope', function() {
var client = marionette.client(),
helper = require('./helper'),
timeoutA,
timeoutB;
helper.skipInitialError(client);
setup(function() {
timeoutA = client.scope({ scriptTimeout: 1000 });
timeoutB = client.scope({ scriptTimeout: 250 });
});
test('should handle scope switching', function() {
function sleep() {
setTimeout(function() {
marionetteScriptFinished();
}, 600);
}
timeoutA.executeAsyncScript(sleep);
var err;
try {
timeoutB.executeAsyncScript(sleep);
} catch (e) {
err = e;
}
// sleep throws error on shorter timeout
assert.ok(err);
});
});

View file

@ -0,0 +1,95 @@
/* global assert */
'use strict';
require('../helper');
suite('scope', function() {
var client = marionette.client(),
helper = require('./helper');
helper.skipInitialError(client);
suite('sync waitFor', function() {
test('should not yield in sync code', function() {
var tries = 0;
var succeed = 10;
// short signature
client.waitFor(function() {
return (++tries === succeed);
});
assert.strictEqual(tries, 10);
});
test('should eventually timeout', function() {
var timeout = 450;
var start = Date.now();
var err;
try {
client.waitFor(function() {
return false;
}, { timeout: timeout });
} catch (e) {
err = e;
}
assert.ok(err);
assert.operator(Date.now() - start, '>=', timeout);
});
test('should do the mocha sync', function() {
var tries = 0, success = 3;
client.waitFor(function fn(waitForComplete) {
tries += 1;
if (tries === success) {
waitForComplete(null, true);
}
setTimeout(fn.bind(this, waitForComplete), 10);
});
assert.strictEqual(tries, success);
});
test('should do the mocha fail sync', function(done) {
var err = new Error('failure!');
try {
client.waitFor(function(waitForComplete) {
waitForComplete(err);
});
} catch (e) {
assert.strictEqual(e, err);
done();
}
});
});
suite('callback style wait for calls', function() {
test('should throw an error when given', function() {
var err = new Error('failure!');
var isSync = false;
try {
client.waitFor(function(waitForComplete) {
waitForComplete(err);
}, function(e) {
assert.strictEqual(e, err);
isSync = true;
});
} catch(e) {
assert.equal(e, err);
}
assert.ok(isSync, 'callback style is sync');
});
test('should fire when async condition is met', function() {
var tries = 0;
var success = 3;
client.waitFor(function() {
return ++tries === success;
}, function() {
assert.strictEqual(tries, 3);
});
assert.equal(tries, 3, 'is sync');
});
});
});

View file

@ -0,0 +1 @@
--profile-base profile.js

View file

@ -0,0 +1,225 @@
/* global DeviceInteraction, MockDriver, exampleCmds, assert, helper */
'use strict';
suite('marionette/actions', function() {
var driver, subject, client, device,
Actions, Client;
helper.require('actions', function(obj) {
Actions = obj;
});
helper.require('client', function(obj) {
Client = obj;
});
device = new DeviceInteraction(exampleCmds, function() {
return subject;
});
setup(function() {
driver = new MockDriver();
client = new Client(driver);
subject = new Actions(client);
});
suite('initialization', function() {
test('should set client', function() {
assert.strictEqual(subject.client, client);
});
});
suite('.press', function() {
var element, x, y;
setup(function() {
element = { id: '{fake-uuid-root}' };
x = 0;
y = 0;
subject.press(element, x, y);
});
test('should have a press action in the chain', function() {
var pressAction = [['press', element.id, x, y]];
assert.deepEqual(subject.actionChain, pressAction);
});
});
suite('.release', function() {
setup(function() {
subject.release();
});
test('should have a release action in the chain', function() {
var releaseAction = [['release']];
assert.deepEqual(subject.actionChain, releaseAction);
});
});
suite('.move', function() {
var element;
setup(function() {
element = { id: '{fake-uuid-root}' };
subject.move(element);
});
test('should have a move action in the chain', function() {
var moveAction = [['move', element.id]];
assert.deepEqual(subject.actionChain, moveAction);
});
});
suite('.moveByOffset', function() {
var x, y;
setup(function() {
x = 1;
y = 1;
subject.moveByOffset(x, y);
});
test('should have a move by offset action in the chain', function() {
var moveByOffsetAction = [['moveByOffset', x, y]];
assert.deepEqual(subject.actionChain, moveByOffsetAction);
});
});
suite('.wait', function() {
var time;
setup(function() {
time = 1;
subject.wait(time);
});
test('should have a wait action in the chain', function() {
var waitAction = [['wait', time]];
assert.deepEqual(subject.actionChain, waitAction);
});
});
suite('.cancel', function() {
setup(function() {
subject.cancel();
});
test('should have a cancel action in the chain', function() {
var cancelAction = [['cancel']];
assert.deepEqual(subject.actionChain, cancelAction);
});
});
suite('.tap', function() {
var element, x, y;
setup(function() {
element = { id: '{fake-uuid-root}' };
x = 0;
y = 0;
subject.tap(element, x, y);
});
test('should have a tap action in the chain', function() {
var tapAction = [
['press', element.id, x, y],
['release']
];
assert.deepEqual(subject.actionChain, tapAction);
});
});
suite('.doubleTap', function() {
var element, x, y;
setup(function() {
element = { id: '{fake-uuid-root}' };
x = 0;
y = 0;
subject.doubleTap(element, x, y);
});
test('should have a double tap action in the chain', function() {
var doubleTapAction = [
['press', element.id, x, y],
['release'],
['press', element.id, x, y],
['release']
];
assert.deepEqual(subject.actionChain, doubleTapAction);
});
});
suite('.flick', function() {
var element, x1, y1, x2, y2;
setup(function() {
element = { id: '{fake-uuid-root}' };
x1 = 0;
y1 = 0;
x2 = 100;
y2 = 100;
});
function shouldHaveFlickAction() {
var firstAction = ['press', element.id, x1, y1];
var lastAction = ['release'];
var lastActionIndex = subject.actionChain.length - 1;
assert.deepEqual(subject.actionChain[0], firstAction);
for (var i = 1; i < lastActionIndex; i += 2) {
assert.strictEqual(subject.actionChain[i][0], 'moveByOffset');
assert.strictEqual(subject.actionChain[i + 1][0], 'wait');
}
assert.deepEqual(subject.actionChain[lastActionIndex], lastAction);
}
test('should have a flick action in the chain', function() {
subject.flick(element, x1, y1, x2, y2);
shouldHaveFlickAction();
});
test('should have a flick action in the chain ' +
'when the duration param is 300', function() {
var duration = 300;
subject.flick(element, x1, y1, x2, y2, duration);
shouldHaveFlickAction();
});
});
suite('.longPress', function() {
var element, time;
setup(function() {
element = { id: '{fake-uuid-root}' };
time = 1;
subject.longPress(element, time);
});
test('should have a long press action in the chain', function() {
var longPressAction = [
['press', element.id],
['wait', time],
['release']
];
assert.deepEqual(subject.actionChain, longPressAction);
});
});
suite('.perform', function() {
device.
issues('perform').
shouldSend({
name: 'actionChain',
parameters: {
chain: [],
nextId: null
}
}).
serverResponds('value').
callbackReceives('value');
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,213 @@
/* global assert, helper */
'use strict';
suite('marionette/command-stream', function() {
var subject, socket,
Responder,
CommandStream;
helper.require('responder', function(obj) {
Responder = obj;
});
helper.require('command-stream', function(obj) {
CommandStream = obj;
});
setup(function() {
socket = new Responder();
subject = new CommandStream(socket);
});
suite('initialization', function() {
test('should save socket', function() {
assert.strictEqual(subject.socket, socket);
});
test('should be an event emitter', function() {
assert.instanceOf(subject.on, Function);
});
});
suite('socket event: data', function() {
var calledWith, data;
setup(function() {
data = {};
subject.on('command', function() {
calledWith = Array.prototype.slice.call(arguments);
});
subject.socket.emit('data', new Buffer(subject.stringify(data)));
});
test('should call add', function() {
assert.deepEqual(calledWith, [data]);
});
});
suite('.stringify', function() {
var command = {works: true},
result;
function shouldStringify() {
test('should return json (n:json)', function() {
var string = JSON.stringify(command);
assert.strictEqual(result, String(string.length) + ':' + string);
});
}
suite('when given an object', function() {
setup(function() {
result = subject.stringify(command);
});
shouldStringify();
});
suite('when given a string', function() {
setup(function() {
command = 'some random string';
result = subject.stringify(command);
});
shouldStringify();
});
});
suite('.send', function() {
var calledWith = [], data = {uniq: true};
function sendsToSocket(fnName) {
setup(function() {
subject.socket[fnName] = function() {
calledWith = Array.prototype.slice.call(arguments);
};
subject.send(data);
});
test('should write to socket', function() {
assert.strictEqual(calledWith[0], subject.stringify(data));
});
}
suite('when using socket.send', function() {
sendsToSocket('send');
});
suite('when using socket.write', function() {
sendsToSocket('write');
test('should write in utf8', function() {
assert.strictEqual(calledWith[1], 'utf8');
});
});
});
suite('.add', function() {
var commands = [],
chunks = {},
commandList = {
success: {'from': 'someX'},
fail: {'from': 'zya'}
};
function add(string, log) {
var buffer = new Buffer(string);
subject.add(buffer);
}
function emitsCommand(name, index) {
index = index || 0;
name = name || 'success';
test('should emit ' + name + ' command at #' + index + '', function() {
assert.deepEqual(commands[index], commandList[name]);
});
}
function hasCommands(number) {
test('should have ' + String(number) + ' of commands', function() {
assert.strictEqual(commands.length, number);
});
}
setup(function() {
commands = [];
Object.keys(commandList).forEach(function(key) {
chunks[key] = subject.stringify(commandList[key]);
});
subject.on(subject.commandEvent, function(response) {
commands.push(response);
});
});
suite('when given the entire command', function() {
setup(function() {
add(chunks.success);
});
emitsCommand('success', 0);
hasCommands(1);
});
suite('when given single command in multiple chunks', function() {
setup(function() {
add(chunks.success.slice(0, 1));
add(chunks.success.slice(1, 2));
add(chunks.success.slice(2));
});
emitsCommand('success', 0);
hasCommands(1);
});
suite('when given multiple commands in two chunks', function() {
setup(function() {
var chunk = chunks.success + chunks.fail,
piece1,
piece2,
half = Math.floor(chunk.length / 2);
piece1 = chunk.slice(0, half);
piece2 = chunk.slice(half);
//sanity check
assert.strictEqual(piece1 + piece2, chunk);
add(piece1);
add(piece2);
});
emitsCommand('success', 0);
emitsCommand('fail', 1);
hasCommands(2);
});
suite('when given both commands in one chunk', function() {
setup(function() {
add(chunks.fail + chunks.success);
});
emitsCommand('fail', 0);
emitsCommand('success', 1);
hasCommands(2);
});
});
});

View file

@ -0,0 +1,358 @@
/* global assert, exampleCmds, helper */
'use strict';
suite('marionette/drivers/abstract', function() {
var subject,
Backend,
sent = [];
helper.require('drivers/abstract', function(obj) {
Backend = obj;
});
setup(function() {
subject = new Backend();
sent = [];
subject._sendCommand = function() {
sent.push(arguments);
};
});
suite('initialization', function() {
test('should setup ._sendQueue', function() {
assert.deepEqual(subject._sendQueue, []);
});
test('should not be ready', function() {
assert.strictEqual(subject.ready, false);
});
test('should setup ._responseQueue', function() {
assert.instanceOf(subject._responseQueue, Array);
});
test('should have timeout set to 10000', function() {
assert.strictEqual(subject.timeout, 10000);
});
test('should be _waiting', function() {
assert.strictEqual(subject._waiting, true);
});
});
suite('event: device response', function() {
var callback,
callbackResponse,
response;
setup(function() {
callbackResponse = null;
response = {
id: 10,
response: {
from: 'marionette',
value: 'hit'
}
};
//emulate connect
subject.connectionId = 10;
subject.ready = true;
subject._waiting = false;
callback = function() {
callbackResponse = arguments;
};
subject.send(exampleCmds.newSession(), callback);
assert.strictEqual(subject._waiting, true);
});
suite('when response is for different device', function() {
setup(function() {
subject.connectionId = 101;
subject._onDeviceResponse(response);
});
test('should trigger response callbacks', function() {
assert.strictEqual(callbackResponse, null);
});
});
suite('when response is for device id', function() {
var calledNext;
setup(function() {
calledNext = false;
subject._nextCommand = function() {
calledNext = true;
Backend.prototype._nextCommand.apply(this, arguments);
};
subject._onDeviceResponse(response);
});
test('should trigger response callbacks', function() {
assert.deepEqual(callbackResponse[0], response.response);
});
test('should clear response queue', function() {
assert.strictEqual(subject._responseQueue.length, 0);
});
test('should not be waiting', function() {
assert.strictEqual(subject._waiting, false);
});
});
});
suite('.close', function() {
var calledClose;
setup(function() {
calledClose = false;
subject.ready = true;
subject._responseQueue = [function() {}];
subject._close = function() {
calledClose = true;
};
subject.close();
});
test('should call _close', function() {
assert.strictEqual(calledClose, true);
});
test('should not be ready', function() {
assert.strictEqual(subject.ready, false);
});
test('should clean up _responseQueue', function() {
assert.strictEqual(subject._responseQueue.length, 0);
});
});
suite('.connect with protocol 1', function() {
var cmd, calledChild;
setup(function(done) {
cmd = exampleCmds.connectProto1();
calledChild = false;
subject._connect = function() {
subject.connectionId = 10;
calledChild = true;
//this will cause connect to callback to fire
subject._onDeviceResponse({
id: 10,
response: cmd
});
};
assert.strictEqual(subject._waiting, true);
subject.connect(function() {
done();
});
});
test('should set .marionetteProtocol', function() {
assert.property(subject, 'marionetteProtocol');
assert.strictEqual(subject.marionetteProtocol, 1 /* fallback */);
});
test('should set .applicationType', function() {
assert.property(subject, 'applicationType');
assert.strictEqual(subject.applicationType, cmd.applicationType);
});
test('should set .traits', function() {
assert.property(subject, 'traits');
assert.strictEqual(subject.traits, cmd.traits);
});
test('should call _connect', function() {
assert.strictEqual(calledChild, true);
});
test('should not be waiting', function() {
assert.strictEqual(subject._waiting, false);
});
test('should be ready', function() {
assert.strictEqual(subject.ready, true);
});
});
suite('.connect with protocol 2', function() {
var cmd, calledChild;
setup(function(done) {
cmd = exampleCmds.connectProto2();
calledChild = false;
subject._connect = function() {
subject.connectionId = 10;
calledChild = true;
//this will cause connect to callback to fire
subject._onDeviceResponse({
id: 10,
response: cmd
});
};
assert.strictEqual(subject._waiting, true);
subject.connect(function() {
done();
});
});
test('should set .marionetteProtocol', function() {
assert.property(subject, 'marionetteProtocol');
assert.strictEqual(subject.marionetteProtocol, cmd.marionetteProtocol);
});
test('should set .applicationType', function() {
assert.property(subject, 'applicationType');
assert.strictEqual(subject.applicationType, cmd.applicationType);
});
test('should call _connect', function() {
assert.strictEqual(calledChild, true);
});
test('should not be waiting', function() {
assert.strictEqual(subject._waiting, false);
});
test('should be ready', function() {
assert.strictEqual(subject.ready, true);
});
});
suite('._nextCommand', function() {
var cmd1, cmd2;
setup(function() {
cmd1 = exampleCmds.newSession();
cmd2 = exampleCmds.newSession({isOther: true});
subject._sendQueue[0] = cmd1;
subject._sendQueue[1] = cmd2;
});
suite('when waiting', function() {
setup(function() {
subject._waiting = true;
subject._nextCommand();
});
test('should not send command to server', function() {
assert.strictEqual(sent.length, 0);
});
});
suite('when not waiting', function() {
setup(function() {
subject._waiting = false;
subject._nextCommand();
});
test('should be waiting', function() {
assert.strictEqual(subject._waiting, true);
});
test('should send command to server', function() {
assert.deepEqual(sent[0][0], cmd1);
});
});
suite('when there are no comamnds and we are not waiting', function() {
setup(function() {
//emulate connect
subject._waiting = false;
subject._responseQueue = [];
subject._sendQueue = [];
subject._nextCommand();
});
test('should not be waiting', function() {
assert.strictEqual(subject._waiting, false);
});
});
});
suite('.send', function() {
var cmd, cb = function() {};
setup(function() {
cmd = exampleCmds.newSession();
});
suite('when device is not ready', function() {
test('should throw an error', function() {
assert.throws(function() {
subject.send({ type: 'newSession' });
}, /not ready/);
});
});
suite('when not waiting for a response', function() {
setup(function() {
//emulate connect
subject.ready = true;
subject._waiting = false;
subject.send(cmd, cb);
});
test('should send command', function() {
assert.strictEqual(sent.length, 1);
});
test('should be waiting', function() {
assert.strictEqual(subject._waiting, true);
});
});
suite('when waiting for a response', function() {
var nextCalled;
setup(function() {
subject.ready = true;
subject._waiting = true;
nextCalled = false;
subject._nextCommand = function() {
nextCalled = true;
};
subject.send(cmd, cb);
});
test('should call next', function() {
assert.strictEqual(nextCalled, true);
});
test('should add send command to queue', function() {
assert.strictEqual(subject._sendQueue[0], cmd);
});
test('should add calback to response queue', function() {
assert.strictEqual(subject._responseQueue[0], cb);
});
});
});
});

View file

@ -0,0 +1,33 @@
'use strict';
var debug = require('debug')('marionette:tcp-sync-test-server');
var net = require('net');
var response = '54:{"applicationType": "gecko", "marionetteProtocol": 2}';
var server = net.createServer(function(connection) { //'connection' listener
debug('server connected');
connection.on('end', function() {
debug('server disconnected');
});
connection.on('error', function(e) {
debug('error', e);
});
connection.write(response);
connection.pipe(connection);
});
function runServer(port) {
server.listen(port, function() { //'listening' listener
debug('server bound');
});
}
if (require.main === module) {
try {
runServer(1234);
}
catch(e) {
}
}

View file

@ -0,0 +1,34 @@
'use strict';
var fork = require('child_process').fork;
suite('marionette/drivers/tcp-sync', function() {
var child = null;
teardown(function() {
if (child) {
child.kill();
child = null;
}
});
test('can wait for marionette socket', function(done) {
this.timeout(5000);
var TcpSync =
require(__dirname + '/../../../lib/marionette/drivers/tcp-sync');
var tcpSync = new TcpSync({port: 1234, host: 'localhost'});
tcpSync.waitForSocket( {timeout: 3000} , function() {
done();
});
setTimeout(function () {
fork(__dirname + '/tcp-sync-test-server.js');
}, 1000);
});
});

View file

@ -0,0 +1,155 @@
/* global assert, helper */
'use strict';
suite('marionette/drivers/tcp', function() {
if (typeof(window) !== 'undefined') {
return;
}
var Abstract,
Driver = require('../../../lib/marionette/drivers/tcp'),
FakeSocket = require('../../support/socket');
helper.require(
'marionette/drivers/abstract',
'Marionette.Drivers.Abstract', function(obj) {
Abstract = obj;
}
);
function issueFirstResponse() {
subject._onDeviceResponse({
id: subject.connectionId,
response: {}
});
}
var subject,
RealSocket,
sockets = [];
var socketRetry = require('socket-retry-connect');
var realWaitForSocket;
setup(function() {
realWaitForSocket = socketRetry.waitForSocket;
RealSocket = Driver.Socket;
Driver.Socket = FakeSocket;
FakeSocket.sockets = sockets;
socketRetry.waitForSocket = function(options, callback) {
var socket = new FakeSocket(options.port);
callback(null, socket);
};
});
teardown(function() {
socketRetry.waitForSocket = realWaitForSocket;
Driver.Socket = RealSocket;
});
setup(function() {
subject = new Driver();
});
test('should accept port and host', function() {
var subject = new Driver({
port: 8888,
host: 'foobar'
});
assert.strictEqual(subject.port, 8888);
assert.strictEqual(subject.host, 'foobar');
});
suite('._sendCommand', function() {
var sent = [];
setup(function(done) {
subject.connect(function() {
subject.client.send = function() {
sent.push(arguments);
};
subject.client.send({
type: 'foo',
});
done();
});
// issue first response so connect will fire
issueFirstResponse();
});
test('should send request to socket', function() {
assert.deepEqual(sent, [
[{type: 'foo'}]
]);
});
});
// TODO(gaye): What is going on here / what is emtest()?
suite.skip('client event: command', function() {
var sent = [];
setup(function() {
sent.length = 0;
subject._onDeviceResponse = function() {
sent.push(arguments);
};
subject._connect();
subject.client.emtest('command', { type: 'foo' });
});
test('should call onDeviceResponse', function() {
assert.deepEqual(sent, [
[{ id: 0, response: {type: 'foo'} }]
]);
});
});
suite('._connect', function() {
suite('retrying', function() {
var net = require('net');
var port = 60066;
setup(function() {
Driver.Socket = RealSocket;
socketRetry.waitForSocket = realWaitForSocket;
subject = new Driver({ port: port });
});
test('should eventually connect', function(done) {
subject._connect();
setTimeout(function() {
var server = net.createServer(function(socket) {
server.close();
done();
}).listen(port);
}, 50);
});
});
});
suite('._close', function() {
setup(function(done) {
subject.connect(function() {
subject.close();
done();
});
issueFirstResponse();
});
test('should close socket', function() {
assert.strictEqual(subject.socket.destroyed, true);
});
});
});

View file

@ -0,0 +1,283 @@
/* global DeviceInteraction, MockDriver, assert, exampleCmds, helper */
'use strict';
// this is hack to ensure device interactions are loaded
suite('marionette/element', function() {
var driver, subject, client, id, device,
Element, Client;
helper.require('element', function(obj) {
Element = obj;
});
helper.require('client', function(obj) {
Client = obj;
});
id = '{fake-uuid-root}';
function simpleCommand(method, type, response, key) {
suite('.' + method, function() {
device.
issues(method).
shouldSend({
name: type,
parameters: {
id: id
}
}).
serverResponds(response).
callbackReceives(key);
});
}
device = new DeviceInteraction(exampleCmds, function() {
return subject;
});
setup(function() {
driver = new MockDriver();
client = new Client(driver);
subject = new Element(id, client);
});
suite('initialization', function() {
test('should set id', function() {
assert.strictEqual(subject.id, id);
});
test('should set client', function() {
assert.strictEqual(subject.client, client);
});
});
suite('._sendCommand', function() {
device.
issues('_sendCommand', {name: 'test'}).
shouldSend({name: 'test', parameters: {id: id}}).
serverResponds('value').
callbackReceives('value');
});
suite('.findElement', function() {
device.
issues('findElement', '#id').
shouldSend({
name: 'findElement',
parameters: {
value: '#id',
element: id,
using: 'css selector'
}
}).
serverResponds('findElementResponse');
test('should send callback a single element', function() {
var value = device.commandCallback.value;
var resultId = exampleCmds.findElementResponse().value;
assert.instanceOf(value, Element);
assert.strictEqual(value.id, resultId);
});
});
suite('.findElements', function() {
device.
issues('findElements', '#id').
shouldSend({
name: 'findElements',
parameters: {
value: '#id',
element: id,
using: 'css selector'
}
}).
serverResponds('findElementsResponse');
test('should send callback an element instance', function() {
var map = device.commandCallback.value.map(function(el) {
return el.id;
});
assert.deepEqual(map, exampleCmds.findElementsResponse().value);
});
});
suite('.scriptWith', function() {
var calledWith,
fn = function() {},
cb = function() {};
setup(function() {
subject.client.executeScript = function() {
calledWith = arguments;
};
});
test('should call client.executeScript with ' +
'element as argument', function() {
subject.scriptWith(fn);
assert.strictEqual(calledWith[0], fn);
assert.deepEqual(calledWith[1], [
subject
]);
assert.strictEqual(calledWith[2], undefined);
});
test('should call client.executeScript with element ' +
' + callback', function(){
subject.scriptWith(fn, cb);
assert.strictEqual(calledWith[0], fn);
assert.deepEqual(calledWith[1], [
subject
]);
assert.strictEqual(calledWith[2], cb);
});
test('should call client.executeScript with element ' +
' + args + callback', function(){
subject.scriptWith(fn, ['foo', 'bar'], cb);
assert.strictEqual(calledWith[0], fn);
assert.deepEqual(calledWith[1], [
subject, 'foo', 'bar'
]);
assert.strictEqual(calledWith[2], cb);
});
});
suite('.equals', function() {
var equals;
var notEquals;
setup(function() {
equals = new Element(id, client);
notEquals = new Element('___I_AM_TITAN', client);
});
test('equals', function() {
assert.isTrue(subject.equals(equals));
});
test('not equal', function() {
assert.isFalse(subject.equals(notEquals));
});
});
suite('.getAttribute', function() {
var attr = 'name';
device.
issues('getAttribute', attr).
shouldSend({
name: 'getElementAttribute',
parameters: {
name: attr,
id: id
}
}).
serverResponds('value').
callbackReceives('value');
});
suite('.sendKeys', function() {
suite('when given a array', function() {
var input = ['f', 'o', 'o'];
device.
issues('sendKeys', input).
shouldSend({
name: 'sendKeysToElement',
parameters: {
value: input,
id: id
}
}).
serverResponds('ok').
callbackReceives();
});
suite('when given a string', function() {
var msg = 'foo';
device.
issues('sendKeys', msg).
shouldSend({
name: 'sendKeysToElement',
parameters: {
value: [msg],
id: id
}
}).
serverResponds('ok').
callbackReceives();
});
});
suite('.cssProperty', function() {
var property = 'margin';
device.
issues('cssProperty', property).
shouldSend({
name: 'getElementValueOfCssProperty',
parameters: {
propertyName: property,
id: id
}
}).
serverResponds('value').
callbackReceives('value');
});
suite('.tap', function() {
suite('when given the x and y offsets', function() {
var x = 10;
var y = 15;
device.
issues('tap', x, y).
shouldSend({
name: 'singleTap',
parameters: {
x: x,
y: y,
id: id
}
}).
serverResponds('value').
callbackReceives('value');
});
suite('when given the x offset only', function() {
var x = 10;
device.
issues('tap', x).
shouldSend({
name: 'singleTap',
parameters: {
x: x,
id: id
}
}).
serverResponds('value').
callbackReceives('value');
});
suite('when no x and y offsets', function() {
simpleCommand('tap', 'singleTap', 'value');
});
});
simpleCommand('tagName', 'getElementTagName', 'value', 'value');
simpleCommand('click', 'clickElement', 'ok');
simpleCommand('text', 'getElementText', 'value', 'value');
simpleCommand('clear', 'clearElement', 'ok');
simpleCommand('selected', 'isElementSelected', 'value', 'value');
simpleCommand('enabled', 'isElementEnabled', 'value', 'value');
simpleCommand('displayed', 'isElementDisplayed', 'value', 'value');
simpleCommand('size', 'getElementRect', 'value', 'value');
simpleCommand('location', 'getElementRect', 'value', 'value');
simpleCommand('rect', 'getElementRect', 'value', 'value');
});

View file

@ -0,0 +1,80 @@
/* global assert, helper */
'use strict';
suite('marionette/error', function() {
var MarionetteError;
helper.require('error', function(obj) {
MarionetteError = obj;
});
test('should expose .ERRORS', function() {
assert.property(MarionetteError, 'ERRORS');
});
suite('#error', function() {
test('should populate message', function() {
var err = new MarionetteError({}, {message: 'msg'});
assert.property(err, 'message');
assert.include(err.message, 'msg');
});
test('should populate stacktrace', function() {
var trace = 'brie';
var err = new MarionetteError({}, {stacktrace: trace});
assert.property(err, 'stack');
assert.include(err.message, trace);
assert.include(err.stack, trace);
});
test('should populate type', function() {
var err = new MarionetteError({}, {});
assert.property(err, 'type');
});
test('should recognise known error code', function() {
var err = new MarionetteError({}, {error: 'no such element'});
assert.property(err, 'type');
assert.equal(err.type, 'NoSuchElement');
});
test('should fall back to GenericError for unknown error code', function() {
var err = new MarionetteError({}, {error: 'brunost'});
assert.property(err, 'type');
assert.equal(err.type, 'GenericError');
});
test('should support all errors', function() {
for (var s in MarionetteError.ERRORS) {
var err = new MarionetteError({}, {error: s});
assert.strictEqual(err.type, MarionetteError.ERRORS[s]);
assert.include(err.message, MarionetteError.ERRORS[s]);
assert.instanceOf(err, Error);
}
});
test('should contain client', function() {
var client = {};
var result = new MarionetteError(client, {
status: 7777,
message: 'foo',
stack: 'bar'
});
assert.equal(result.client, client);
assert.strictEqual(
result.name,
MarionetteError.ERRORS['webdriver error']
);
});
test('should use GenericError error when unknown error code', function() {
var result = new MarionetteError({}, {error: 'cheese'});
assert.strictEqual(
result.name,
MarionetteError.ERRORS['webdriver error']
);
});
});
});

View file

@ -0,0 +1,32 @@
/* global assert, helper */
'use strict';
suite('marionette/index', function() {
var Index;
suiteSetup(function() {
Index = helper.requireLib('marionette/index');
});
test('should have paths', function() {
assert.instanceOf(Index.Element, Object);
assert.instanceOf(Index.Client, Object);
assert.instanceOf(Index.Drivers, Object);
assert.instanceOf(Index.CommandStream, Object);
assert.instanceOf(Index.Drivers.Abstract, Object);
if (typeof(window) === 'undefined') {
assert.instanceOf(Index.Drivers.Tcp, Object);
} else {
try {
if (typeof(window.navigator.mozTCPSocket) !== 'undefined') {
assert.instanceOf(Index.Drivers.MozTcp, Object);
}
} catch(e) {
}
}
});
});

View file

@ -0,0 +1,57 @@
/* global DeviceInteraction, MockDriver, assert, exampleCmds, helper */
'use strict';
suite('marionette/multi-actions', function() {
var driver, subject, actions, client, device,
MultiActions, Actions, Client;
helper.require('multi-actions', function(obj) {
MultiActions = obj;
});
helper.require('actions', function(obj) {
Actions = obj;
});
helper.require('client', function(obj) {
Client = obj;
});
device = new DeviceInteraction(exampleCmds, function() {
return subject;
});
setup(function() {
driver = new MockDriver();
client = new Client(driver);
actions = new Actions(client);
subject = new MultiActions(client);
});
suite('initialization', function() {
test('should set client', function() {
assert.strictEqual(subject.client, client);
});
});
suite('.add', function() {
setup(function() {
subject.add(actions);
});
test('should have a action chain', function() {
assert.deepEqual(subject.multiActions[0], actions.actionChain);
});
});
suite('.perform', function() {
device.
issues('perform').
shouldSend({
type: 'multiAction',
value: [],
max_length: 0
}).
serverResponds('ok').
callbackReceives();
});
});

View file

@ -0,0 +1,98 @@
/* global assert */
'use strict';
var FakeSocket = require('../support/socket'),
CommandStream = require('../../lib/marionette/command-stream'),
ConnectionManger = require('../../lib/node/connection-manager');
suite('connection-manager', function() {
var RealSocket, subject, sockets = [];
suiteSetup(function() {
RealSocket = ConnectionManger.Socket;
ConnectionManger.Socket = FakeSocket;
FakeSocket.sockets = sockets;
});
suiteTeardown(function() {
ConnectionManger.Socket = RealSocket;
});
setup(function() {
sockets.length = 0;
subject = new ConnectionManger();
});
suite('initialization', function() {
test('should initialize .connections', function() {
assert.deepEqual(subject.connections, {});
});
});
suite('.remove', function() {
test('should remove connection from list', function() {
subject.open();
assert.ok(subject.connections[0]);
subject.close(0);
assert.notOk(subject.connections[0]);
});
});
suite('.get', function() {
test('should return an open connection', function() {
var open = subject.open();
assert.strictEqual(subject.get(open.id), open.connection);
});
});
suite('.open', function() {
var result, con;
setup(function() {
result = subject.open();
con = subject.connections[0];
});
test('should initialize socket and stream', function() {
assert.ok(sockets[0]);
assert.strictEqual(con.socket, sockets[0]);
assert.strictEqual(sockets[0].port, subject.defaultPort);
assert.instanceOf(con, CommandStream);
});
test('should increment currentId', function() {
assert.strictEqual(subject.currentId, 1);
});
test('should return id and stream', function() {
assert.deepEqual(result, {
id: 0,
connection: subject.connections[0]
});
});
function isRemovedOnEvent(event) {
suite('on socket ' + event, function() {
setup(function() {
assert.ok(subject.connections[0]);
con.socket.emit(event);
});
test('should remove itself from connections', function() {
assert.notOk(subject.connections[0]);
});
});
}
isRemovedOnEvent('close');
isRemovedOnEvent('end');
});
});

View file

@ -0,0 +1,17 @@
'use strict';
var EventEmitter = require('events').EventEmitter;
var WSClient = function() {
this.sendCalls = [];
EventEmitter.call(this);
};
WSClient.prototype = Object.create(EventEmitter.prototype);
WSClient.prototype.onsend = function() {};
WSClient.prototype.send = function() {
this.onsend.apply(this, arguments);
this.sendCalls.push(Array.prototype.slice.call(arguments));
};
module.exports = exports = WSClient;

View file

@ -0,0 +1,121 @@
/* global Marionette, assert */
(function(module, ns) {
'use strict';
function DeviceInteraction(exampleCmds, subject) {
var commands = exampleCmds;
var expectedResponse;
setup(function() {
expectedResponse = null;
});
function driver() {
if (subject().client) {
return subject().client.driver;
}
return subject().driver;
}
return {
commandCallback: function commandCallback(error, value) {
commandCallback.error = error;
commandCallback.value = value;
},
withProtocol: function(version) {
setup(function() {
subject().protocol = version;
});
return this;
},
issues: function issues() {
var args = Array.prototype.slice.call(arguments),
cmd = args.shift(),
result,
commandCallback = this.commandCallback;
setup(function() {
if (!subject()) {
throw new Error('DeviceInteraction must be provided a subject');
}
args.push(commandCallback);
if (!(cmd in subject())) {
throw new Error('client does not have method ' + cmd);
}
result = subject()[cmd].apply(subject(), args);
});
test('should be chainable', function() {
assert.strictEqual(result, subject());
});
return this;
},
shouldSend: function shouldSend(options) {
var check = function(option, value) {
test('should send ' + option, function() {
var sent = driver().sent[0];
if (!(option in sent)) {
throw new Error(
option + ' was never sent as part of command to server');
}
assert.deepEqual(sent[option], value);
});
};
for (var key in options) {
if (options.hasOwnProperty(key)) {
check(key, options[key]);
}
}
return this;
},
serverResponds: function serverResponds(type, options) {
setup(function() {
if (!(type in commands)) {
throw new Error('No such example command: ' + type);
}
expectedResponse = commands[type](options);
driver().respond(expectedResponse);
});
return this;
},
callbackReceives: function callbackReceives(key) {
var commandCallback = this.commandCallback;
if (key) {
test('should receive ' + key + ' in response', function() {
if (typeof expectedResponse[key] == 'undefined') {
throw new Error(key + ' should not be undefined ' +
'for test mocks use a real value');
}
assert.strictEqual(commandCallback.value, expectedResponse[key]);
});
} else {
test('should receive whole body response', function() {
assert.strictEqual(commandCallback.value, expectedResponse);
});
}
return this;
}
};
}
module.exports = DeviceInteraction;
}.apply(
this,
(this.Marionette) ?
[Marionette('support/device-interaction'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,45 @@
/* global Marionette */
(function(module) {
'use strict';
function FakeXhr() {
this.openArgs = null;
this.sendArgs = null;
this.headers = {};
this.responseHeaders = {};
}
FakeXhr.prototype = {
open: function() {
this.openArgs = arguments;
},
getResponseHeader: function(key) {
return this.responseHeaders[key];
},
setRequestHeader: function(key, value) {
this.headers[key] = value;
},
send: function() {
this.sendArgs = arguments;
},
respond: function(data, code) {
this.readyState = 4;
this.responseHeaders['content-type'] = 'application/json';
this.responseText = JSON.stringify(data);
this.status = code || 200;
this.onreadystatechange();
}
};
module.exports = FakeXhr;
}.apply(
this,
(this.Marionette) ?
[Marionette('support/fake-xhr'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,43 @@
/* global Marionette */
(function(module) {
'use strict';
function MockDriver() {
this.sent = [];
this.queue = [];
this.timeout = 0;
}
MockDriver.prototype = {
connectionId: 0,
setScriptTimeout: function(timeout) {
this.timeout = timeout;
},
reset: function() {
this.sent.length = 0;
this.queue.length = 0;
},
send: function(cmd, cb) {
this.sent.push(cmd);
this.queue.push(cb);
},
respond: function(cmd) {
if (this.queue.length) {
(this.queue.shift())(cmd);
}
}
};
module.exports = MockDriver;
}.apply(
this,
(this.Marionette) ?
[Marionette('support/mock-driver'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,45 @@
/* global Marionette */
/**
@namespace
*/
(function(module, ns) {
'use strict';
var Responder = ns.require('responder');
function FakeSocket() {
Responder.call(this);
FakeSocket.sockets.push(this);
this.destroyed = false;
if(arguments.length === 2) {
this.host = arguments[0];
this.port = arguments[1];
}
}
FakeSocket.sockets = [];
FakeSocket.prototype = Object.create(Responder.prototype);
FakeSocket.prototype.connect = function(port, host, cb) {
this.port = port;
this.host = host;
cb && cb();
};
FakeSocket.prototype.destroy = function() {
this.destroyed = true;
};
FakeSocket.prototype.close = FakeSocket.prototype.destroy;
module.exports = exports = FakeSocket;
}.apply(
this,
(this.Marionette) ?
[Marionette('support/socket'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,12 @@
{
"name": "Marionette JS Client",
"description": "JavaScript API/Client for Marionette (https://developer.mozilla.org/en-US/docs/Marionette)",
"version": "1.7.1",
"url": "https://github.com/mozilla-b2g/marionette-js-client",
"logo": "none",
"options": {
"paths": "lib/marionette/",
"themedir": "docs/theme",
"linkNatives": true
}
}