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:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
19
gaia/tests/jsmarionette/.jshintrc
Executable file
19
gaia/tests/jsmarionette/.jshintrc
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../.jshintrc",
|
||||
"curly": false,
|
||||
"evil": true,
|
||||
"node": true,
|
||||
"validthis": true,
|
||||
|
||||
"globals": {
|
||||
"Promise": true
|
||||
},
|
||||
|
||||
"predef": [
|
||||
"suite",
|
||||
"suiteSetup",
|
||||
"suiteTeardown",
|
||||
"teardown",
|
||||
"test"
|
||||
]
|
||||
}
|
||||
3
gaia/tests/jsmarionette/capabilities.json
Executable file
3
gaia/tests/jsmarionette/capabilities.json
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"raisesAccessibilityExceptions": true
|
||||
}
|
||||
135
gaia/tests/jsmarionette/client/json-wire-protocol/lib/index.js
Executable file
135
gaia/tests/jsmarionette/client/json-wire-protocol/lib/index.js
Executable file
|
|
@ -0,0 +1,135 @@
|
|||
'use strict';
|
||||
var Transform = require('stream').Transform;
|
||||
|
||||
var SEPARATOR = ':';
|
||||
var SEPARATOR_CODE = SEPARATOR.charCodeAt(0);
|
||||
|
||||
/**
|
||||
* First ocurrence of where string occurs in a buffer.
|
||||
*
|
||||
* NOTE: this is not UTF8 safe generally we expect to find the correct
|
||||
* char fairly quickly unless the buffer is incorrectly formatted.
|
||||
*
|
||||
* @param {Buffer} buffer haystack.
|
||||
* @param {String} code needle.
|
||||
* @return {Numeric} -1 if not found index otherwise.
|
||||
*/
|
||||
function indexInBuffer(buffer, code) {
|
||||
if (buffer.length === 0)
|
||||
return -1;
|
||||
|
||||
var index = 0;
|
||||
var length = buffer.length;
|
||||
|
||||
do {
|
||||
if (buffer[index] === code) return index;
|
||||
} while (
|
||||
++index && index + 1 < length
|
||||
);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* converts an object to a string representation suitable for storage on disk.
|
||||
* Its very important to note that the length in the string refers to the utf8
|
||||
* size of the json content in bytes (as utf8) not the JS string length.
|
||||
*
|
||||
* @param {Object} object to stringify.
|
||||
* @return {String} serialized object.
|
||||
*/
|
||||
function stringify(object) {
|
||||
var json = JSON.stringify(object);
|
||||
var len = Buffer.byteLength(json);
|
||||
|
||||
return len + SEPARATOR + json;
|
||||
}
|
||||
|
||||
/**
|
||||
* attempts to parse a given buffer or string.
|
||||
*
|
||||
* @param {Uint8Array|Buffer} input in byteLength:{json..} format.
|
||||
* @return {Objec} JS object.
|
||||
*/
|
||||
function parse(input) {
|
||||
var stream = new Stream();
|
||||
var result;
|
||||
|
||||
stream.once('data', function(data) {
|
||||
result = data;
|
||||
});
|
||||
|
||||
stream.write(input);
|
||||
|
||||
if (!result) {
|
||||
throw new Error(
|
||||
'no command available from parsing:' + input
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function Stream() {
|
||||
this._pendingLength = null;
|
||||
this._buffer = new Buffer(0);
|
||||
|
||||
Transform.call(this, { objectMode: true });
|
||||
}
|
||||
|
||||
Stream.prototype = {
|
||||
__proto__: Transform.prototype,
|
||||
|
||||
_transform: function(chunk, encoding, cb) {
|
||||
if (!this._pendingLength) {
|
||||
var idx = indexInBuffer(chunk, SEPARATOR_CODE);
|
||||
|
||||
// Nothing to do just buffer it...
|
||||
if (idx === -1) {
|
||||
this._buffer = Buffer.concat([this._buffer, chunk]);
|
||||
return cb();
|
||||
}
|
||||
|
||||
// number of bytes in the json segment...
|
||||
var length = Buffer.concat([this._buffer, chunk.slice(0, idx)]);
|
||||
this._pendingLength = parseInt(length.toString(), 10);
|
||||
this._buffer = new Buffer(0);
|
||||
|
||||
// We have transitioned to a pending length state so do another pass.
|
||||
return this._transform(chunk.slice(idx + 1), encoding, cb);
|
||||
}
|
||||
|
||||
// Total length too small nothing to do...
|
||||
if (this._buffer.length + chunk.length < this._pendingLength) {
|
||||
this._buffer = Buffer.concat([this._buffer, chunk]);
|
||||
return cb();
|
||||
}
|
||||
|
||||
var buffer = Buffer.concat([this._buffer, chunk]);
|
||||
var remainder = null;
|
||||
|
||||
if (buffer.length > this._pendingLength) {
|
||||
remainder = buffer.slice(this._pendingLength);
|
||||
buffer = buffer.slice(0, this._pendingLength);
|
||||
}
|
||||
|
||||
// Reset internal state and push current json string.
|
||||
this._pendingLength = null;
|
||||
this._buffer = new Buffer(0);
|
||||
this.push(JSON.parse(buffer.toString()));
|
||||
|
||||
// If we have any remaining data we need to keep processing...
|
||||
if (remainder) {
|
||||
return this._transform(remainder, encoding, cb);
|
||||
}
|
||||
|
||||
// Otherwise we are done yay....
|
||||
return cb();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
exports.parse = parse;
|
||||
exports.stringify = stringify;
|
||||
exports.Stream = Stream;
|
||||
exports.separator = SEPARATOR;
|
||||
7
gaia/tests/jsmarionette/client/json-wire-protocol/package.json
Executable file
7
gaia/tests/jsmarionette/client/json-wire-protocol/package.json
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "json-wire-protocol",
|
||||
"version": "1.0.0",
|
||||
"author": "James Lal",
|
||||
"main": "lib/index.js",
|
||||
"description": "JSON wire protocol parser/writer (used in mozilla debugger protocol)"
|
||||
}
|
||||
139
gaia/tests/jsmarionette/client/json-wire-protocol/test/index-test.js
Executable file
139
gaia/tests/jsmarionette/client/json-wire-protocol/test/index-test.js
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
var jsonWireProtocol = require('../');
|
||||
|
||||
suite('json wire protocol', function() {
|
||||
var subject;
|
||||
var TWO_BYTE = 'ž';
|
||||
|
||||
function createBytes(content) {
|
||||
return new Buffer(content);
|
||||
}
|
||||
|
||||
test('.separator', function() {
|
||||
assert.ok(jsonWireProtocol.separator);
|
||||
});
|
||||
|
||||
suite('#stringify', function() {
|
||||
test('ASCII only', function() {
|
||||
var input = { a: 'abcdefg' };
|
||||
var expected = JSON.stringify(input);
|
||||
|
||||
expected = expected.length + ':' + expected;
|
||||
var output = jsonWireProtocol.stringify(input);
|
||||
|
||||
assert.deepEqual(expected, output);
|
||||
});
|
||||
|
||||
test('invalid strings', function() {
|
||||
var invalid = 'xfoobar!';
|
||||
|
||||
assert.throws(function() {
|
||||
jsonWireProtocol.parse(invalid);
|
||||
});
|
||||
});
|
||||
|
||||
test('with multibyte chars', function() {
|
||||
var input = { a: TWO_BYTE + TWO_BYTE + TWO_BYTE };
|
||||
var expected = JSON.stringify(input);
|
||||
|
||||
expected = '14:' + expected;
|
||||
assert.deepEqual(jsonWireProtocol.stringify(input), expected);
|
||||
});
|
||||
});
|
||||
|
||||
suite('#parse', function() {
|
||||
test('working string', function() {
|
||||
var input = { woot: TWO_BYTE };
|
||||
var string = jsonWireProtocol.stringify(input);
|
||||
assert.deepEqual(
|
||||
input,
|
||||
jsonWireProtocol.parse(createBytes(string))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('.Stream', function() {
|
||||
setup(function() {
|
||||
subject = new jsonWireProtocol.Stream();
|
||||
});
|
||||
|
||||
suite('#write', function() {
|
||||
|
||||
suite('multiple commands over two buffers', function() {
|
||||
var commandA = { a: 'cool' };
|
||||
var commandB = { b: 'woot' };
|
||||
var commandC = { c: 'wtfman' };
|
||||
var commandD = { d: TWO_BYTE };
|
||||
|
||||
var all = [
|
||||
commandA,
|
||||
commandB,
|
||||
commandC
|
||||
]
|
||||
.map(jsonWireProtocol.stringify)
|
||||
.join('');
|
||||
var half = all.length / 2;
|
||||
|
||||
var bufferA = createBytes(all.slice(0, half));
|
||||
var bufferB = createBytes(all.slice(half) +
|
||||
jsonWireProtocol.stringify(commandD));
|
||||
|
||||
var parsed;
|
||||
|
||||
setup(function() {
|
||||
parsed = [];
|
||||
|
||||
subject.on('data', function(result) {
|
||||
parsed.push(result);
|
||||
});
|
||||
|
||||
subject.write(bufferA);
|
||||
subject.write(bufferB);
|
||||
});
|
||||
|
||||
test('result after writing to stream', function() {
|
||||
assert.deepEqual(
|
||||
parsed,
|
||||
[commandA, commandB, commandC, commandD]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('multiple chunks until length', function(done) {
|
||||
var expected = { longer: TWO_BYTE + 'a' + TWO_BYTE };
|
||||
var string = jsonWireProtocol.stringify(expected);
|
||||
|
||||
subject.on('data', function(result) {
|
||||
assert.deepEqual(result, expected);
|
||||
done();
|
||||
});
|
||||
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
subject.write(createBytes(string[i]));
|
||||
}
|
||||
});
|
||||
|
||||
suite('entire buffer', function() {
|
||||
var buffer;
|
||||
/* 13 ascii bytes */
|
||||
var string = '{"one":"foo"}';
|
||||
|
||||
setup(function() {
|
||||
buffer = createBytes('13:' + string);
|
||||
});
|
||||
|
||||
test('read entire buffer', function(done) {
|
||||
var expected = { one: 'foo' };
|
||||
subject.once('data', function(data) {
|
||||
assert.deepEqual(data, expected);
|
||||
done();
|
||||
});
|
||||
|
||||
subject.write(buffer);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
264
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/actions.js
Executable file
264
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/actions.js
Executable 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')]
|
||||
));
|
||||
1656
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/client.js
Executable file
1656
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/client.js
Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -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')]
|
||||
));
|
||||
|
|
@ -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')]
|
||||
));
|
||||
|
|
@ -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')]
|
||||
));
|
||||
|
|
@ -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;
|
||||
106
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/drivers/tcp.js
Executable file
106
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/drivers/tcp.js
Executable 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;
|
||||
361
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/element.js
Executable file
361
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/element.js
Executable 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')]
|
||||
));
|
||||
131
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/error.js
Executable file
131
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/error.js
Executable 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')]
|
||||
));
|
||||
|
|
@ -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')]
|
||||
));
|
||||
20
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/index.js
Executable file
20
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/index.js
Executable 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')]
|
||||
));
|
||||
101
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/marionette.js
Executable file
101
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/marionette.js
Executable 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
|
||||
));
|
||||
67
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/message.js
Executable file
67
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/message.js
Executable 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,
|
||||
};
|
||||
|
|
@ -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')]
|
||||
));
|
||||
212
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/responder.js
Executable file
212
gaia/tests/jsmarionette/client/marionette-client/lib/marionette/responder.js
Executable 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')]
|
||||
));
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
20
gaia/tests/jsmarionette/client/marionette-client/package.json
Executable file
20
gaia/tests/jsmarionette/client/marionette-client/package.json
Executable 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"
|
||||
}
|
||||
}
|
||||
103
gaia/tests/jsmarionette/client/marionette-client/test/helper.js
Executable file
103
gaia/tests/jsmarionette/client/marionette-client/test/helper.js
Executable 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;
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
18
gaia/tests/jsmarionette/client/marionette-client/test/integration/helper.js
Executable file
18
gaia/tests/jsmarionette/client/marionette-client/test/integration/helper.js
Executable 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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
--profile-base profile.js
|
||||
225
gaia/tests/jsmarionette/client/marionette-client/test/marionette/actions-test.js
Executable file
225
gaia/tests/jsmarionette/client/marionette-client/test/marionette/actions-test.js
Executable 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');
|
||||
});
|
||||
});
|
||||
1552
gaia/tests/jsmarionette/client/marionette-client/test/marionette/client-test.js
Executable file
1552
gaia/tests/jsmarionette/client/marionette-client/test/marionette/client-test.js
Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
283
gaia/tests/jsmarionette/client/marionette-client/test/marionette/element-test.js
Executable file
283
gaia/tests/jsmarionette/client/marionette-client/test/marionette/element-test.js
Executable 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');
|
||||
|
||||
});
|
||||
|
|
@ -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']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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')]
|
||||
));
|
||||
45
gaia/tests/jsmarionette/client/marionette-client/test/support/fake-xhr.js
Executable file
45
gaia/tests/jsmarionette/client/marionette-client/test/support/fake-xhr.js
Executable 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')]
|
||||
));
|
||||
43
gaia/tests/jsmarionette/client/marionette-client/test/support/mock-driver.js
Executable file
43
gaia/tests/jsmarionette/client/marionette-client/test/support/mock-driver.js
Executable 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')]
|
||||
));
|
||||
45
gaia/tests/jsmarionette/client/marionette-client/test/support/socket.js
Executable file
45
gaia/tests/jsmarionette/client/marionette-client/test/support/socket.js
Executable 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')]
|
||||
));
|
||||
12
gaia/tests/jsmarionette/client/marionette-client/yuidoc.json
Executable file
12
gaia/tests/jsmarionette/client/marionette-client/yuidoc.json
Executable 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
|
||||
}
|
||||
}
|
||||
63
gaia/tests/jsmarionette/client/socket-retry-connect/index.js
Executable file
63
gaia/tests/jsmarionette/client/socket-retry-connect/index.js
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
'use strict';
|
||||
var debug = require('debug')('socket-retry-connect'),
|
||||
net = require('net');
|
||||
|
||||
var MAX_TRIES = 10;
|
||||
var WAIT_BETWEEN_TRIES_MS = 250;
|
||||
|
||||
/**
|
||||
* Tries to connect to a given socket location.
|
||||
* Time between retires grows in relation to attempts (attempt * RETRY_TIMER).
|
||||
*
|
||||
* waitForSocket({ port: 2828, maxTries: 10 }, function(err, socket) {
|
||||
* });
|
||||
*
|
||||
* Note- there is a third argument used to recursion that should
|
||||
* never be used publicly.
|
||||
*
|
||||
* Options:
|
||||
* - (Number) port: to connect to.
|
||||
* - (String) host: to connect to.
|
||||
* - (Number) tries: number of times to attempt the connect.
|
||||
*
|
||||
* @param {Object} options for connection.
|
||||
* @param {Function} callback [err, socket].
|
||||
*/
|
||||
function waitForSocket(options, callback, _tries) {
|
||||
if (!options.port)
|
||||
throw new Error('.port is a required option');
|
||||
|
||||
var maxTries = options.tries || MAX_TRIES;
|
||||
var host = options.host || 'localhost';
|
||||
var port = options.port;
|
||||
|
||||
|
||||
debug('attempt to open socket', port, host);
|
||||
_tries = _tries || 0;
|
||||
if (_tries >= maxTries)
|
||||
return callback(new Error('cannot open socket'));
|
||||
|
||||
function handleError() {
|
||||
debug('socket is not ready trying');
|
||||
// retry connection
|
||||
setTimeout(
|
||||
waitForSocket,
|
||||
// wait at least WAIT_BETWEEN_TRIES_MS or a multiplier
|
||||
// of the attempts.
|
||||
(WAIT_BETWEEN_TRIES_MS * _tries) || WAIT_BETWEEN_TRIES_MS,
|
||||
options,
|
||||
callback,
|
||||
++_tries
|
||||
);
|
||||
}
|
||||
|
||||
var socket = net.connect(port, host, function(one, two) {
|
||||
debug('connected', port, host);
|
||||
socket.removeListener('error', handleError);
|
||||
callback(null, socket);
|
||||
});
|
||||
socket.once('error', handleError);
|
||||
}
|
||||
|
||||
module.exports.waitForSocket = waitForSocket;
|
||||
|
||||
11
gaia/tests/jsmarionette/client/socket-retry-connect/package.json
Executable file
11
gaia/tests/jsmarionette/client/socket-retry-connect/package.json
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "socket-retry-connect",
|
||||
"version": "0.0.1",
|
||||
"author": "James Lal [:lightsofapollo]",
|
||||
"description": "Attempt to connect to socket multiple times",
|
||||
"main": "index.js",
|
||||
|
||||
"dependencies" : {
|
||||
"debug": "*"
|
||||
}
|
||||
}
|
||||
57
gaia/tests/jsmarionette/client/socket-retry-connect/test.js
Executable file
57
gaia/tests/jsmarionette/client/socket-retry-connect/test.js
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
'use strict';
|
||||
suite('socket retry connect', function() {
|
||||
var PORT = 60012;
|
||||
var assert = require('assert'),
|
||||
subject = require('./index').waitForSocket,
|
||||
net = require('net');
|
||||
|
||||
var _server;
|
||||
function createServer(onConnect) {
|
||||
onConnect = onConnect || function() {};
|
||||
// saves reference to last server
|
||||
return (_server = net.createServer(onConnect).listen(PORT));
|
||||
}
|
||||
|
||||
teardown(function() {
|
||||
// make sure we actually close the thing
|
||||
try {
|
||||
_server.close();
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
function returnsSocket(callback) {
|
||||
callback = callback || function() {};
|
||||
return function(err, socket) {
|
||||
assert.ok(!err, err && err.message);
|
||||
assert.ok(socket instanceof net.Socket);
|
||||
callback();
|
||||
};
|
||||
}
|
||||
|
||||
test('socket is open', function(done) {
|
||||
var pending = 2;
|
||||
function next() {
|
||||
if (--pending === 0) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
// verify that both the server gets hit and socket callback fires.
|
||||
createServer(next);
|
||||
subject({ port: PORT }, returnsSocket(next));
|
||||
});
|
||||
|
||||
test('waits before connection is open', function(done) {
|
||||
this.timeout('5s');
|
||||
setTimeout(createServer, 575);
|
||||
subject({ port: PORT }, returnsSocket(done));
|
||||
});
|
||||
|
||||
test('fails to connect after retires', function(done) {
|
||||
this.timeout('3s');
|
||||
subject({ port: PORT, tries: 2 }, function(err) {
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/socket/));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
15
gaia/tests/jsmarionette/client/sockit-to-me/binding.gyp
Executable file
15
gaia/tests/jsmarionette/client/sockit-to-me/binding.gyp
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
'variables': {
|
||||
'gpp': '<!(type g++ | grep "is" > /dev/null && echo "yep" || echo "no")'
|
||||
},
|
||||
'targets': [{
|
||||
'target_name': 'sockit',
|
||||
'sources': ['src/addon.c',
|
||||
'src/sockit.cc'],
|
||||
'conditions': [[
|
||||
'"<@(gpp)"=="no"',
|
||||
{ 'sources!': ['src/addon.c',
|
||||
'src/sockit.cc'] }
|
||||
]]
|
||||
}]
|
||||
}
|
||||
25
gaia/tests/jsmarionette/client/sockit-to-me/example.js
Executable file
25
gaia/tests/jsmarionette/client/sockit-to-me/example.js
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
'use strict';
|
||||
// Import the add-on.
|
||||
var sockittome = require('./');
|
||||
|
||||
// Create a new sockit object.
|
||||
var sockit = new sockittome.Sockit();
|
||||
|
||||
// Connect to a host and port.
|
||||
sockit.connect({ host: 'www.google.com', port: 80 });
|
||||
|
||||
// Write some data! Takes either a String or a node.js 'Buffer' object.
|
||||
sockit.write('GET / HTTP/1.0\r\n\r\n');
|
||||
//sockit.write(new Buffer('GET / HTTP/1.0\r\n\r\n'));
|
||||
|
||||
// Read some tasty bytes! Returns a node.js 'Buffer' object, always.
|
||||
var buffer = sockit.read(1024);
|
||||
|
||||
// Observe amazing results.
|
||||
console.log('Read ', buffer.length, ' bytes');
|
||||
|
||||
// And beautiful raw HTTP response.
|
||||
console.log(buffer.toString());
|
||||
|
||||
// Close it up.
|
||||
sockit.close();
|
||||
22
gaia/tests/jsmarionette/client/sockit-to-me/package.json
Executable file
22
gaia/tests/jsmarionette/client/sockit-to-me/package.json
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "sockit-to-me",
|
||||
"version": "1.0.2",
|
||||
"author": "Ghislain 'Aus' Lacroix <aus@mozilla.com>",
|
||||
"description": "A synchronous socket API for node.js.",
|
||||
"main": "build/Release/sockit",
|
||||
"repository": "https://github.com/mozilla-b2g/gaia/tree/master/tests/jsmarionette/client/sockit-to-me",
|
||||
"license": "MIT",
|
||||
|
||||
"engines": {
|
||||
"node": "^4 || ^5"
|
||||
},
|
||||
|
||||
"devDependencies": {
|
||||
"node-gyp": "^3.0.1"
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"install": "CC=clang CXX=clang++ node-gyp configure build --verbose"
|
||||
}
|
||||
}
|
||||
|
||||
15
gaia/tests/jsmarionette/client/sockit-to-me/src/addon.cc
Executable file
15
gaia/tests/jsmarionette/client/sockit-to-me/src/addon.cc
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
// Global includes
|
||||
#include <node.h>
|
||||
|
||||
// Local includes
|
||||
#include "sockit.h"
|
||||
|
||||
// So we don't spend half the time typing 'v8::'
|
||||
using namespace v8;
|
||||
|
||||
// Initialize
|
||||
void InitAll(Handle<Object> aExports) {
|
||||
Sockit::Init(aExports);
|
||||
}
|
||||
|
||||
NODE_MODULE(sockit, InitAll)
|
||||
629
gaia/tests/jsmarionette/client/sockit-to-me/src/sockit.cc
Executable file
629
gaia/tests/jsmarionette/client/sockit-to-me/src/sockit.cc
Executable file
|
|
@ -0,0 +1,629 @@
|
|||
// Global includes
|
||||
#include <node.h>
|
||||
#include <node_buffer.h>
|
||||
|
||||
// Platform dependent includes.
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
|
||||
// Local includes
|
||||
#include "sockit.h"
|
||||
|
||||
// So we don't spend half the time typing 'v8::'
|
||||
using namespace v8;
|
||||
|
||||
// Error Messages
|
||||
#define E_ALREADY_CONNECTING \
|
||||
"ALREADY CONNECTING! You must call close before calling connect again."
|
||||
#define E_ALREADY_CONNECTED \
|
||||
"ALREADY CONNECTED! You must call close before calling connect again."
|
||||
|
||||
// Clever constants
|
||||
static const int MAX_LOOKUP_RETRIES = 3;
|
||||
static const int POLL_TIMEOUT_MS = 60000;
|
||||
|
||||
// Global for debug log enable/disable at runtime.
|
||||
static bool gDebugLog = false;
|
||||
|
||||
Persistent<Function> Sockit::constructor;
|
||||
|
||||
static void _debug_log(const char *aMsg) {
|
||||
if(gDebugLog) {
|
||||
fprintf(stderr, "[sockit-to-me] %s\n", aMsg);
|
||||
}
|
||||
}
|
||||
|
||||
Sockit::Sockit() : mSocket(0),
|
||||
mPollTimeout(POLL_TIMEOUT_MS),
|
||||
mIsConnecting(false),
|
||||
mIsConnected(false) {
|
||||
|
||||
}
|
||||
|
||||
Sockit::~Sockit() {
|
||||
// Close our socket if it's still open.
|
||||
if(mSocket != 0) {
|
||||
// XXXAus: We should probably report error on close.
|
||||
close(mSocket);
|
||||
}
|
||||
}
|
||||
|
||||
/*static*/ void
|
||||
Sockit::Init(Handle<Object> aExports) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
|
||||
Local<FunctionTemplate> object = FunctionTemplate::New(isolate, New);
|
||||
// Set the classname our object will have in JavaScript land.
|
||||
object->SetClassName(String::NewFromUtf8(isolate, "Sockit"));
|
||||
|
||||
// Reserve space for our object instance.
|
||||
object->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
// Add the 'Connect' function.
|
||||
NODE_SET_PROTOTYPE_METHOD(object, "connect", Connect);
|
||||
// Add the 'Read' function.
|
||||
NODE_SET_PROTOTYPE_METHOD(object, "read", Read);
|
||||
// Add the 'Write' function.
|
||||
NODE_SET_PROTOTYPE_METHOD(object, "write", Write);
|
||||
// Add the 'Close' function.
|
||||
NODE_SET_PROTOTYPE_METHOD(object, "close", Close);
|
||||
// Add the 'SetPollTimeout' function.
|
||||
NODE_SET_PROTOTYPE_METHOD(object, "setPollTimeout", SetPollTimeout);
|
||||
// Add the 'SetDebugLog' function.
|
||||
NODE_SET_PROTOTYPE_METHOD(object, "setDebugLog", SetDebugLog);
|
||||
|
||||
// Add the constructor.
|
||||
constructor.Reset(isolate, object->GetFunction());
|
||||
aExports->Set(String::NewFromUtf8(isolate, "Sockit"),
|
||||
object->GetFunction());
|
||||
}
|
||||
|
||||
/*static*/ void
|
||||
Sockit::New(const FunctionCallbackInfo<Value>& aArgs) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
|
||||
if (aArgs.IsConstructCall()) {
|
||||
Sockit* sockit = new Sockit();
|
||||
sockit->Wrap(aArgs.This());
|
||||
aArgs.GetReturnValue().Set(aArgs.This());
|
||||
}
|
||||
else {
|
||||
const int argc = 0;
|
||||
Local<Value> argv[argc] = { };
|
||||
Local<Function> cons = Local<Function>::New(isolate, constructor);
|
||||
aArgs.GetReturnValue().Set(cons->NewInstance(argc, argv));
|
||||
}
|
||||
}
|
||||
|
||||
/*static*/ void
|
||||
Sockit::Connect(const FunctionCallbackInfo<Value>& aArgs) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
|
||||
// Always return self.
|
||||
aArgs.GetReturnValue().Set(aArgs.This());
|
||||
|
||||
if(aArgs.Length() < 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Not enough arguments.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(aArgs.Length() > 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Too many arguments.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!aArgs[0]->IsObject()) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(
|
||||
String::NewFromUtf8(isolate,
|
||||
"Invalid argument. Must be an Object.")));
|
||||
return;
|
||||
}
|
||||
|
||||
Local<Object> options = aArgs[0]->ToObject();
|
||||
|
||||
Local<String> hostKey = String::NewFromUtf8(isolate, "host");
|
||||
Local<String> portKey = String::NewFromUtf8(isolate, "port");
|
||||
|
||||
if(!options->Has(hostKey) ||
|
||||
!options->Get(hostKey)->IsString()) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(
|
||||
String::NewFromUtf8(
|
||||
isolate,
|
||||
"Object must have 'host' field and it must be a string.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!options->Has(portKey) ||
|
||||
!options->Get(portKey)->IsNumber()) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(
|
||||
String::NewFromUtf8(
|
||||
isolate,
|
||||
"Object must have 'port' field and it must be a number.")));
|
||||
return;
|
||||
}
|
||||
|
||||
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
|
||||
|
||||
// Ensure we're not already attempting to connect.
|
||||
if(sockit->mIsConnecting) {
|
||||
_debug_log(E_ALREADY_CONNECTING);
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate, E_ALREADY_CONNECTING)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure we're not already connected.
|
||||
if(sockit->mIsConnected) {
|
||||
_debug_log(E_ALREADY_CONNECTED);
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate, E_ALREADY_CONNECTED)));
|
||||
return;
|
||||
}
|
||||
|
||||
// We can consider ourselves in an attempt to connect at this point.
|
||||
sockit->mIsConnecting = true;
|
||||
|
||||
// XXXAus: Move the DNS piece into it's own method.
|
||||
|
||||
String::Utf8Value optionsHost(options->Get(hostKey)->ToString());
|
||||
|
||||
bool shouldRetry = false;
|
||||
int retryCount = 0;
|
||||
struct hostent *hostEntry;
|
||||
|
||||
do {
|
||||
hostEntry = gethostbyname(*optionsHost);
|
||||
|
||||
// Getting the host address failed. Let's see why.
|
||||
if(hostEntry == NULL) {
|
||||
// Check h_errno, we might retry if the error indicates we should.
|
||||
switch(h_errno) {
|
||||
case HOST_NOT_FOUND:
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate,
|
||||
"Couldn't resolve host.")));
|
||||
return;
|
||||
break;
|
||||
|
||||
case NO_RECOVERY:
|
||||
isolate->ThrowException(
|
||||
Exception::Error(
|
||||
String::NewFromUtf8(isolate,
|
||||
"Unexpected failure during host lookup.")));
|
||||
return;
|
||||
break;
|
||||
|
||||
case NO_DATA:
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate,
|
||||
"Host exists but has no address.")));
|
||||
return;
|
||||
break;
|
||||
|
||||
case TRY_AGAIN:
|
||||
shouldRetry = true;
|
||||
retryCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while(shouldRetry && retryCount <= MAX_LOOKUP_RETRIES);
|
||||
|
||||
if(hostEntry == NULL) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate,
|
||||
"Couldn't resolve host even after retries.")));
|
||||
return;
|
||||
}
|
||||
|
||||
int socketHandle = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if(socketHandle < 0) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate, "Failed to create socket.")));
|
||||
return;
|
||||
}
|
||||
|
||||
sockit->mSocket = socketHandle;
|
||||
|
||||
// Set socket non-blocking prior to connect. We'll poll for connect success.
|
||||
int flags = fcntl(socketHandle, F_GETFL, 0);
|
||||
if(flags == -1) {
|
||||
flags = 0;
|
||||
}
|
||||
int result = fcntl(socketHandle, F_SETFL, flags | O_NONBLOCK);
|
||||
if(result < 0) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(
|
||||
String::NewFromUtf8(isolate, "Failed to set non blocking to socket.")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Magic socket incantation.
|
||||
struct sockaddr_in address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
// Always use first address returned, it's good enough for our purposes.
|
||||
address.sin_addr.s_addr =
|
||||
inet_addr(inet_ntoa(*(struct in_addr *)hostEntry->h_addr_list[0]));
|
||||
// Using static cast here to enforce short as required by IPv4 functions.
|
||||
address.sin_port =
|
||||
htons(static_cast<short>(options->Get(portKey)->ToNumber()->Uint32Value()));
|
||||
|
||||
result = connect(
|
||||
sockit->mSocket, (sockaddr *) &address, sizeof(struct sockaddr)
|
||||
);
|
||||
|
||||
if(result == -1 && errno != EINPROGRESS) {
|
||||
// We failed to connect. Close to socket we created.
|
||||
close(sockit->mSocket);
|
||||
// In case there's another connect attempt, clean up the socket data.
|
||||
sockit->mSocket = 0;
|
||||
sockit->mIsConnecting = false;
|
||||
// Report failure to connect.
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate, "Failed to connect.")));
|
||||
return;
|
||||
}
|
||||
|
||||
struct pollfd fds;
|
||||
fds.fd = sockit->mSocket;
|
||||
fds.events = POLLWRBAND | POLLWRNORM | POLLNVAL;
|
||||
fds.revents = 0;
|
||||
|
||||
result = poll(&fds, 1, sockit->mPollTimeout);
|
||||
|
||||
// If we've failed, we'll clean-up and try and send an exception.
|
||||
if(result != 1) {
|
||||
// Close and clean-up.
|
||||
close(sockit->mSocket);
|
||||
sockit->mSocket = 0;
|
||||
sockit->mIsConnecting = false;
|
||||
sockit->mIsConnected = false;
|
||||
|
||||
if(result == 0) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(
|
||||
String::NewFromUtf8(isolate, "Polling socket connect() timeout!")));
|
||||
}
|
||||
else if(result < 0 || (fds.revents & POLLERR)) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(
|
||||
String::NewFromUtf8(isolate, "Error polling connect() socket!")));
|
||||
}
|
||||
else if(!((fds.revents & POLLWRBAND) || (fds.revents & POLLWRNORM)) ) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(
|
||||
String::NewFromUtf8(isolate, "Unhandled poll event connect() socket!")));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, we're really connected.
|
||||
sockit->mIsConnected = true;
|
||||
// And we're no longer attempting to connect.
|
||||
sockit->mIsConnecting = false;
|
||||
}
|
||||
|
||||
/*static*/ void
|
||||
Sockit::Read(const FunctionCallbackInfo<Value>& aArgs) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
|
||||
// Default to returning undefined
|
||||
aArgs.GetReturnValue().Set(Undefined(isolate));
|
||||
|
||||
if(aArgs.Length() != 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(
|
||||
String::NewFromUtf8(
|
||||
isolate,
|
||||
"Not enough arguments, read takes the number of bytes to be read from the socket.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!aArgs[0]->IsNumber()) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Argument must be a Number.")));
|
||||
return;
|
||||
}
|
||||
|
||||
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
|
||||
|
||||
// Make sure we're connected to something.
|
||||
if(sockit->mSocket == 0) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(
|
||||
String::NewFromUtf8(
|
||||
isolate,
|
||||
"Not connected. To read data you must call connect first.")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Figure out how many bytes the user wants us to read from the socket.
|
||||
unsigned int bytesToRead = aArgs[0]->ToNumber()->Uint32Value();
|
||||
// Allocate a shiny buffer.
|
||||
char *buffer = new char[bytesToRead];
|
||||
// Track how many bytes we've read and total bytes read.
|
||||
int totalBytesRead = 0;
|
||||
int bytesRead = 0;
|
||||
// Read me some tasty bytes!
|
||||
struct pollfd fds;
|
||||
fds.fd = sockit->mSocket;
|
||||
fds.events = POLLIN;
|
||||
|
||||
do {
|
||||
fds.revents = 0;
|
||||
|
||||
int result = poll(&fds, 1, sockit->mPollTimeout);
|
||||
if(result == 0) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate,
|
||||
"Polling socket recv() timeout!")));
|
||||
break;
|
||||
}
|
||||
else if(result < 0 || (fds.revents & POLLERR)) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate,
|
||||
"Error polling recv() socket!")));
|
||||
break;
|
||||
}
|
||||
else if(!(fds.revents & POLLIN)) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate,
|
||||
"Unhandled poll event recv() socket!")));
|
||||
break;
|
||||
}
|
||||
bytesRead = recv(sockit->mSocket, &buffer[totalBytesRead], bytesToRead, 0);
|
||||
|
||||
if(bytesRead < 0) {
|
||||
|
||||
if(errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
isolate->ThrowException(
|
||||
Exception::Error(
|
||||
String::NewFromUtf8(isolate,
|
||||
"Error while reading from socket!")));
|
||||
break;
|
||||
}
|
||||
if(bytesRead == 0) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate,
|
||||
"Connection reset by peer!")));
|
||||
break;
|
||||
}
|
||||
|
||||
bytesToRead -= bytesRead;
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
bytesRead = 0;
|
||||
}
|
||||
while(bytesToRead > 0);
|
||||
|
||||
if(totalBytesRead) {
|
||||
// Create a node heap allocated buffer first.
|
||||
Local<Object> heapBuffer = node::Buffer::New(isolate, totalBytesRead).ToLocalChecked();
|
||||
|
||||
// Copy our temporary buffer data into the heap buffer.
|
||||
memcpy(node::Buffer::Data(heapBuffer), buffer, totalBytesRead);
|
||||
// Done with our temporary buffer.
|
||||
delete [] buffer;
|
||||
// Return those tasty bytes!
|
||||
aArgs.GetReturnValue().Set(heapBuffer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Done with our temporary buffer.
|
||||
delete [] buffer;
|
||||
|
||||
// Return NULL!
|
||||
aArgs.GetReturnValue().Set(Null(isolate));
|
||||
return;
|
||||
}
|
||||
|
||||
/*static*/ void
|
||||
Sockit::Write(const FunctionCallbackInfo<Value>& aArgs) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
|
||||
if(aArgs.Length() < 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Not enough arguments.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(aArgs.Length() > 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(
|
||||
String::NewFromUtf8(isolate,
|
||||
"Too many arguments.")));
|
||||
return;
|
||||
}
|
||||
|
||||
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
|
||||
|
||||
// Always return self even when we throw.
|
||||
aArgs.GetReturnValue().Set(aArgs.This());
|
||||
|
||||
// Make sure we're connected to something.
|
||||
if(sockit->mSocket == 0) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(
|
||||
String::NewFromUtf8(
|
||||
isolate,
|
||||
"Not connected. To write data you must call connect first.")));
|
||||
return;
|
||||
}
|
||||
|
||||
int success = 0;
|
||||
|
||||
if(aArgs[0]->IsString()) {
|
||||
// Convert to utf8 value before sending.
|
||||
Local<String> dataString = aArgs[0]->ToString();
|
||||
String::Utf8Value data(dataString);
|
||||
// Write the data to the socket.
|
||||
success = sockit->Write(*data, dataString->Utf8Length());
|
||||
}
|
||||
else if(aArgs[0]->IsObject() &&
|
||||
aArgs[0]->ToObject()->GetConstructorName()->Equals(String::NewFromUtf8(isolate, "Buffer"))) {
|
||||
Local<Object> buffer = aArgs[0]->ToObject();
|
||||
success =
|
||||
sockit->Write(node::Buffer::Data(buffer), node::Buffer::Length(buffer));
|
||||
}
|
||||
else {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(
|
||||
String::NewFromUtf8(isolate,
|
||||
"Invalid argument, must be a 'String' or 'Buffer' object.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(success < 0) {
|
||||
isolate->ThrowException(
|
||||
Exception::Error(String::NewFromUtf8(isolate,
|
||||
"Unable to write data to socket.")));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int
|
||||
Sockit::Write(const char *aBuffer, const int aLength) {
|
||||
|
||||
int bytesToWrite = aLength;
|
||||
int totalBytesWritten = 0;
|
||||
int bytesWritten = 0;
|
||||
|
||||
do {
|
||||
bytesWritten =
|
||||
send(mSocket,
|
||||
aBuffer + (totalBytesWritten * sizeof(char)),
|
||||
bytesToWrite,
|
||||
0
|
||||
);
|
||||
|
||||
if(bytesWritten < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
totalBytesWritten += bytesWritten;
|
||||
bytesToWrite -= bytesWritten;
|
||||
|
||||
bytesWritten = 0;
|
||||
}
|
||||
while(bytesToWrite > 0);
|
||||
|
||||
return totalBytesWritten;
|
||||
}
|
||||
|
||||
/*static*/ void
|
||||
Sockit::Close(const FunctionCallbackInfo<Value>& aArgs) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
|
||||
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
|
||||
if(sockit->mSocket != 0) {
|
||||
// If close fails we have bigger problems on our hands than a stale
|
||||
// socket that isn't closed.
|
||||
close(sockit->mSocket);
|
||||
}
|
||||
|
||||
sockit->mSocket = 0;
|
||||
sockit->mIsConnecting = false;
|
||||
sockit->mIsConnected = false;
|
||||
|
||||
aArgs.GetReturnValue().Set(aArgs.This());
|
||||
return;
|
||||
}
|
||||
|
||||
/*static*/ void
|
||||
Sockit::SetPollTimeout(const FunctionCallbackInfo<Value>& aArgs) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
|
||||
if(aArgs.Length() < 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Not enough arguments.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(aArgs.Length() > 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Too many arguments.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!aArgs[0]->IsNumber()) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Argument is not a number.")));
|
||||
return;
|
||||
}
|
||||
|
||||
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
|
||||
sockit->mPollTimeout = aArgs[0]->Int32Value();
|
||||
|
||||
aArgs.GetReturnValue().Set(aArgs.This());
|
||||
return;
|
||||
}
|
||||
|
||||
/*static*/ void
|
||||
Sockit::SetDebugLog(const FunctionCallbackInfo<Value>& aArgs) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
|
||||
if(aArgs.Length() < 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Not enough arguments.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(aArgs.Length() > 1) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Too many arguments.")));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!aArgs[0]->IsBoolean()) {
|
||||
isolate->ThrowException(
|
||||
Exception::TypeError(String::NewFromUtf8(isolate,
|
||||
"Argument is not a boolean.")));
|
||||
return;
|
||||
}
|
||||
|
||||
gDebugLog = aArgs[0]->BooleanValue();
|
||||
|
||||
if(gDebugLog) {
|
||||
_debug_log("debug log enabled");
|
||||
}
|
||||
|
||||
aArgs.GetReturnValue().Set(aArgs.This());
|
||||
return;
|
||||
}
|
||||
39
gaia/tests/jsmarionette/client/sockit-to-me/src/sockit.h
Executable file
39
gaia/tests/jsmarionette/client/sockit-to-me/src/sockit.h
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#ifndef __SOCKIT_H__
|
||||
#define __SOCKIT_H__
|
||||
|
||||
#include <node.h>
|
||||
#include <node_object_wrap.h>
|
||||
|
||||
class Sockit : public node::ObjectWrap {
|
||||
public:
|
||||
static void Init(v8::Handle<v8::Object>);
|
||||
|
||||
private:
|
||||
Sockit();
|
||||
~Sockit();
|
||||
|
||||
static v8::Persistent<v8::Function> constructor;
|
||||
|
||||
static void New(const v8::FunctionCallbackInfo<v8::Value>&);
|
||||
|
||||
static void Connect(const v8::FunctionCallbackInfo<v8::Value>&);
|
||||
|
||||
static void Read(const v8::FunctionCallbackInfo<v8::Value>&);
|
||||
|
||||
static void Write(const v8::FunctionCallbackInfo<v8::Value>&);
|
||||
|
||||
int Write(const char *, const int);
|
||||
|
||||
static void Close(const v8::FunctionCallbackInfo<v8::Value>&);
|
||||
|
||||
static void SetPollTimeout(const v8::FunctionCallbackInfo<v8::Value>&);
|
||||
static void SetDebugLog(const v8::FunctionCallbackInfo<v8::Value>&);
|
||||
|
||||
int mSocket;
|
||||
int mPollTimeout;
|
||||
|
||||
bool mIsConnecting;
|
||||
bool mIsConnected;
|
||||
};
|
||||
|
||||
#endif
|
||||
380
gaia/tests/jsmarionette/client/sockit-to-me/test/sockit_test.js
Executable file
380
gaia/tests/jsmarionette/client/sockit-to-me/test/sockit_test.js
Executable file
|
|
@ -0,0 +1,380 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
var ChildProcess = require('child_process');
|
||||
|
||||
// Always use debug binary during testing.
|
||||
var Sockit = require('..');
|
||||
|
||||
suite('sockit-to-me', function() {
|
||||
// Firing up a process can be slow on Travis CI so we allow a longer
|
||||
// timeout for our tests.
|
||||
this.timeout(10000);
|
||||
|
||||
var host = '127.0.0.1';
|
||||
var port = 44321;
|
||||
var helo = 'HELO\r\n';
|
||||
var ackn = 'ACK\r\n';
|
||||
|
||||
var server = null;
|
||||
var subject = null;
|
||||
// global flag set to true if we kill the server
|
||||
// needed for the close / disconnect
|
||||
var server_killed = false;
|
||||
|
||||
function startServer(callback) {
|
||||
server_killed = false;
|
||||
// Start the child process.
|
||||
server = ChildProcess.fork(__dirname + '/../test_support/server.js');
|
||||
// Register listener
|
||||
server.on('message', function(message) {
|
||||
// Server has actually started listening on the expected host and port.
|
||||
if(message.reply == 'started') {
|
||||
// Setup complete.
|
||||
callback();
|
||||
}
|
||||
});
|
||||
// Ask server to start.
|
||||
server.send({ command: 'start' });
|
||||
}
|
||||
|
||||
function stopServer(callback) {
|
||||
// Register listener to shutdown child process once the server has
|
||||
// successfully closed it's listening socket.
|
||||
server.on('message', function(message) {
|
||||
// Server has actually stopped.
|
||||
if(message.reply == 'stopped') {
|
||||
// Indicate server was killed.
|
||||
server_killed = true;
|
||||
// Disconnect child process.
|
||||
server.disconnect();
|
||||
// Teardown complete.
|
||||
callback();
|
||||
}
|
||||
});
|
||||
// Ask server to stop.
|
||||
server.send({ command: 'stop' });
|
||||
}
|
||||
|
||||
setup(function(done) {
|
||||
subject = new Sockit.Sockit();
|
||||
startServer(done);
|
||||
});
|
||||
|
||||
teardown(function(done) {
|
||||
// Close connection to server.
|
||||
subject.close();
|
||||
if (!server_killed) {
|
||||
stopServer(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
suite('#connect', function() {
|
||||
|
||||
test('successful connections', function(done) {
|
||||
// Register a listener to ensure that we really did connect to the server
|
||||
// as we expected to.
|
||||
server.on('message', function(message) {
|
||||
if(message.reply == 'connected') {
|
||||
done();
|
||||
}
|
||||
});
|
||||
// Connect throws on error and is synchronous.
|
||||
subject.connect({ host: host, port: port });
|
||||
});
|
||||
|
||||
suite('argument shape errors', function() {
|
||||
test('not enough arguments', function() {
|
||||
var err;
|
||||
|
||||
try {
|
||||
assert.ok(subject.connect() instanceof Error);
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('argument of wrong type', function() {
|
||||
var err;
|
||||
|
||||
try {
|
||||
assert.ok(subject.connect(23) instanceof Error);
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('argument without string "host" attribute', function() {
|
||||
var err;
|
||||
|
||||
try {
|
||||
subject.connect({ port: 23 });
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('argument without number "port" attribute', function() {
|
||||
var err;
|
||||
|
||||
try {
|
||||
subject.connect({ host: 'host' });
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
});
|
||||
|
||||
test('error is thrown when connection cannot be established',
|
||||
function() {
|
||||
var err;
|
||||
|
||||
// Connect throws on error and is synchronous.
|
||||
try {
|
||||
subject.connect({ host: host + 'garbage', port: port });
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('connection will timeout when expected when using setPollTimeout',
|
||||
function() {
|
||||
|
||||
subject.setPollTimeout(1000);
|
||||
|
||||
var err;
|
||||
var start = Date.now();
|
||||
try {
|
||||
subject.connect({ host: '1.2.3.4', port: 31337 });
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
assert.ok(Date.now() - start >= 1000);
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('calling connect twice will fail', function() {
|
||||
var err;
|
||||
try {
|
||||
subject.connect({ host: host, port: port });
|
||||
subject.connect({ host: host, port: port });
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
assert.ok(err.message.indexOf('ALREADY CONNECTED') != -1);
|
||||
});
|
||||
});
|
||||
|
||||
suite('#read', function() {
|
||||
|
||||
test('correct data is returned', function(done) {
|
||||
// Register a listener to ensure that the server really is ready to write
|
||||
// data when we ask it to.
|
||||
server.on('message', function(message) {
|
||||
// Connected, ask server to send data.
|
||||
if(message.reply == 'connected') {
|
||||
// Ask server to send 'helo'.
|
||||
server.send({ command: 'send', data: helo });
|
||||
|
||||
// Read the response.
|
||||
var response = subject.read(helo.length);
|
||||
// Ensure we got the response we expected.
|
||||
assert.equal(helo, response.toString());
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// Connect to server.
|
||||
subject.connect({ host: host, port: port });
|
||||
});
|
||||
|
||||
test('error is thrown when not connected', function() {
|
||||
var err;
|
||||
|
||||
try {
|
||||
subject.read(1);
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('error is thrown when a number is not specified', function() {
|
||||
var err;
|
||||
subject.connect({ host: host, port: port });
|
||||
|
||||
try {
|
||||
subject.read();
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('1s timeout if no data', function(done) {
|
||||
subject.setPollTimeout(1000);
|
||||
|
||||
// Register a listener to ensure that the server really is ready
|
||||
server.on('message', function(message) {
|
||||
// Connected, ask server to send NOTHING.
|
||||
if(message.reply == 'connected') {
|
||||
|
||||
var err;
|
||||
|
||||
try {
|
||||
// Read the response.
|
||||
subject.read(helo.length);
|
||||
}
|
||||
catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// Connect to server.
|
||||
subject.connect({ host: host, port: port });
|
||||
});
|
||||
|
||||
test('socket closing', function(done) {
|
||||
|
||||
subject.setPollTimeout(1000);
|
||||
// Register a listener to ensure that the server really is ready
|
||||
server.on('message', function(message) {
|
||||
// Connected, ask server to send NOTHING.
|
||||
if(message.reply == 'connected') {
|
||||
|
||||
var err;
|
||||
server.kill('SIGKILL');
|
||||
server_killed = true;
|
||||
|
||||
try {
|
||||
// Read the response.
|
||||
subject.read(helo.length);
|
||||
}
|
||||
catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// Connect to server.
|
||||
subject.connect({ host: host, port: port });
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
suite('#write', function() {
|
||||
|
||||
test('specified data is written', function(done) {
|
||||
// Register a litener to ensure that the server is really ready to read
|
||||
// data when we ask it to and to verify that it received what we sent.
|
||||
server.on('message', function(message) {
|
||||
if(message.reply == 'connected') {
|
||||
// Tell the server to expect data from the client.
|
||||
server.send({ command: 'recv', data: ackn });
|
||||
// Send data to server.
|
||||
subject.write(ackn);
|
||||
}
|
||||
else if(message.reply == 'expected') {
|
||||
done();
|
||||
}
|
||||
});
|
||||
// Connect to server.
|
||||
subject.connect({ host: host, port: port });
|
||||
});
|
||||
|
||||
test('error is thrown when not connected', function() {
|
||||
var err;
|
||||
|
||||
try {
|
||||
subject.write('data');
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('error is thrown when no argument is specified', function() {
|
||||
var err;
|
||||
subject.connect({ host: host, port: port });
|
||||
|
||||
try {
|
||||
subject.write();
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('error is thrown when invalid argument is specified', function() {
|
||||
var err;
|
||||
subject.connect({ host: host, port: port });
|
||||
|
||||
try {
|
||||
subject.write({});
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('#setDebugLog', function() {
|
||||
|
||||
test('succeeds', function() {
|
||||
subject.setDebugLog(true);
|
||||
});
|
||||
|
||||
test('error is thrown with invalid argument count', function() {
|
||||
var err;
|
||||
try {
|
||||
subject.setDebugLog();
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('error is thrown with bad argument type', function() {
|
||||
var err;
|
||||
try {
|
||||
subject.setDebugLog('a string is not the right type');
|
||||
} catch(e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
65
gaia/tests/jsmarionette/client/sockit-to-me/test_support/server.js
Executable file
65
gaia/tests/jsmarionette/client/sockit-to-me/test_support/server.js
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
'use strict';
|
||||
var net = require('net');
|
||||
|
||||
var server = {
|
||||
_port: 44321,
|
||||
_host: '127.0.0.1',
|
||||
_server: null,
|
||||
|
||||
_client: null,
|
||||
_accumulator: null,
|
||||
|
||||
start: function() {
|
||||
this._server = net.createServer(this.connection.bind(this));
|
||||
this._server.listen(this._port, this._host, function() {
|
||||
process.send({ reply: 'started' });
|
||||
});
|
||||
},
|
||||
|
||||
connection: function(socket) {
|
||||
this._client = socket;
|
||||
this._accumulator = '';
|
||||
|
||||
process.send({ reply: 'connected' });
|
||||
},
|
||||
|
||||
send: function(message) {
|
||||
this._client.write(message);
|
||||
},
|
||||
|
||||
recv: function(expected) {
|
||||
this._client.setEncoding('utf8');
|
||||
this._client.on('data', function(buffer) {
|
||||
this._accumulator += buffer;
|
||||
if(this._accumulator == expected) {
|
||||
process.send({ reply: 'expected' });
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
this._server.close(function() {
|
||||
process.send({ reply: 'stopped' });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
process.on('message', function(message) {
|
||||
switch(message.command) {
|
||||
case 'start':
|
||||
server.start();
|
||||
break;
|
||||
|
||||
case 'send':
|
||||
server.send(message.data);
|
||||
break;
|
||||
|
||||
case 'recv':
|
||||
server.recv(message.data);
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
server.stop();
|
||||
break;
|
||||
}
|
||||
});
|
||||
70
gaia/tests/jsmarionette/client/sockit-to-me/tools/copy.js
Executable file
70
gaia/tests/jsmarionette/client/sockit-to-me/tools/copy.js
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
/**
|
||||
* Checks which sockit-to-me binary we need. This will be null unless
|
||||
*
|
||||
* 1. Platform is linux
|
||||
* 2. gcc not installed
|
||||
* 3. Linux version is ubuntu 12.04
|
||||
*
|
||||
* @param {Function} callback will be invoked with null, 'i386', or 'amd64'.
|
||||
*/
|
||||
function getRequiredVersion(callback) {
|
||||
if (process.platform !== 'linux') {
|
||||
console.log('[sockit-to-me] platform not linux');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
exec('type g++', function(err, stdout) {
|
||||
if (stdout.indexOf('g++ is') !== -1) {
|
||||
console.log('[sockit-to-me] found g++');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
exec('lsb_release -a', function(err, stdout) {
|
||||
if (stdout.indexOf('precise') === -1) {
|
||||
console.log('[sockit-to-me] distribution not precise');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
// Choose between 32 and 64 bit versions.
|
||||
exec('uname -a', function(err, stdout) {
|
||||
var result;
|
||||
if (stdout.indexOf('i386') !== -1) {
|
||||
result = 'i386';
|
||||
} else if (stdout.indexOf('x86_64') !== -1) {
|
||||
result = 'amd64';
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
|
||||
return callback(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
getRequiredVersion(function(version) {
|
||||
if (version === null) {
|
||||
// We can build on the fly.
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Copy an existing binary into build/Release.
|
||||
console.log('[sockit-to-me] will use existing binary');
|
||||
console.log('[sockit-to-me] ' + version);
|
||||
exec('mkdir -p build', function() {
|
||||
var source = 'bin/ubuntu-12.04.3-' + version;
|
||||
var dest = 'build/Release';
|
||||
var cp = ['cp', '-r', source, dest].join(' ');
|
||||
exec(cp, function() {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
28
gaia/tests/jsmarionette/client/sockit-to-me/tools/info.js
Executable file
28
gaia/tests/jsmarionette/client/sockit-to-me/tools/info.js
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
function main() {
|
||||
var info = {
|
||||
__dirname: __dirname,
|
||||
__filename: __filename,
|
||||
process: {
|
||||
arch: process.arch,
|
||||
config: process.config,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
execArgv: process.execArgv,
|
||||
platform: process.platform
|
||||
}
|
||||
};
|
||||
|
||||
exec('whoami', function(err, stdout, stderr) {
|
||||
info.whoami = stdout.replace(/\s+/g, '');
|
||||
console.log('[sockit-to-me] ' + JSON.stringify(info, null, 4));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
19
gaia/tests/jsmarionette/client/sockit-to-me/tools/prebuild.sh
Executable file
19
gaia/tests/jsmarionette/client/sockit-to-me/tools/prebuild.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
# curl -L https://raw.github.com/mozilla-b2g/sockit-to-me/master/tools/prebuild.sh | bash -s stable
|
||||
# This script prebuilds nodejs and sockit-to-me on ubuntu since we can't do
|
||||
# it on the fly on the build machines (they don't have gcc).
|
||||
UBUNTU=/home/ubuntu
|
||||
|
||||
sudo apt-get -y update --fix-missing
|
||||
sudo apt-get -y install g++ gcc git make
|
||||
cd $UBUNTU && git clone https://github.com/joyent/node
|
||||
cd $UBUNTU && git clone https://github.com/mozilla-b2g/sockit-to-me
|
||||
|
||||
# Build nodejs.
|
||||
cd $UBUNTU/node && git checkout v0.10.21 && ./configure && make && sudo make install
|
||||
|
||||
# Install node-gyp.
|
||||
sudo npm install -g node-gyp
|
||||
|
||||
# Build sockit-to-me.
|
||||
cd $UBUNTU/sockit-to-me && node-gyp configure build
|
||||
58
gaia/tests/jsmarionette/helper.js
Executable file
58
gaia/tests/jsmarionette/helper.js
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
'use strict';
|
||||
/* global module, require */
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
var Helper = {
|
||||
|
||||
unlockScreen: function(client) {
|
||||
// wait that the lockscreen is ready
|
||||
client.waitFor(function () {
|
||||
return client.executeScript(function () {
|
||||
if (!window || !window.wrappedJSObject.lockScreenWindowManager) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var wrappedObject = window.wrappedJSObject;
|
||||
var lockScreen = wrappedObject.lockScreen || wrappedObject.LockScreen;
|
||||
|
||||
return typeof lockScreen.unlock === 'function';
|
||||
});
|
||||
});
|
||||
client.executeScript(
|
||||
fs.readFileSync('./tests/atoms/gaia_lock_screen.js') +
|
||||
'GaiaLockScreen.unlock();\n');
|
||||
},
|
||||
|
||||
lockScreen: function(client) {
|
||||
// wait that the lockscreen is ready
|
||||
client.waitFor(function () {
|
||||
return client.executeScript(function () {
|
||||
if (!window || !window.wrappedJSObject.lockScreenWindowManager) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var wrappedObject = window.wrappedJSObject;
|
||||
var lockScreen = wrappedObject.lockScreen || wrappedObject.LockScreen;
|
||||
|
||||
return typeof lockScreen.lock === 'function';
|
||||
});
|
||||
});
|
||||
client.executeScript(
|
||||
fs.readFileSync('./tests/atoms/gaia_lock_screen.js') +
|
||||
'GaiaLockScreen.lock();\n');
|
||||
},
|
||||
|
||||
delay: function(client, interval, givenCallback) {
|
||||
var start = Date.now();
|
||||
client.waitFor(function(callback) {
|
||||
if (Date.now() - start >= interval) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(null, null);
|
||||
}
|
||||
}, null, givenCallback);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Helper;
|
||||
10
gaia/tests/jsmarionette/marionette_tests.txt
Executable file
10
gaia/tests/jsmarionette/marionette_tests.txt
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
*/client/marionette-client/test/integration/*
|
||||
*/client/marionette-client/test/integration/drivers/*
|
||||
*/plugins/marionette-apps/test/*_test.js
|
||||
*/plugins/marionette-content-script/test/*_test.js
|
||||
*/plugins/marionette-file-manager/test/integration/*
|
||||
*/plugins/marionette-helper/test/*_test.js
|
||||
*/plugins/marionette-js-logger/test/*_test.js
|
||||
*/plugins/marionette-plugin-forms/test/integration/tests/*
|
||||
*/plugins/marionette-settings-api/test/*_test.js
|
||||
*/runner/marionette-js-runner/test/integration/*
|
||||
125
gaia/tests/jsmarionette/mocha/mocha-json-proxy/consumer.js
Executable file
125
gaia/tests/jsmarionette/mocha/mocha-json-proxy/consumer.js
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
'use strict';
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var MESSAGE_PREFIX = 'mocha-proxy';
|
||||
|
||||
// runnable events which need a runnable wrapper
|
||||
var RUNNABLE_EVENTS = [
|
||||
'pending',
|
||||
'pass',
|
||||
'fail',
|
||||
'test',
|
||||
'test end',
|
||||
'suite',
|
||||
'suite end'
|
||||
];
|
||||
|
||||
function Runnable(event) {
|
||||
for (var key in event) {
|
||||
this[key] = event[key];
|
||||
}
|
||||
}
|
||||
|
||||
Runnable.prototype = {
|
||||
slow: function() {
|
||||
return this._slow;
|
||||
},
|
||||
|
||||
fullTitle: function() {
|
||||
var title = '';
|
||||
if (this.parent) {
|
||||
title = this.parent.fullTitle();
|
||||
if (title) {
|
||||
title += ' ';
|
||||
}
|
||||
}
|
||||
title += this.title;
|
||||
return title;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Consumer for a mocha runner process using the mocha-json-proxy reporter.
|
||||
*
|
||||
*
|
||||
* var consumer = new Consumer(
|
||||
* forkedMochaProcess // should use _mocha NOT mocha (which spawns another
|
||||
* process)
|
||||
* );
|
||||
*
|
||||
* // consumer is also an event emitter which emits standard mocha events.
|
||||
* consumer.on('start', function() {
|
||||
* });
|
||||
*
|
||||
* consumer.on('pass', function() {
|
||||
* });
|
||||
*
|
||||
* @param {ChildProcess} child to listen to messages on.
|
||||
*/
|
||||
function Consumer(child) {
|
||||
EventEmitter.call(this);
|
||||
|
||||
this._suiteStack = [];
|
||||
this._objects = {};
|
||||
|
||||
child.on('message', function(content) {
|
||||
if (Array.isArray(content) && content[0] === MESSAGE_PREFIX) {
|
||||
this.transformEvent(content[1]);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Consumer.prototype = {
|
||||
__proto__: EventEmitter.prototype,
|
||||
|
||||
referenceObject: function(event, object) {
|
||||
// objects without the _id property don't need to be a single reference.
|
||||
if (!object || !object._id) {
|
||||
return object;
|
||||
}
|
||||
// find the original reference or create it.
|
||||
var ref = this._objects[object._id];
|
||||
|
||||
if (!ref) {
|
||||
// convert the object into an instanceof Runnable if its event is a suite
|
||||
// or test.
|
||||
if (RUNNABLE_EVENTS.indexOf(event) !== -1) {
|
||||
object = new Runnable(object);
|
||||
}
|
||||
// when no reference is found this is the correct object.
|
||||
ref = this._objects[object._id] = object;
|
||||
} else {
|
||||
// otherwise we need to update the reference
|
||||
Object.keys(object).forEach(function(key) {
|
||||
var value = object[key];
|
||||
ref[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
// Re-create the parent based on the referenced ID.
|
||||
if (ref._parentId) {
|
||||
ref.parent = this.referenceObject('suite', { _id: ref._parentId });
|
||||
delete ref._parentId;
|
||||
}
|
||||
|
||||
return ref;
|
||||
},
|
||||
|
||||
transformEvent: function(event) {
|
||||
var name = event[0];
|
||||
var runnable = event[1] = this.referenceObject(name, event[1]);
|
||||
|
||||
if (name === 'suite') {
|
||||
this._suiteStack.push(runnable);
|
||||
// add the root .suite
|
||||
if (!this.suite) {
|
||||
this.suite = runnable;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit.apply(this, event);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Consumer;
|
||||
15
gaia/tests/jsmarionette/mocha/mocha-json-proxy/package.json
Executable file
15
gaia/tests/jsmarionette/mocha/mocha-json-proxy/package.json
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "mocha-json-proxy",
|
||||
"version": "0.0.7",
|
||||
"author": "James Lal [:lightsofapollo]",
|
||||
"description": "Mocha JSON proxy reporter",
|
||||
"main": "consumer",
|
||||
|
||||
"dependencies": {
|
||||
"debug": "*"
|
||||
},
|
||||
|
||||
"peerDependencies": {
|
||||
"mocha": "^2"
|
||||
}
|
||||
}
|
||||
146
gaia/tests/jsmarionette/mocha/mocha-json-proxy/reporter.js
Executable file
146
gaia/tests/jsmarionette/mocha/mocha-json-proxy/reporter.js
Executable file
|
|
@ -0,0 +1,146 @@
|
|||
'use strict';
|
||||
var Base = require('mocha').reporters.Base;
|
||||
|
||||
// list of taboo properties to never clone
|
||||
var TABOO = ['err'];
|
||||
|
||||
var EVENTS = [
|
||||
'pending',
|
||||
'pass',
|
||||
'fail',
|
||||
'test',
|
||||
'test end',
|
||||
'suite',
|
||||
'suite end',
|
||||
'start',
|
||||
'end'
|
||||
];
|
||||
|
||||
var ALLOWED_OBJECTS = ['err'];
|
||||
|
||||
function write(event, content) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
|
||||
if (!process.env[Reporter.FORK_ENV]) {
|
||||
process.stdout.write(JSON.stringify(args) + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
process.send(['mocha-proxy', args]);
|
||||
}
|
||||
|
||||
function cloneValue(value) {
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
return cloneTestObject(value);
|
||||
case 'function':
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function cloneTestObject(object) {
|
||||
var keys = Object.keys(object),
|
||||
result = {};
|
||||
|
||||
for (var i = 0, len = keys.length; i < len; i++) {
|
||||
var key = keys[i];
|
||||
var value = object[key];
|
||||
|
||||
// don't clone taboo properties
|
||||
if (TABOO.indexOf(key) !== -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// In order to optimize transmission, don't clone the parent. Annotate its
|
||||
// ID so the consumer can correctly re-create the relationship.
|
||||
if (key === 'parent') {
|
||||
result._parentId = value._id;
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't copy objects recursively
|
||||
if (typeof value === 'object' && ALLOWED_OBJECTS.indexOf(key) === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[key] = cloneValue(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function cloneError(err) {
|
||||
var result = {
|
||||
name: err.name || '',
|
||||
stack: err.stack,
|
||||
type: err.type || 'Error',
|
||||
constructorName: err.constructorName || 'Error',
|
||||
expected: err.expected || null,
|
||||
actual: err.actual || null
|
||||
};
|
||||
|
||||
if (err && 'uncaught' in err) {
|
||||
result.uncaught = err.uncaught;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function defaultHandler(type, runner, payload) {
|
||||
return write(type, cloneValue(payload));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Static object responsible for converting each event type.
|
||||
*/
|
||||
var EventHandler = {
|
||||
fail: function(event, runner, payload, err) {
|
||||
var jsonPayload = cloneValue(payload);
|
||||
// remove .err so it does not override the real error.
|
||||
if (jsonPayload.err) {
|
||||
delete jsonPayload.err;
|
||||
}
|
||||
|
||||
write(event, jsonPayload, cloneError(err));
|
||||
}
|
||||
};
|
||||
|
||||
function Reporter(runner) {
|
||||
var objects = [];
|
||||
var ids = 1;
|
||||
|
||||
Base.call(this, runner);
|
||||
|
||||
EVENTS.forEach(function(event) {
|
||||
var handler = EventHandler[event] || defaultHandler;
|
||||
handler = handler.bind(this, event, runner);
|
||||
runner.on(event, function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var runnable = args[0];
|
||||
|
||||
// when we find an object create a unique ID for it.
|
||||
if (typeof runnable === 'object' && runnable !== null) {
|
||||
var idx = objects.indexOf(runnable);
|
||||
if (idx === -1) {
|
||||
runnable._id = ids++;
|
||||
objects.push(runnable);
|
||||
} else {
|
||||
// assign a unique objects[idx]._id to the runnable.
|
||||
runnable._id = objects[idx]._id;
|
||||
}
|
||||
}
|
||||
handler.apply(this, args);
|
||||
}.bind(this));
|
||||
}, this);
|
||||
}
|
||||
|
||||
Reporter.write = write;
|
||||
|
||||
Reporter.prototype = Object.create(Base.prototype);
|
||||
|
||||
Reporter.FORK_ENV = 'MOCHA_PROXY_SEND_ONLY';
|
||||
|
||||
module.exports = Reporter;
|
||||
23
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/acceptance/consumer.js
Executable file
23
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/acceptance/consumer.js
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
/* global forkFixture */
|
||||
'use strict';
|
||||
var Consumer = require('../../consumer');
|
||||
var Mocha = require('mocha');
|
||||
|
||||
suite('reporter', function() {
|
||||
function testReporter(reporterType) {
|
||||
suite(reporterType, function() {
|
||||
['pass', 'fail', 'pending'].forEach(function(testType) {
|
||||
test(test, function(done) {
|
||||
var child = forkFixture(testType);
|
||||
var runner = new Consumer(child);
|
||||
/* jshint nonew: false */
|
||||
new Mocha.reporters[reporterType](runner);
|
||||
/* jshint nonew: true */
|
||||
runner.once('end', done);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
['Dot', 'Spec'].forEach(testReporter);
|
||||
});
|
||||
46
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/acceptance/reporter.js
Executable file
46
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/acceptance/reporter.js
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
/* global assert, forkFixture */
|
||||
'use strict';
|
||||
suite('reporter', function() {
|
||||
var msgs = [];
|
||||
var child;
|
||||
|
||||
setup(function(done) {
|
||||
this.timeout('10s');
|
||||
child = forkFixture('pass');
|
||||
child.on('message', function(content) {
|
||||
if (Array.isArray(content) && content[0] === 'mocha-proxy') {
|
||||
var msg = content[1];
|
||||
msgs.push(msg[0]);
|
||||
if (msg[0] === 'end') {
|
||||
return done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('got messages', function() {
|
||||
var expectedMsgs = [
|
||||
'start',
|
||||
'suite',
|
||||
'suite',
|
||||
'test',
|
||||
'pass',
|
||||
'test end',
|
||||
'test',
|
||||
'pass',
|
||||
'test end',
|
||||
'suite end',
|
||||
'suite',
|
||||
'test',
|
||||
'pass',
|
||||
'test end',
|
||||
'suite end',
|
||||
'suite end',
|
||||
'end'
|
||||
];
|
||||
|
||||
assert.deepEqual(msgs, expectedMsgs);
|
||||
});
|
||||
|
||||
});
|
||||
150
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/consumer.js
Executable file
150
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/consumer.js
Executable file
|
|
@ -0,0 +1,150 @@
|
|||
/* global forkFixture */
|
||||
'use strict';
|
||||
var assert = require('assert');
|
||||
|
||||
suite('consumer', function() {
|
||||
var child;
|
||||
var Consumer = require('../consumer');
|
||||
var subject;
|
||||
|
||||
var EVENTS = [
|
||||
'start',
|
||||
'pass',
|
||||
'test',
|
||||
'test end',
|
||||
'suite',
|
||||
'suite end',
|
||||
'end'
|
||||
];
|
||||
|
||||
var emitted;
|
||||
|
||||
setup(function(done) {
|
||||
emitted = {};
|
||||
child = forkFixture('pass');
|
||||
subject = new Consumer(child);
|
||||
|
||||
EVENTS.forEach(function(event) {
|
||||
emitted = {};
|
||||
subject.on(event, function() {
|
||||
if (!emitted[event]) {
|
||||
emitted[event] = [];
|
||||
}
|
||||
|
||||
emitted[event].push(Array.prototype.slice.call(arguments));
|
||||
});
|
||||
});
|
||||
|
||||
subject.once('end', done);
|
||||
});
|
||||
|
||||
function emitsRunnable(event) {
|
||||
if (typeof event === 'string') {
|
||||
return emitted[event].forEach(emitsRunnable);
|
||||
}
|
||||
|
||||
assert.ok(typeof event[0].slow === 'function', 'has .slow');
|
||||
assert.equal(event[0].slow(), event[0]._slow);
|
||||
|
||||
assert.ok(typeof event[0].fullTitle === 'function', 'has .fullTitle()');
|
||||
|
||||
if (event[0].parent && event[0].parent.fullTitle()) {
|
||||
assert.equal(
|
||||
event[0].fullTitle(),
|
||||
event[0].parent.fullTitle() + ' ' + event[0].title
|
||||
);
|
||||
} else {
|
||||
assert.equal(
|
||||
event[0].fullTitle(),
|
||||
event[0].title
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test('.suite', function() {
|
||||
assert.ok(subject.suite);
|
||||
assert.ok(subject.suite.root);
|
||||
});
|
||||
|
||||
suite('suite', function() {
|
||||
test('is runnable', function() {
|
||||
emitsRunnable('suite');
|
||||
});
|
||||
|
||||
test('child suite should have reference to parent', function() {
|
||||
var parent = emitted.suite[0][0];
|
||||
var child1 = emitted.suite[1][0];
|
||||
var child2 = emitted.suite[2][0];
|
||||
|
||||
assert.ok(!parent.parent, 'root suite has no parent');
|
||||
assert.strictEqual(
|
||||
child1.parent,
|
||||
parent,
|
||||
'first child references parent'
|
||||
);
|
||||
assert.strictEqual(
|
||||
child2.parent,
|
||||
parent,
|
||||
'second child references parent'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('suite end', function() {
|
||||
emitsRunnable('suite end');
|
||||
});
|
||||
|
||||
test('test', function() {
|
||||
emitsRunnable('test');
|
||||
});
|
||||
|
||||
test('pass', function() {
|
||||
emitsRunnable('pass');
|
||||
});
|
||||
|
||||
test('test end', function() {
|
||||
emitsRunnable('test end');
|
||||
});
|
||||
|
||||
suite('objects with ._ids are the same', function() {
|
||||
var tests;
|
||||
var suites;
|
||||
|
||||
function group(events, target) {
|
||||
events.forEach(function(event) {
|
||||
emitted[event].forEach(function(items) {
|
||||
items.forEach(function(item) {
|
||||
if (!target[item.title]) {
|
||||
target[item.title] = [];
|
||||
}
|
||||
|
||||
target[item.title].push(item);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setup(function() {
|
||||
tests = {};
|
||||
suites = {};
|
||||
|
||||
group(['test', 'pass', 'test end'], tests);
|
||||
group(['suite', 'suite end'], suites);
|
||||
});
|
||||
|
||||
test('tests async', function() {
|
||||
var test = tests.async[0];
|
||||
tests.async.forEach(function(item) {
|
||||
assert.strictEqual(test, item);
|
||||
});
|
||||
});
|
||||
|
||||
test('suite', function() {
|
||||
var suite = suites.pass[0];
|
||||
suites.pass.forEach(function(item) {
|
||||
assert.strictEqual(suite, item);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
17
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/fixtures/fail.js
vendored
Executable file
17
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/fixtures/fail.js
vendored
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
suite('fail', function() {
|
||||
test('sync', function() {
|
||||
throw new Error('sync');
|
||||
});
|
||||
|
||||
test('async', function(done) {
|
||||
done(new Error('async'));
|
||||
});
|
||||
|
||||
test('uncaught', function() {
|
||||
process.nextTick(function() {
|
||||
throw new Error('woot');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
8
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/fixtures/pass.js
vendored
Executable file
8
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/fixtures/pass.js
vendored
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
'use strict';
|
||||
suite('pass', function() {
|
||||
test('sync', function() {});
|
||||
test('async', function(done) { process.nextTick(done); });
|
||||
});
|
||||
suite('pass 2', function() {
|
||||
test('sync 2', function() {});
|
||||
});
|
||||
4
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/fixtures/pending.js
vendored
Executable file
4
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/fixtures/pending.js
vendored
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
'use strict';
|
||||
suite('pending', function() {
|
||||
test('mepending');
|
||||
});
|
||||
9
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/fixtures/suites.js
vendored
Executable file
9
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/fixtures/suites.js
vendored
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
'use strict';
|
||||
suite('a', function() {
|
||||
suite('b', function() {
|
||||
suite('c', function() {
|
||||
test('done', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
63
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/helper.js
Executable file
63
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/helper.js
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
'use strict';
|
||||
var exec = require('child_process').exec,
|
||||
fork = require('child_process').fork,
|
||||
EE = require('events').EventEmitter,
|
||||
debug = require('debug')('mocha-json-proxy:test');
|
||||
|
||||
|
||||
function forkFixture(test) {
|
||||
var reporterPath = __dirname + '/../reporter.js';
|
||||
var exec = __dirname + '/../../../../.bin/_mocha';
|
||||
var cmd = [
|
||||
'--reporter', reporterPath,
|
||||
'--ui', 'tdd',
|
||||
__dirname + '/fixtures/' + test
|
||||
];
|
||||
|
||||
var env = {};
|
||||
for (var key in process.env) {
|
||||
env[key] = process.env[key];
|
||||
}
|
||||
|
||||
var runnerOpts = {
|
||||
env: env
|
||||
};
|
||||
|
||||
// turn on the option to send messages directly between processes.
|
||||
runnerOpts.env[require(reporterPath).FORK_ENV] = true;
|
||||
|
||||
return fork(exec, cmd, runnerOpts);
|
||||
}
|
||||
|
||||
function runFixture(test, callback) {
|
||||
var cmd =
|
||||
__dirname + '/../../../../.bin/mocha' +
|
||||
' --ui tdd --reporter ' + __dirname + '/../reporter ' +
|
||||
__dirname + '/fixtures/' + test;
|
||||
|
||||
var emitter = new EE();
|
||||
exec(cmd, function(err, stdout, stderr) {
|
||||
debug('stdout', stdout);
|
||||
if (stderr) {
|
||||
console.error('ERR>>', stderr);
|
||||
}
|
||||
|
||||
callback(null, emitter);
|
||||
|
||||
var out = stdout.trim().split('\n');
|
||||
out.forEach(function(line) {
|
||||
var json;
|
||||
try {
|
||||
json = JSON.parse(line);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
emitter.emit.apply(emitter, json);
|
||||
});
|
||||
emitter.emit('helper end');
|
||||
});
|
||||
}
|
||||
|
||||
global.assert = require('chai').assert;
|
||||
global.runFixture = runFixture;
|
||||
global.forkFixture = forkFixture;
|
||||
232
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/reporter.js
Executable file
232
gaia/tests/jsmarionette/mocha/mocha-json-proxy/test/reporter.js
Executable file
|
|
@ -0,0 +1,232 @@
|
|||
/* global runFixture */
|
||||
'use strict';
|
||||
var assert = require('assert');
|
||||
|
||||
suite('reporter', function() {
|
||||
|
||||
function event(input, callback) {
|
||||
var file, type;
|
||||
|
||||
if (typeof input === 'string') {
|
||||
file = type = input;
|
||||
}
|
||||
|
||||
if (typeof input === 'object') {
|
||||
file = input[0];
|
||||
type = input[1];
|
||||
}
|
||||
|
||||
|
||||
return test('event: ' + type, function(done) {
|
||||
runFixture(file, function(err, emit) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
callback(emit, done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isTest(input, obj) {
|
||||
assert.ok(input.fn.indexOf('function') !== -1, 'has function');
|
||||
assert.ok(input.title);
|
||||
assert.equal(input.type, 'test');
|
||||
assert.ok(input._slow);
|
||||
assert.ok(input._id, 'has _id');
|
||||
|
||||
if (obj) {
|
||||
for (var key in obj) {
|
||||
assert.equal(input[key], obj[key], key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function aggregateAll(events, emitter) {
|
||||
var result = {};
|
||||
|
||||
function add(event, data, err) {
|
||||
if (err)
|
||||
data.eventErr = err;
|
||||
|
||||
if (!result[data.title]) {
|
||||
result[data.title] = [];
|
||||
}
|
||||
|
||||
result[data.title].push(data);
|
||||
}
|
||||
|
||||
events.forEach(function(event) {
|
||||
emitter.on(event, add.bind(null, event));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function aggregate(event, emitter) {
|
||||
var result = {};
|
||||
emitter.on(event, function(data, err) {
|
||||
// copy the err property if available.
|
||||
if (err)
|
||||
data.sentErr = err;
|
||||
|
||||
result[data.title] = data;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function isError(err, msg) {
|
||||
if (msg)
|
||||
assert.ok(err.msg.matches(msg), 'matches msg');
|
||||
|
||||
assert.ok(err.stack, 'err.stack');
|
||||
}
|
||||
|
||||
function testIdsMatch(list) {
|
||||
// usually a test event.
|
||||
var firstEvent = list[list.length - 1];
|
||||
|
||||
// verify the id is valid
|
||||
assert.ok(firstEvent._id, 'has an _id');
|
||||
|
||||
var id = firstEvent._id;
|
||||
|
||||
// verify each id matches the other ids.
|
||||
list.forEach(function(item) {
|
||||
assert.equal(item._id, id);
|
||||
});
|
||||
}
|
||||
|
||||
['test', 'test end'].forEach(function(eventName) {
|
||||
event(['fail', eventName], function(emit, done) {
|
||||
var testEnd = aggregate(eventName, emit);
|
||||
|
||||
emit.on('helper end', function() {
|
||||
assert.ok(!testEnd.sync.err, 'err is not set');
|
||||
assert.ok(!testEnd.async.err, 'err is not set');
|
||||
|
||||
isTest(testEnd.sync);
|
||||
isTest(testEnd.async);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('fail', function() {
|
||||
event('fail', function(emit, done) {
|
||||
var fails = aggregate('fail', emit);
|
||||
|
||||
emit.on('helper end', function() {
|
||||
isTest(fails.sync, { state: 'failed' });
|
||||
isTest(fails.async, { state: 'failed' });
|
||||
|
||||
assert.ok(fails.sync.sentErr, 'sync has err');
|
||||
assert.ok(fails.async.sentErr, 'async has err');
|
||||
|
||||
isError(fails.sync.sentErr);
|
||||
isError(fails.async.sentErr);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
event(['fail', 'ids'], function(emit, done) {
|
||||
var results =
|
||||
aggregateAll(['test', 'test end', 'fail'], emit);
|
||||
|
||||
emit.once('helper end', function() {
|
||||
testIdsMatch(results.sync);
|
||||
testIdsMatch(results.async);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
suite('pass', function() {
|
||||
event('pass', function(emit, done) {
|
||||
var passed = aggregate('pass', emit);
|
||||
|
||||
emit.on('helper end', function() {
|
||||
assert.ok(passed.sync, 'sync test');
|
||||
assert.ok(passed.async, 'async test');
|
||||
|
||||
isTest(passed.sync, { state: 'passed' });
|
||||
isTest(passed.async, { state: 'passed' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
event(['pass', 'ids'], function(emit, done) {
|
||||
var results =
|
||||
aggregateAll(['test', 'test end', 'pass'], emit);
|
||||
|
||||
emit.once('helper end', function() {
|
||||
testIdsMatch(results.sync);
|
||||
testIdsMatch(results.async);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('pending', function() {
|
||||
event('pending', function(emit, done) {
|
||||
emit.once('pending', function(data) {
|
||||
assert.equal(data.title, 'mepending', 'title');
|
||||
assert.ok(data.pending, 'pending');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
event(['pending', 'ids'], function(emit, done) {
|
||||
var results =
|
||||
aggregateAll(['test end', 'pending'], emit);
|
||||
|
||||
emit.once('helper end', function() {
|
||||
testIdsMatch(results.mepending);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* TODO(gaye): Re-enable these...
|
||||
event('start', function(emit, done) {
|
||||
emit.once('start', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
event('end', function(emit, done) {
|
||||
emit.once('end', function(data) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
event(['suites', 'suite (end)'], function(emit, done) {
|
||||
var events = [];
|
||||
var expected = [
|
||||
['start', '', 1],
|
||||
['start', 'a', 2],
|
||||
['start', 'b', 3],
|
||||
['start', 'c', 4],
|
||||
['end', 'c', 4],
|
||||
['end', 'b', 3],
|
||||
['end', 'a', 2],
|
||||
['end', '', 1]
|
||||
];
|
||||
|
||||
function add(type, data) {
|
||||
events.push([type, data.title, data._id]);
|
||||
}
|
||||
|
||||
emit.on('suite', add.bind(null, 'start'));
|
||||
emit.on('suite end', add.bind(null, 'end'));
|
||||
|
||||
emit.once('helper end', function() {
|
||||
assert.deepEqual(events, expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
5
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/examples/duration.js
Executable file
5
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/examples/duration.js
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
test('duration', function(done) {
|
||||
setTimeout(done, 100, null);
|
||||
});
|
||||
|
||||
7
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/examples/fail.js
Executable file
7
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/examples/fail.js
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
test('fail', function() {
|
||||
throw new Error('myerror');
|
||||
});
|
||||
|
||||
test('pass later', function() {
|
||||
});
|
||||
0
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/examples/pass.js
Executable file
0
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/examples/pass.js
Executable file
1
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/examples/pending.js
Executable file
1
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/examples/pending.js
Executable file
|
|
@ -0,0 +1 @@
|
|||
test.skip('pending');
|
||||
1
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/index.js
Executable file
1
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/index.js
Executable file
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('./lib/tbpl');
|
||||
164
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/lib/tbpl.js
Executable file
164
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/lib/tbpl.js
Executable file
|
|
@ -0,0 +1,164 @@
|
|||
'use strict';
|
||||
var Base = require('mocha').reporters.Base;
|
||||
var fsPath = require('path');
|
||||
|
||||
/**
|
||||
* Initialize a new TBPL reporter.
|
||||
* @constructor
|
||||
* @param {Runner} runner mocha test runner.
|
||||
*/
|
||||
function TBPL(runner) {
|
||||
Base.call(this, runner);
|
||||
|
||||
this.onEnd = this.onEnd.bind(this);
|
||||
runner.on('end', this.onEnd);
|
||||
this.onFail = this.onFail.bind(this);
|
||||
runner.on('fail', this.onFail);
|
||||
this.onPass = this.onPass.bind(this);
|
||||
runner.on('pass', this.onPass);
|
||||
this.onPending = this.onPending.bind(this);
|
||||
runner.on('pending', this.onPending);
|
||||
this.onTest = this.onTest.bind(this);
|
||||
runner.on('test', this.onTest);
|
||||
this.onTestEnd = this.onTestEnd.bind(this);
|
||||
runner.on('test end', this.onTestEnd);
|
||||
|
||||
this.failing = 0;
|
||||
this.passing = 0;
|
||||
this.pending = 0;
|
||||
}
|
||||
module.exports = TBPL;
|
||||
|
||||
|
||||
TBPL.prototype = {
|
||||
__proto__: Base.prototype,
|
||||
|
||||
/**
|
||||
* Number of failing tests.
|
||||
* @type {number}
|
||||
*/
|
||||
failing: 0,
|
||||
|
||||
/**
|
||||
* Number of passing tests.
|
||||
* @type {number}
|
||||
*/
|
||||
passing: 0,
|
||||
|
||||
/**
|
||||
* Number of pending tests.
|
||||
* @type {number}
|
||||
*/
|
||||
pending: 0,
|
||||
|
||||
/**
|
||||
* Output a summary of the mocha run.
|
||||
*/
|
||||
onEnd: function() {
|
||||
console.log('*~*~* Results *~*~*');
|
||||
console.log('passed: %d', this.passing);
|
||||
console.log('failed: %d', this.failing);
|
||||
console.log('todo: %d', this.pending);
|
||||
this.epilogue();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Test} test failing test.
|
||||
* @param {Error} err failure.
|
||||
*/
|
||||
onFail: function(test, err) {
|
||||
var title = this.getTitle(test),
|
||||
file = this.getFile(test);
|
||||
console.log('TEST-UNEXPECTED-FAIL | %s | %s', file, title);
|
||||
console.log(err.stack);
|
||||
this.failing += 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Test} test passing test.
|
||||
*/
|
||||
onPass: function(test) {
|
||||
var title = this.getTitle(test),
|
||||
file = this.getFile(test);
|
||||
console.log('TEST-PASS | %s | %s', file, title);
|
||||
this.passing += 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Test} test pending test.
|
||||
*/
|
||||
onPending: function(test) {
|
||||
var title = this.getTitle(test),
|
||||
file = this.getFile(test);
|
||||
console.log('TEST-PENDING | %s | %s', file, title);
|
||||
this.pending += 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Test} test started test.
|
||||
*/
|
||||
onTest: function(test) {
|
||||
var title = this.getTitle(test),
|
||||
file = this.getFile(test);
|
||||
console.log('TEST-START | %s | %s', file, title);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Test} test finished test.
|
||||
*/
|
||||
onTestEnd: function(test) {
|
||||
var title = this.getTitle(test),
|
||||
file = this.getFile(test);
|
||||
console.log('TEST-END | %s | %s took %d ms', file, title, test.duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Test} test some test.
|
||||
* @return {string} the title of the test.
|
||||
*/
|
||||
getTitle: function(test) {
|
||||
return this.sanitize(test.fullTitle());
|
||||
},
|
||||
|
||||
getFile: function(test) {
|
||||
var file = null;
|
||||
if ('file' in test) {
|
||||
file = test.file;
|
||||
}
|
||||
|
||||
if ('parent' in test) {
|
||||
var tmpfile = this.getFile(test.parent);
|
||||
if (tmpfile) {
|
||||
file = tmpfile;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the file relative if we have one...
|
||||
if (file) {
|
||||
// Resolve ensures we never end up with two trailing slashes...
|
||||
var pwd = fsPath.resolve(process.cwd()) + '/';
|
||||
// Make sure the file starts with pwd before replacing it.
|
||||
if (file.indexOf(pwd) === 0) {
|
||||
file = file.replace(pwd, '');
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} str some string that could potentially have character
|
||||
* sequences that tbpl would understand.
|
||||
* @return {string} sanitized string.
|
||||
*/
|
||||
sanitize: function(str) {
|
||||
// These are controversial words and we must censor them!
|
||||
return str
|
||||
.replace(/PROCESS-CRASH/g, '*************')
|
||||
.replace(/TEST-END/g, '********')
|
||||
.replace(/TEST-KNOWN-FAIL/g, '***************')
|
||||
.replace(/TEST-PASS/g, '*********')
|
||||
.replace(/TEST-START/g, '***********')
|
||||
.replace(/TEST-UNEXPECTED-FAIL/g, '********************');
|
||||
}
|
||||
};
|
||||
14
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/package.json
Executable file
14
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/package.json
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "mocha-tbpl-reporter",
|
||||
"version": "0.2.1",
|
||||
"author": "The Gaia Team <dev-gaia@lists.mozilla.org>",
|
||||
"description": "Mocha reporter for TBPL (tinderboxpushlog)",
|
||||
|
||||
"devDependencies": {
|
||||
"mocha": "^1.0.0"
|
||||
},
|
||||
|
||||
"peerDependencies": {
|
||||
"mocha": ">=1.0"
|
||||
}
|
||||
}
|
||||
166
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/test/tbpl_test.js
Executable file
166
gaia/tests/jsmarionette/mocha/mocha-tbpl-reporter/test/tbpl_test.js
Executable file
|
|
@ -0,0 +1,166 @@
|
|||
'use strict';
|
||||
var TBPL = require('../lib/tbpl'),
|
||||
assert = require('assert'),
|
||||
sinon = require('sinon');
|
||||
|
||||
suite('TBPL', function() {
|
||||
var subject, runner, on, log;
|
||||
|
||||
setup(function() {
|
||||
runner = {
|
||||
on: function(type, listener) {}
|
||||
};
|
||||
|
||||
on = sinon.spy(runner, 'on');
|
||||
log = sinon.spy(console, 'log');
|
||||
subject = new TBPL(runner);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
on.restore();
|
||||
log.restore();
|
||||
});
|
||||
|
||||
test('#constructor', function() {
|
||||
sinon.assert.calledWith(on, 'end');
|
||||
sinon.assert.calledWith(on, 'fail');
|
||||
sinon.assert.calledWith(on, 'pass');
|
||||
sinon.assert.calledWith(on, 'pending');
|
||||
sinon.assert.calledWith(on, 'test');
|
||||
sinon.assert.calledWith(on, 'test end');
|
||||
assert.strictEqual(subject.failing, 0);
|
||||
assert.strictEqual(subject.passing, 0);
|
||||
assert.strictEqual(subject.pending, 0);
|
||||
});
|
||||
|
||||
test('#onEnd', function() {
|
||||
subject.onEnd();
|
||||
assert.ok(log.calledWith('*~*~* Results *~*~*'));
|
||||
assert.ok(log.calledWith('passed: %d', 0));
|
||||
assert.ok(log.calledWith('failed: %d', 0));
|
||||
assert.ok(log.calledWith('todo: %d', 0));
|
||||
});
|
||||
|
||||
test('#onFail', function() {
|
||||
var failing = subject.failing;
|
||||
var file = 'doge_such_broke_test.js';
|
||||
var err = new Error('xfoo');
|
||||
subject.onFail(
|
||||
{
|
||||
file: file,
|
||||
fullTitle: function() {
|
||||
return 'some title';
|
||||
}
|
||||
},
|
||||
err
|
||||
);
|
||||
|
||||
assert.strictEqual(subject.failing, failing + 1);
|
||||
assert.ok(log.calledWith(
|
||||
'TEST-UNEXPECTED-FAIL | %s | %s',
|
||||
file,
|
||||
'some title'
|
||||
));
|
||||
assert.ok(log.calledWith(err.stack));
|
||||
});
|
||||
|
||||
test('#onPass', function() {
|
||||
var passing = subject.passing;
|
||||
subject.onPass({
|
||||
file: 'file.js',
|
||||
fullTitle: function() {
|
||||
return 'some title';
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(subject.passing, passing + 1);
|
||||
assert.ok(log.calledWith('TEST-PASS | %s | %s', 'file.js', 'some title'));
|
||||
});
|
||||
|
||||
test('#onPending', function() {
|
||||
var pending = subject.pending;
|
||||
subject.onPending({
|
||||
file: 'file.js',
|
||||
fullTitle: function() {
|
||||
return 'some title';
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(subject.pending, pending + 1);
|
||||
assert.ok(
|
||||
log.calledWith('TEST-PENDING | %s | %s', 'file.js', 'some title')
|
||||
);
|
||||
});
|
||||
|
||||
test('#onTest', function() {
|
||||
subject.onTest({
|
||||
file: 'file.js',
|
||||
fullTitle: function() {
|
||||
return 'some title';
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(log.calledWith('TEST-START | %s | %s', 'file.js', 'some title'));
|
||||
});
|
||||
|
||||
test('#onTestEnd', function() {
|
||||
subject.onTestEnd({
|
||||
duration: 100,
|
||||
file: 'file.js',
|
||||
fullTitle: function() {
|
||||
return 'some title';
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
log.calledWith(
|
||||
'TEST-END | %s | %s took %d ms', 'file.js', 'some title', 100
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('#getFile - relative', function() {
|
||||
var file = subject.getFile({
|
||||
file: __dirname + '/xfoo'
|
||||
});
|
||||
assert.equal(
|
||||
file,
|
||||
'node_modules/mocha-tbpl-reporter/test/xfoo'
|
||||
);
|
||||
});
|
||||
|
||||
test('#getFile - relative different pwd', function() {
|
||||
var file = subject.getFile({
|
||||
file: 'woot/bar.js'
|
||||
});
|
||||
assert.equal(file, 'woot/bar.js');
|
||||
});
|
||||
|
||||
test('#getFile - absolute', function() {
|
||||
var file = subject.getFile({
|
||||
file: '/magic/foo/bar/file.js'
|
||||
});
|
||||
assert.equal(file, '/magic/foo/bar/file.js');
|
||||
});
|
||||
|
||||
test('#getTitle', function() {
|
||||
var result = subject.getTitle({
|
||||
fullTitle: function() {
|
||||
return 'some title TEST-END';
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result, 'some title ********');
|
||||
});
|
||||
|
||||
test('#sanitize', function() {
|
||||
var str = 'Fuzzy PROCESS-CRASH was a TEST-END, ' +
|
||||
'fuzzy TEST-KNOWN-FAIL had no TEST-PASS, ' +
|
||||
'fuzzy TEST-START wasn\'t TEST-UNEXPECTED-FAIL was he?';
|
||||
var result = subject.sanitize(str);
|
||||
assert.equal(result,
|
||||
'Fuzzy ************* was a ********, ' +
|
||||
'fuzzy *************** had no *********, ' +
|
||||
'fuzzy *********** wasn\'t ******************** was he?');
|
||||
});
|
||||
});
|
||||
66
gaia/tests/jsmarionette/plugins/marionette-apps/examples/launch.js
Executable file
66
gaia/tests/jsmarionette/plugins/marionette-apps/examples/launch.js
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @fileoverview An example that shows how to use the MarionetteApps plugin
|
||||
* to get a list of the installed apps and launch one.
|
||||
*/
|
||||
var Host = require('marionette-host-environment'),
|
||||
Marionette = require('marionette-client'),
|
||||
path = require('path');
|
||||
|
||||
|
||||
/**
|
||||
* Options to setup our b2g instance with.
|
||||
* @type {Object}
|
||||
*/
|
||||
var OPTIONS = {
|
||||
settings: {
|
||||
'ftu.manifestURL': null,
|
||||
'lockscreen.enabled': false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* File path to B2G.
|
||||
* @const {string}
|
||||
*/
|
||||
var B2G_PATH = path.resolve(__dirname, '../b2g');
|
||||
|
||||
|
||||
/**
|
||||
* Origin URL for the calendar app.
|
||||
* @const {string}
|
||||
*/
|
||||
var CALENDAR_URL = 'app://calendar.gaiamobile.org';
|
||||
|
||||
|
||||
/**
|
||||
* Gets called once we have a child process booted with Firefox OS.
|
||||
* @param {Marionette.Client} client Marionette client to use.
|
||||
* @param {ChildProcess} childProcess Child process running b2g.
|
||||
*/
|
||||
function demo(client, childProcess) {
|
||||
client.apps.launch(CALENDAR_URL, function() {
|
||||
setTimeout(childProcess.kill.bind(childProcess), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Bring up a b2g desktop instance.
|
||||
Host.spawn(B2G_PATH, OPTIONS, function(err, port, childProcess) {
|
||||
if (process.env.DEBUG) {
|
||||
childProcess.stdout.pipe(process.stdout);
|
||||
}
|
||||
|
||||
// Connect to the marionette server.
|
||||
var driver = new Marionette.Drivers.TcpSync({ port: port });
|
||||
driver.connect(function() {
|
||||
// Wrap the marionette connection with a client that we can pass
|
||||
// to our plugin.
|
||||
var client = new Marionette.Client(driver);
|
||||
client.plugin('apps', require('../index'));
|
||||
client.startSession(demo.bind(null, client, childProcess));
|
||||
});
|
||||
});
|
||||
43
gaia/tests/jsmarionette/plugins/marionette-apps/index.js
Executable file
43
gaia/tests/jsmarionette/plugins/marionette-apps/index.js
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
'use strict';
|
||||
var Apps = require('./lib/apps');
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Apps} apps private marionette apps api.
|
||||
*/
|
||||
function MarionetteApps(apps) {
|
||||
for (var key in MarionetteApps.ModuleToFunction) {
|
||||
var methodName = MarionetteApps.ModuleToFunction[key];
|
||||
var module = require('./lib/' + key);
|
||||
// bind the public interface to the object
|
||||
this[methodName] = module[methodName].bind(null, apps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of public modules to expose to the main interface.
|
||||
* Key is module, value is function on the module.
|
||||
*/
|
||||
MarionetteApps.ModuleToFunction = {
|
||||
close: 'close',
|
||||
launch: 'launch',
|
||||
list: 'list',
|
||||
switchtoapp: 'switchToApp',
|
||||
switchtoactivity: 'switchToActivity',
|
||||
getapp: 'getApp',
|
||||
getself: 'getSelf'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new marionette apps plugin.
|
||||
* @param {Marionette.Client} client marionette client to use.
|
||||
* @param {Objects} options optional configuration.
|
||||
* @return {MarionetteApps} new marionette apps.
|
||||
*/
|
||||
function setup(client, options) {
|
||||
var apps = Apps.setup(client, options);
|
||||
return new MarionetteApps(apps);
|
||||
}
|
||||
|
||||
exports.setup = setup;
|
||||
97
gaia/tests/jsmarionette/plugins/marionette-apps/lib/app.js
Executable file
97
gaia/tests/jsmarionette/plugins/marionette-apps/lib/app.js
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
'use strict';
|
||||
var GeckoObject = require(__dirname + '/geckoobject');
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Marionette.Client} client Marionette client to use.
|
||||
*/
|
||||
function App(client) {
|
||||
GeckoObject.apply(this, arguments);
|
||||
this._client = client;
|
||||
}
|
||||
module.exports = App;
|
||||
|
||||
|
||||
App.prototype = {
|
||||
__proto__: GeckoObject.prototype,
|
||||
|
||||
|
||||
/**
|
||||
* @type {Marionette.Client}
|
||||
* @private
|
||||
*/
|
||||
_client: undefined,
|
||||
|
||||
|
||||
/**
|
||||
* The origin of the site that triggered the installation of the app.
|
||||
* @type {string}
|
||||
*/
|
||||
installOrigin: undefined,
|
||||
|
||||
|
||||
/**
|
||||
* The time that the app was installed.
|
||||
* This is generated using Date().getTime(),
|
||||
* represented as the number of milliseconds
|
||||
* since midnight of January 1st, 1970.
|
||||
* @type {number}
|
||||
*/
|
||||
installTime: undefined,
|
||||
|
||||
|
||||
/**
|
||||
* The currently stored instance of the manifest of the app.
|
||||
* @type {Object}
|
||||
*/
|
||||
manifest: undefined,
|
||||
|
||||
|
||||
/**
|
||||
* Where the manifest was found.
|
||||
* @type {string}
|
||||
*/
|
||||
manifestURL: undefined,
|
||||
|
||||
|
||||
/**
|
||||
* The origin of the app (protocol, host, and optional port number).
|
||||
* For example: http://example.com.
|
||||
* @type {string}
|
||||
*/
|
||||
origin: undefined,
|
||||
|
||||
|
||||
/**
|
||||
* An object containing an array of one or more receipts.
|
||||
* Each receipt is a string. If there are no receipts, this is null.
|
||||
* @type {Object}
|
||||
*/
|
||||
receipts: undefined,
|
||||
|
||||
|
||||
checkForUpdate: function() {
|
||||
throw 'Not yet implemented';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Launches the application. Does not return any value.
|
||||
*
|
||||
* @param {String} entrypoint for app.
|
||||
*/
|
||||
launch: function(entrypoint) {
|
||||
var client = this._client.scope({ context: 'content' });
|
||||
client.executeAsyncScript(function(id, entrypoint) {
|
||||
var ObjectCache = window.wrappedJSObject.ObjectCache;
|
||||
var app = ObjectCache._inst.get(id);
|
||||
app.launch(entrypoint || null);
|
||||
marionetteScriptFinished();
|
||||
}, [this._id, entrypoint], function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
97
gaia/tests/jsmarionette/plugins/marionette-apps/lib/apps.js
Executable file
97
gaia/tests/jsmarionette/plugins/marionette-apps/lib/apps.js
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
'use strict';
|
||||
var BootWatcher = require('./bootwatcher');
|
||||
var Mgmt = require('./mgmt');
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Marionette.Client} client Marionette client to use.
|
||||
*/
|
||||
function Apps(client) {
|
||||
this.mgmt = new Mgmt(client);
|
||||
this._client = client;
|
||||
}
|
||||
module.exports = Apps;
|
||||
|
||||
|
||||
/**
|
||||
* Make a new apps and register a client hook to startSession that triggers
|
||||
* the BootWatcher.
|
||||
* @param {Marionette.Client} client Marionette client to use.
|
||||
* @param {Object} options Optional map of attributes for Apps.
|
||||
* @return {Apps} instance.
|
||||
*/
|
||||
Apps.setup = function(client, options) {
|
||||
var apps = new Apps(client);
|
||||
|
||||
client.addHook('startSession', function(done) {
|
||||
BootWatcher.setup(client, function(err) {
|
||||
if (err) return done && done(err);
|
||||
apps.mgmt.prepareClient(done);
|
||||
});
|
||||
});
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
|
||||
Apps.prototype = {
|
||||
/**
|
||||
* A mgmt object that exposes functions that let dashboards
|
||||
* manage and launch apps on a user's behalf.
|
||||
* @type {Mgmt}
|
||||
*/
|
||||
mgmt: undefined,
|
||||
|
||||
|
||||
/**
|
||||
* @type {Marionette.Client}
|
||||
* @private
|
||||
*/
|
||||
_client: undefined,
|
||||
|
||||
|
||||
/**
|
||||
* Returns information about the current app, which means
|
||||
* an installed app whose domain matches the domain of the calling app.
|
||||
* Note: Multiple apps per origin are not supported.
|
||||
* To host several apps from one domain, set up a subdomain for each app;
|
||||
* for example: myapp.mydomain.com, otherapp.mydomain.com, and so forth.
|
||||
* @return {DOMRequest} Request that supports onsuccess, onerror callbacks.
|
||||
*/
|
||||
getSelf: function(callback) {
|
||||
return this.mgmt.getSelf(callback);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of all installed apps from this origin.
|
||||
* For example, if you call this on the Firefox Marketplace,
|
||||
* you will get the list of apps installed by the Firefox Marketplace.
|
||||
* @return {DOMRequest} Request that supports onsuccess, onerror callbacks.
|
||||
*/
|
||||
getInstalled: function() {
|
||||
throw 'Not yet implemented';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Triggers the installation of an app. During the installation process,
|
||||
* the app is validated and the user is prompted to approve the installation.
|
||||
* If the app has previously been installed from the same domain,
|
||||
* calling install() again may silently overwrite the existing install data.
|
||||
* This can be used to modify the purchase receipt, for example,
|
||||
* when a user upgrades from a free app to a premium app.
|
||||
* @param {string} url A string URL containing the location of the manifest
|
||||
* to be installed. In the case of self distribution
|
||||
* (where the installing origin is the same as the app origin),
|
||||
* the installing site may omit the origin part of the URL
|
||||
* and provide an absolute path (beginning with /).
|
||||
* @param {Object} receipts (Optional) A JSON object containing an array
|
||||
* of one or more receipts.
|
||||
* @return {DOMRequest} Request that supports onsuccess, onerror callbacks.
|
||||
*/
|
||||
install: function(url, receipts) {
|
||||
throw 'Not yet implemented';
|
||||
}
|
||||
};
|
||||
79
gaia/tests/jsmarionette/plugins/marionette-apps/lib/bootwatcher.js
Executable file
79
gaia/tests/jsmarionette/plugins/marionette-apps/lib/bootwatcher.js
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
'use strict';
|
||||
/**
|
||||
* @fileoverview Simple tool to notify listeners when a Firefox OS instance
|
||||
* finishes booting.
|
||||
*/
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Marionette.Client} client Marionette client to use.
|
||||
*/
|
||||
function BootWatcher(client) {
|
||||
EventEmitter.call(this, arguments);
|
||||
this._client = client;
|
||||
}
|
||||
module.exports = BootWatcher;
|
||||
|
||||
/**
|
||||
* Initialize the BootWatcher plugin!
|
||||
* @param {Marionette.Client} client Marionette client to use.
|
||||
* @param {Object} options Optional map of attributes for MarionetteApps.
|
||||
* @param {Function} cb Optional callback function to call after setup.
|
||||
* @return {BootWatcher} instance of bootwatcher.
|
||||
*/
|
||||
BootWatcher.setup = function(client, options, cb) {
|
||||
var bootwatcher = new BootWatcher(client);
|
||||
if (arguments.length === 2 && typeof(options) === 'function') {
|
||||
cb = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
process.nextTick(function() {
|
||||
// wait until client is booted up
|
||||
try {
|
||||
bootwatcher.start();
|
||||
} catch (error) {
|
||||
console.error('Never saw webapps-registry-ready yes');
|
||||
} finally {
|
||||
bootwatcher.emit('boot');
|
||||
if (cb) {
|
||||
cb(null, bootwatcher);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return bootwatcher;
|
||||
};
|
||||
|
||||
BootWatcher.prototype = {
|
||||
__proto__: EventEmitter.prototype,
|
||||
|
||||
/**
|
||||
* @type {Marionette.Client}
|
||||
* @private
|
||||
*/
|
||||
_client: undefined,
|
||||
|
||||
/**
|
||||
* Start watching for when we're booted.
|
||||
* Initially we were watching the window for the 'applicationready'
|
||||
* event. However, some javascript exceptions in the system app may
|
||||
* keep 'applicationready' from being fired, so poll instead. Also,
|
||||
* an error may be thrown while we're executing. In that case, retry.
|
||||
*/
|
||||
start: function() {
|
||||
var client = this._client;
|
||||
client.waitFor(function() {
|
||||
return client.executeScript(function() {
|
||||
try {
|
||||
var sessionStorage = window.wrappedJSObject.sessionStorage;
|
||||
var item = sessionStorage.getItem('webapps-registry-ready');
|
||||
return item === 'yes';
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}, { timeout: 30000 });
|
||||
}
|
||||
};
|
||||
78
gaia/tests/jsmarionette/plugins/marionette-apps/lib/close.js
Executable file
78
gaia/tests/jsmarionette/plugins/marionette-apps/lib/close.js
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
'use strict';
|
||||
var switchToApp = require('./switchtoapp').switchToApp;
|
||||
|
||||
/**
|
||||
* Small search timeout for faster polling.
|
||||
* @const {number}
|
||||
*/
|
||||
var SEARCH_TIMEOUT = 100;
|
||||
|
||||
/**
|
||||
* Close a currently running application.
|
||||
*
|
||||
* @param {Apps} apps state.
|
||||
* @param {String} origin of the application.
|
||||
* @param {String} [entrypoint] of the application.
|
||||
* @param {Function} callback [Error err].
|
||||
*/
|
||||
function close(apps, origin, entrypoint, callback) {
|
||||
if (typeof entrypoint === 'function') {
|
||||
callback = entrypoint;
|
||||
entrypoint = null;
|
||||
}
|
||||
|
||||
// populated during the app switch contains the "App" instance.
|
||||
var appInstance;
|
||||
|
||||
var client = apps._client.scope({
|
||||
searchTimeout: SEARCH_TIMEOUT
|
||||
});
|
||||
|
||||
/**
|
||||
* Go back to root frame now that app is closed.
|
||||
*/
|
||||
function switchToRoot(app, err) {
|
||||
if (err) return callback(err);
|
||||
client.switchToFrame(waitUntilClosed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the iframe with the origin is no longer in the dom tree.
|
||||
*/
|
||||
function waitUntilClosed(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
var query = 'iframe[src*="' + appInstance.source + '"]';
|
||||
// wait until the element has been removed.
|
||||
client.waitFor(function(done) {
|
||||
// poll until it's gone
|
||||
client.findElement(query, function(err) {
|
||||
// no element is found is the success condition.
|
||||
if (err && err.type === 'NoSuchElement') {
|
||||
return done(null, true);
|
||||
}
|
||||
|
||||
// error or element still exists...
|
||||
done(err, false);
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
|
||||
// switch to root context
|
||||
client.switchToFrame();
|
||||
|
||||
// find the app we are looking for
|
||||
return switchToApp(apps, origin, entrypoint, function(err, el, app) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// used in the following callbacks to ensure the right frame is closed.
|
||||
appInstance = app;
|
||||
|
||||
return client.executeScript(function() { window.close(); }, switchToRoot);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.close = close;
|
||||
22
gaia/tests/jsmarionette/plugins/marionette-apps/lib/geckoobject.js
Executable file
22
gaia/tests/jsmarionette/plugins/marionette-apps/lib/geckoobject.js
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
/**
|
||||
* @fileoverview A base class for WebApi objects like App that get shared
|
||||
* between gecko and node.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
function GeckoObject() {
|
||||
}
|
||||
module.exports = GeckoObject;
|
||||
|
||||
|
||||
GeckoObject.prototype = {
|
||||
/**
|
||||
* Id for objects that allows them to be identified across gecko and node.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
_id: ''
|
||||
};
|
||||
139
gaia/tests/jsmarionette/plugins/marionette-apps/lib/getapp.js
Executable file
139
gaia/tests/jsmarionette/plugins/marionette-apps/lib/getapp.js
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
'use strict';
|
||||
var fsPath = require('path'),
|
||||
list = require('./list').list,
|
||||
url = require('url');
|
||||
|
||||
/**
|
||||
* Creates the expected source of a app iframe.
|
||||
*
|
||||
* @param {App} app object to get source from.
|
||||
* @private
|
||||
*/
|
||||
function sourceForEntrypoint(app) {
|
||||
var origin = app.origin;
|
||||
var launchPath = app.entrypoint.details.launch_path;
|
||||
|
||||
var urlParts = url.parse(origin);
|
||||
|
||||
// Handle hashes in the launchPath
|
||||
if (launchPath.indexOf('#') !== -1) {
|
||||
var hashParts = launchPath.split('#');
|
||||
launchPath = hashParts[0];
|
||||
urlParts.hash = hashParts[1];
|
||||
}
|
||||
|
||||
urlParts.pathname = fsPath.join(urlParts.pathname || '/', launchPath);
|
||||
return url.format(urlParts);
|
||||
}
|
||||
|
||||
function sourceForActivity(app, activityHref) {
|
||||
var origin = app.origin;
|
||||
var activities = app.manifest.activities;
|
||||
var isHrefValid = false;
|
||||
|
||||
for (var key in activities) {
|
||||
if (activities[key].href === activityHref) {
|
||||
isHrefValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isHrefValid) {
|
||||
throw new Error(origin + ' does not contain the href ' + activityHref);
|
||||
}
|
||||
|
||||
return origin + activityHref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a given app by its origin and optionally an entrypoint.
|
||||
*
|
||||
* @param {Object} state current app state.
|
||||
* @param {String} origin of the app.
|
||||
* @param {String} [entrypoint] of the app.
|
||||
* @param {Function} callback [Error, App].
|
||||
*/
|
||||
function getApp(state, origin, entrypoint, callback) {
|
||||
if (typeof entrypoint === 'function') {
|
||||
callback = entrypoint;
|
||||
entrypoint = null;
|
||||
}
|
||||
|
||||
callback = callback || state._client.defaultCallback;
|
||||
|
||||
return getAppWithCheckOnOriginOnly(state, origin, function(err, originApp) {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
originApp.source = origin;
|
||||
|
||||
if (entrypoint) {
|
||||
// verify this is a valid entrypoint
|
||||
var details = originApp.manifest.entry_points[entrypoint];
|
||||
if (!details) {
|
||||
return callback(new Error('invalid entrypoint "' + entrypoint + '"'));
|
||||
}
|
||||
|
||||
// add sugar for the apps entrypoint instance.
|
||||
originApp = Object.create(originApp);
|
||||
|
||||
originApp.entrypoint = {
|
||||
name: entrypoint,
|
||||
details: details
|
||||
};
|
||||
|
||||
// must come after the entrypoint assignment
|
||||
originApp.source = sourceForEntrypoint(originApp);
|
||||
}
|
||||
|
||||
return callback(null, originApp);
|
||||
});
|
||||
}
|
||||
|
||||
function getActivity(state, origin, activityHref, callback) {
|
||||
if (typeof activityHref === 'function') {
|
||||
callback = activityHref;
|
||||
activityHref = null;
|
||||
}
|
||||
|
||||
return getAppWithCheckOnOriginOnly(state, origin, function(err, originApp) {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
originApp.source = sourceForActivity(originApp, activityHref);
|
||||
return callback(null, originApp);
|
||||
});
|
||||
}
|
||||
|
||||
function getAppWithCheckOnOriginOnly(state, origin, callback) {
|
||||
callback = callback || state._client.defaultCallback;
|
||||
|
||||
var originApp;
|
||||
return list(state, function(err, apps) {
|
||||
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
for (var i = 0, len = apps.length; i < len; i++) {
|
||||
if (apps[i].origin === origin) {
|
||||
originApp = apps[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!originApp) {
|
||||
return callback(
|
||||
new Error('could not find an app with the origin: "' + origin + '"')
|
||||
);
|
||||
}
|
||||
|
||||
originApp.source = origin;
|
||||
return callback(null, originApp);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.getApp = getApp;
|
||||
module.exports.getActivity = getActivity;
|
||||
module.exports.sourceForEntrypoint = sourceForEntrypoint;
|
||||
16
gaia/tests/jsmarionette/plugins/marionette-apps/lib/getself.js
Executable file
16
gaia/tests/jsmarionette/plugins/marionette-apps/lib/getself.js
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Returns information about the current app.
|
||||
*
|
||||
* apps.getSelf(apps, function(err, app) {
|
||||
* app.origin; // origin of a given app
|
||||
* });
|
||||
*
|
||||
* @param {Apps} apps state.
|
||||
* @param {Function} callback [Err err, <App> app].
|
||||
*/
|
||||
function getSelf(apps, callback) {
|
||||
return apps.getSelf(callback);
|
||||
}
|
||||
|
||||
module.exports.getSelf = getSelf;
|
||||
49
gaia/tests/jsmarionette/plugins/marionette-apps/lib/launch.js
Executable file
49
gaia/tests/jsmarionette/plugins/marionette-apps/lib/launch.js
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
var getApp = require('./getapp').getApp;
|
||||
var waitForApp = require('./waitforapp').waitForApp;
|
||||
|
||||
/**
|
||||
* Launch an application based on its origin and optionally entrypoint.
|
||||
* Will wait until app's iframe is visible before firing callback.
|
||||
*
|
||||
* launch(apps, 'app://calendar.gaiamobile.org', function(err, app) {
|
||||
* // yey
|
||||
* });
|
||||
*
|
||||
* @param {Apps} apps instance.
|
||||
* @param {String} origin of the app.
|
||||
* @param {String} [entrypoint] for the app.
|
||||
* @param {Function} callback [Error err, App app].
|
||||
*/
|
||||
function launch(apps, origin, entrypoint, callback) {
|
||||
if (typeof entrypoint === 'function') {
|
||||
callback = entrypoint;
|
||||
entrypoint = null;
|
||||
}
|
||||
|
||||
callback = callback || apps._client.defaultCallback;
|
||||
|
||||
// Wait for the system to be fully loaded.
|
||||
var client = apps._client;
|
||||
var body = client.findElement('body');
|
||||
client.waitFor(function() {
|
||||
return body.getAttribute('ready-state') == 'fullyLoaded';
|
||||
}, { timeout: 60000 });
|
||||
|
||||
// launch the given app
|
||||
return getApp(apps, origin, entrypoint, function(err, app) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// if a null entrypoint is given it is safely ignored.
|
||||
app.launch(entrypoint);
|
||||
|
||||
// wait for this app to be visible
|
||||
return waitForApp(apps, app.source, function(err, element) {
|
||||
return callback(err, app, element);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.launch = launch;
|
||||
18
gaia/tests/jsmarionette/plugins/marionette-apps/lib/list.js
Executable file
18
gaia/tests/jsmarionette/plugins/marionette-apps/lib/list.js
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Find all apps.
|
||||
*
|
||||
* apps.list(apps, function(err, list) {
|
||||
* // list is an array of "Apps"
|
||||
* list[0].origin; // origin of a given app
|
||||
* });
|
||||
*
|
||||
* @param {Apps} apps state.
|
||||
* @param {Function} callback [Err err, Array<App> apps].
|
||||
* @return {Array}
|
||||
*/
|
||||
function list(apps, callback) {
|
||||
return apps.mgmt.getAll(callback);
|
||||
}
|
||||
|
||||
module.exports.list = list;
|
||||
107
gaia/tests/jsmarionette/plugins/marionette-apps/lib/mgmt.js
Executable file
107
gaia/tests/jsmarionette/plugins/marionette-apps/lib/mgmt.js
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
'use strict';
|
||||
var App = require(__dirname + '/app'),
|
||||
fs = require('fs');
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Marionette.Client} client Marionette client to use.
|
||||
*/
|
||||
function Mgmt(client) {
|
||||
this._client = client;
|
||||
}
|
||||
module.exports = Mgmt;
|
||||
|
||||
|
||||
Mgmt.prototype = {
|
||||
/**
|
||||
* @type {Marionette.Client}
|
||||
* @private
|
||||
*/
|
||||
_client: undefined,
|
||||
|
||||
_formatApp: function(client, data) {
|
||||
var app = new App(client);
|
||||
for (var key in data) {
|
||||
app[key] = data[key];
|
||||
}
|
||||
return app;
|
||||
},
|
||||
|
||||
/**
|
||||
* List all installed apps in the user's repository.
|
||||
*
|
||||
* @param {Function} callback [Error, Array<App>].
|
||||
*/
|
||||
getAll: function(callback) {
|
||||
callback = callback || this._client.defaultCallback;
|
||||
|
||||
var script = fs.readFileSync(
|
||||
__dirname + '/scripts/getallapps.js',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
var client = this._client.scope({ context: 'content' });
|
||||
var format = this._formatApp.bind(this, client);
|
||||
|
||||
return client.executeAsyncScript(script, function(err, operation) {
|
||||
// handle scripting error
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// handle error from operation
|
||||
if (operation.error) {
|
||||
return callback(new Error(operation.error));
|
||||
}
|
||||
|
||||
// success format the apps
|
||||
var apps = operation.result.map(format);
|
||||
return callback(null, apps);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns information about the calling app, if any.
|
||||
* @param {Function} callback [Error, Array<App>].
|
||||
*/
|
||||
getSelf: function(callback) {
|
||||
callback = callback || this._client.defaultCallback;
|
||||
|
||||
var script = fs.readFileSync(
|
||||
__dirname + '/scripts/getself.js',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
var client = this._client.scope({ context: 'content' });
|
||||
var format = this._formatApp.bind(this, client);
|
||||
|
||||
return client.executeAsyncScript(script, function(err, operation) {
|
||||
// handle scripting error
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// handle error from operation
|
||||
if (operation.error) {
|
||||
return callback(new Error(operation.error));
|
||||
}
|
||||
|
||||
// success format the app
|
||||
return callback(null, operation.result ? format(operation.result) : null);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Inject utility functions into gecko through the marionette client.
|
||||
* @param {Function} cb Optional callback function.
|
||||
*/
|
||||
prepareClient: function(cb) {
|
||||
var script = fs.readFileSync(
|
||||
__dirname + '/scripts/objectcache.js',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
this._client.importScript(script, cb);
|
||||
}
|
||||
};
|
||||
48
gaia/tests/jsmarionette/plugins/marionette-apps/lib/scripts/getallapps.js
Executable file
48
gaia/tests/jsmarionette/plugins/marionette-apps/lib/scripts/getallapps.js
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Take an app from a DOMRequest and serialize it to send back
|
||||
* to the client.
|
||||
* @return {Object} App representation.
|
||||
*/
|
||||
function serialize(app) {
|
||||
return {
|
||||
installOrigin: app.installOrigin,
|
||||
installTime: app.installTime,
|
||||
// sync transfer of the entire manifest is slow so for now opt in to the
|
||||
// fields we actually need internally.
|
||||
manifest: {
|
||||
entry_points: app.manifest.entry_points,
|
||||
activities: app.manifest.activities
|
||||
},
|
||||
manifestURL: app.manifestURL,
|
||||
origin: app.origin,
|
||||
receipts: app.receipts
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
var ObjectCache = window.wrappedJSObject.ObjectCache;
|
||||
|
||||
var req = navigator.mozApps.mgmt.getAll();
|
||||
req.onsuccess = function(evt) {
|
||||
var result = evt.target.result;
|
||||
var apps = result.map(function(app) {
|
||||
var id = ObjectCache._inst.set(app);
|
||||
var obj = serialize(app);
|
||||
obj._id = id;
|
||||
return obj;
|
||||
});
|
||||
|
||||
marionetteScriptFinished({
|
||||
result: apps
|
||||
});
|
||||
};
|
||||
|
||||
req.onerror = function(evt) {
|
||||
marionetteScriptFinished({
|
||||
error: req.error.name
|
||||
});
|
||||
};
|
||||
})();
|
||||
42
gaia/tests/jsmarionette/plugins/marionette-apps/lib/scripts/getself.js
Executable file
42
gaia/tests/jsmarionette/plugins/marionette-apps/lib/scripts/getself.js
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
(function() {
|
||||
|
||||
/* global marionetteScriptFinished */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Take an app from a DOMRequest and serialize it to send back
|
||||
* to the client.
|
||||
* @return {Object} App representation.
|
||||
*/
|
||||
function serialize(app) {
|
||||
return {
|
||||
installOrigin: app.installOrigin,
|
||||
installTime: app.installTime,
|
||||
// sync transfer of the entire manifest is slow so for now opt in to the
|
||||
// fields we actually need internally.
|
||||
manifest: {
|
||||
entry_points: app.manifest.entry_points
|
||||
},
|
||||
manifestURL: app.manifestURL,
|
||||
origin: app.origin,
|
||||
receipts: app.receipts
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
var ObjectCache = window.wrappedJSObject.ObjectCache;
|
||||
|
||||
var req = navigator.mozApps.getSelf();
|
||||
req.onsuccess = function(evt) {
|
||||
var app = evt.target.result;
|
||||
var id = ObjectCache._inst.set(app);
|
||||
var obj = serialize(app);
|
||||
obj._id = id;
|
||||
marionetteScriptFinished({ result: obj });
|
||||
};
|
||||
|
||||
req.onerror = function(evt) {
|
||||
marionetteScriptFinished({ error: req.error.name });
|
||||
};
|
||||
})();
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue