Bug 1376128 - Lint testing/marionette; r=automatedtester

MozReview-Commit-ID: DY4yCSBEZrN

--HG--
extra : rebase_source : d4e25369418cc72a6ee9f78d44b050a87403391d
This commit is contained in:
Andreas Tolfsen 2017-06-29 16:40:24 -07:00
parent 32d341c354
commit de001d80b7
30 changed files with 1686 additions and 1345 deletions

View File

@ -6,7 +6,7 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components; const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import('resource://gre/modules/Services.jsm'); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://gre/modules/Log.jsm");
@ -35,15 +35,16 @@ this.EXPORTED_SYMBOLS = ["accessibility"];
this.accessibility = { this.accessibility = {
get service() { get service() {
return service; return service;
} },
}; };
/** /**
* Accessible states used to check element"s state from the accessiblity API * Accessible states used to check element"s state from the accessiblity API
* perspective. * perspective.
* Note: if gecko is built with --disable-accessibility, the interfaces are not *
* defined. This is why we use getters instead to be able to use these * Note: if gecko is built with --disable-accessibility, the interfaces
* statically. * are not defined. This is why we use getters instead to be able to use
* these statically.
*/ */
accessibility.State = { accessibility.State = {
get Unavailable() { get Unavailable() {
@ -57,7 +58,7 @@ accessibility.State = {
}, },
get Selected() { get Selected() {
return Ci.nsIAccessibleStates.STATE_SELECTED; return Ci.nsIAccessibleStates.STATE_SELECTED;
} },
}; };
/** /**
@ -92,7 +93,7 @@ accessibility.ActionableRoles = new Set([
* Factory function that constructs a new {@code accessibility.Checks} * Factory function that constructs a new {@code accessibility.Checks}
* object with enforced strictness or not. * object with enforced strictness or not.
*/ */
accessibility.get = function (strict = false) { accessibility.get = function(strict = false) {
return new accessibility.Checks(!!strict); return new accessibility.Checks(!!strict);
}; };
@ -138,7 +139,8 @@ accessibility.Checks = class {
} }
// First, check if accessibility is ready. // First, check if accessibility is ready.
let docAcc = accessibility.service.getAccessibleFor(element.ownerDocument); let docAcc = accessibility.service
.getAccessibleFor(element.ownerDocument);
let state = {}; let state = {};
docAcc.getState(state, {}); docAcc.getState(state, {});
if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) { if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) {
@ -158,12 +160,14 @@ accessibility.Checks = class {
return; return;
} }
let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
// If event type does not match expected type, skip the event. // If event type does not match expected type, skip the event.
let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) { if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
return; return;
} }
// If event's accessible does not match expected accessible, skip the event.
// If event's accessible does not match expected accessible,
// skip the event.
if (event.accessible !== docAcc) { if (event.accessible !== docAcc) {
return; return;
} }
@ -175,12 +179,12 @@ accessibility.Checks = class {
} else { } else {
resolve(acc); resolve(acc);
} }
} },
}; };
Services.obs.addObserver(eventObserver, "accessible-event"); Services.obs.addObserver(eventObserver, "accessible-event");
}).catch(() => this.error( }).catch(() => this.error(
"Element does not have an accessible object", element)); "Element does not have an accessible object", element));
}; }
/** /**
* Test if the accessible has a role that supports some arbitrary * Test if the accessible has a role that supports some arbitrary
@ -409,7 +413,8 @@ accessibility.Checks = class {
return; return;
} }
let selectedAccessibility = this.matchState(accessible, accessibility.State.Selected); let selectedAccessibility =
this.matchState(accessible, accessibility.State.Selected);
let message; let message;
if (selected && !selectedAccessibility) { if (selected && !selectedAccessibility) {

View File

@ -23,6 +23,8 @@ Cu.import("chrome://marionette/content/interaction.js");
this.EXPORTED_SYMBOLS = ["action"]; this.EXPORTED_SYMBOLS = ["action"];
const {pprint} = error;
// TODO? With ES 2016 and Symbol you can make a safer approximation // TODO? With ES 2016 and Symbol you can make a safer approximation
// to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689 // to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689
/** /**
@ -360,7 +362,7 @@ action.PointerOrigin.get = function(obj) {
origin = this.Viewport; origin = this.Viewport;
} else if (typeof obj == "string") { } else if (typeof obj == "string") {
let name = capitalize(obj); let name = capitalize(obj);
assert.in(name, this, error.pprint`Unknown pointer-move origin: ${obj}`); assert.in(name, this, pprint`Unknown pointer-move origin: ${obj}`);
origin = this[name]; origin = this[name];
} else if (!element.isWebElementReference(obj)) { } else if (!element.isWebElementReference(obj)) {
throw new InvalidArgumentError("Expected 'origin' to be a string or a " + throw new InvalidArgumentError("Expected 'origin' to be a string or a " +
@ -389,24 +391,25 @@ action.PointerType = {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |str| is not a valid pointer type. * If |str| is not a valid pointer type.
*/ */
action.PointerType.get = function (str) { action.PointerType.get = function(str) {
let name = capitalize(str); let name = capitalize(str);
assert.in(name, this, error.pprint`Unknown pointerType: ${str}`); assert.in(name, this, pprint`Unknown pointerType: ${str}`);
return this[name]; return this[name];
}; };
/** /**
* Input state associated with current session. This is a map between input ID and * Input state associated with current session. This is a map between
* the device state for that input source, with one entry for each active input source. * input ID and the device state for that input source, with one entry
* for each active input source.
* *
* Initialized in listener.js * Initialized in listener.js.
*/ */
action.inputStateMap = undefined; action.inputStateMap = undefined;
/** /**
* List of |action.Action| associated with current session. Used to manage dispatching * List of |action.Action| associated with current session. Used to
* events when resetting the state of the input sources. Reset operations are assumed * manage dispatching events when resetting the state of the input sources.
* to be idempotent. * Reset operations are assumed to be idempotent.
* *
* Initialized in listener.js * Initialized in listener.js
*/ */
@ -441,8 +444,8 @@ class InputState {
/** /**
* @param {?} obj * @param {?} obj
* Object with property |type| and optionally |parameters| or |pointerType|, * Object with property |type| and optionally |parameters| or
* representing an action sequence or an action item. * |pointerType|, representing an action sequence or an action item.
* *
* @return {action.InputState} * @return {action.InputState}
* An |action.InputState| object for the type of the |actionSequence|. * An |action.InputState| object for the type of the |actionSequence|.
@ -452,18 +455,18 @@ class InputState {
*/ */
static fromJson(obj) { static fromJson(obj) {
let type = obj.type; let type = obj.type;
assert.in(type, ACTIONS, error.pprint`Unknown action type: ${type}`); assert.in(type, ACTIONS, pprint`Unknown action type: ${type}`);
let name = type == "none" ? "Null" : capitalize(type); let name = type == "none" ? "Null" : capitalize(type);
if (name == "Pointer") { if (name == "Pointer") {
if (!obj.pointerType && (!obj.parameters || !obj.parameters.pointerType)) { if (!obj.pointerType &&
(!obj.parameters || !obj.parameters.pointerType)) {
throw new InvalidArgumentError( throw new InvalidArgumentError(
error.pprint`Expected obj to have pointerType, got: ${obj}`); pprint`Expected obj to have pointerType, got: ${obj}`);
} }
let pointerType = obj.pointerType || obj.parameters.pointerType; let pointerType = obj.pointerType || obj.parameters.pointerType;
return new action.InputState[name](pointerType); return new action.InputState[name](pointerType);
} else {
return new action.InputState[name]();
} }
return new action.InputState[name]();
} }
} }
@ -566,7 +569,8 @@ action.InputState.Pointer = class Pointer extends InputState {
constructor(subtype) { constructor(subtype) {
super(); super();
this.pressed = new Set(); this.pressed = new Set();
assert.defined(subtype, error.pprint`Expected subtype to be defined, got: ${subtype}`); assert.defined(subtype,
pprint`Expected subtype to be defined, got: ${subtype}`);
this.subtype = action.PointerType.get(subtype); this.subtype = action.PointerType.get(subtype);
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
@ -616,14 +620,16 @@ action.InputState.Pointer = class Pointer extends InputState {
}; };
/** /**
* Repesents an action for dispatch. Used in |action.Chain| and |action.Sequence|. * Repesents an action for dispatch. Used in |action.Chain| and
* |action.Sequence|.
* *
* @param {string} id * @param {string} id
* Input source ID. * Input source ID.
* @param {string} type * @param {string} type
* Action type: none, key, pointer. * Action type: none, key, pointer.
* @param {string} subtype * @param {string} subtype
* Action subtype: pause, keyUp, keyDown, pointerUp, pointerDown, pointerMove, pointerCancel. * Action subtype: pause, keyUp, keyDown, pointerUp, pointerDown,
* pointerMove, pointerCancel.
* *
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If any parameters are undefined. * If any parameters are undefined.
@ -634,12 +640,12 @@ action.Action = class {
throw new InvalidArgumentError("Missing id, type or subtype"); throw new InvalidArgumentError("Missing id, type or subtype");
} }
for (let attr of [id, type, subtype]) { for (let attr of [id, type, subtype]) {
assert.string(attr, error.pprint`Expected string, got: ${attr}`); assert.string(attr, pprint`Expected string, got: ${attr}`);
} }
this.id = id; this.id = id;
this.type = type; this.type = type;
this.subtype = subtype; this.subtype = subtype;
}; }
toString() { toString() {
return `[action ${this.type}]`; return `[action ${this.type}]`;
@ -668,7 +674,8 @@ action.Action = class {
} }
let subtype = actionItem.type; let subtype = actionItem.type;
if (!subtypes.has(subtype)) { if (!subtypes.has(subtype)) {
throw new InvalidArgumentError(`Unknown subtype for ${type} action: ${subtype}`); throw new InvalidArgumentError(
`Unknown subtype for ${type} action: ${subtype}`);
} }
let item = new action.Action(id, type, subtype); let item = new action.Action(id, type, subtype);
@ -682,10 +689,10 @@ action.Action = class {
case action.KeyDown: case action.KeyDown:
let key = actionItem.value; let key = actionItem.value;
// TODO countGraphemes // TODO countGraphemes
// TODO key.value could be a single code point like "\uE012" (see rawKey) // TODO key.value could be a single code point like "\uE012"
// or "grapheme cluster" // (see rawKey) or "grapheme cluster"
assert.string(key, assert.string(key,
error.pprint("Expected 'value' to be a string that represents single code point " + pprint("Expected 'value' to be a string that represents single code point " +
`or grapheme cluster, got: ${key}`)); `or grapheme cluster, got: ${key}`));
item.value = key; item.value = key;
break; break;
@ -693,36 +700,38 @@ action.Action = class {
case action.PointerDown: case action.PointerDown:
case action.PointerUp: case action.PointerUp:
assert.positiveInteger(actionItem.button, assert.positiveInteger(actionItem.button,
error.pprint`Expected 'button' (${actionItem.button}) to be >= 0`); pprint`Expected 'button' (${actionItem.button}) to be >= 0`);
item.button = actionItem.button; item.button = actionItem.button;
break; break;
case action.PointerMove: case action.PointerMove:
item.duration = actionItem.duration; item.duration = actionItem.duration;
if (typeof item.duration != "undefined"){ if (typeof item.duration != "undefined") {
assert.positiveInteger(item.duration, assert.positiveInteger(item.duration,
error.pprint`Expected 'duration' (${item.duration}) to be >= 0`); pprint`Expected 'duration' (${item.duration}) to be >= 0`);
} }
item.origin = action.PointerOrigin.get(actionItem.origin); item.origin = action.PointerOrigin.get(actionItem.origin);
item.x = actionItem.x; item.x = actionItem.x;
if (typeof item.x != "undefined") { if (typeof item.x != "undefined") {
assert.integer(item.x, error.pprint`Expected 'x' (${item.x}) to be an Integer`); assert.integer(item.x,
pprint`Expected 'x' (${item.x}) to be an Integer`);
} }
item.y = actionItem.y; item.y = actionItem.y;
if (typeof item.y != "undefined") { if (typeof item.y != "undefined") {
assert.integer(item.y, error.pprint`Expected 'y' (${item.y}) to be an Integer`); assert.integer(item.y,
pprint`Expected 'y' (${item.y}) to be an Integer`);
} }
break; break;
case action.PointerCancel: case action.PointerCancel:
throw new UnsupportedOperationError(); throw new UnsupportedOperationError();
break;
case action.Pause: case action.Pause:
item.duration = actionItem.duration; item.duration = actionItem.duration;
if (typeof item.duration != "undefined") { if (typeof item.duration != "undefined") {
// eslint-disable-next-line
assert.positiveInteger(item.duration, assert.positiveInteger(item.duration,
error.pprint`Expected 'duration' (${item.duration}) to be >= 0`); pprint`Expected 'duration' (${item.duration}) to be >= 0`);
} }
break; break;
} }
@ -732,7 +741,8 @@ action.Action = class {
}; };
/** /**
* Represents a series of ticks, specifying which actions to perform at each tick. * Represents a series of ticks, specifying which actions to perform at
* each tick.
*/ */
action.Chain = class extends Array { action.Chain = class extends Array {
toString() { toString() {
@ -744,17 +754,18 @@ action.Chain = class extends Array {
* Array of objects that each represent an action sequence. * Array of objects that each represent an action sequence.
* *
* @return {action.Chain} * @return {action.Chain}
* Transpose of |actions| such that actions to be performed in a single tick * Transpose of |actions| such that actions to be performed in a
* are grouped together. * single tick are grouped together.
* *
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |actions| is not an Array. * If |actions| is not an Array.
*/ */
static fromJson(actions) { static fromJson(actions) {
assert.array(actions, assert.array(actions,
error.pprint`Expected 'actions' to be an Array, got: ${actions}`); pprint`Expected 'actions' to be an Array, got: ${actions}`);
let actionsByTick = new action.Chain(); let actionsByTick = new action.Chain();
// TODO check that each actionSequence in actions refers to a different input ID // TODO check that each actionSequence in actions refers to a
// different input ID
for (let actionSequence of actions) { for (let actionSequence of actions) {
let inputSourceActions = action.Sequence.fromJson(actionSequence); let inputSourceActions = action.Sequence.fromJson(actionSequence);
for (let i = 0; i < inputSourceActions.length; i++) { for (let i = 0; i < inputSourceActions.length; i++) {
@ -770,7 +781,8 @@ action.Chain = class extends Array {
}; };
/** /**
* Represents one input source action sequence; this is essentially an |Array.<action.Action>|. * Represents one input source action sequence; this is essentially an
* |Array.<action.Action>|.
*/ */
action.Sequence = class extends Array { action.Sequence = class extends Array {
toString() { toString() {
@ -794,10 +806,10 @@ action.Sequence = class extends Array {
let inputSourceState = InputState.fromJson(actionSequence); let inputSourceState = InputState.fromJson(actionSequence);
let id = actionSequence.id; let id = actionSequence.id;
assert.defined(id, "Expected 'id' to be defined"); assert.defined(id, "Expected 'id' to be defined");
assert.string(id, error.pprint`Expected 'id' to be a string, got: ${id}`); assert.string(id, pprint`Expected 'id' to be a string, got: ${id}`);
let actionItems = actionSequence.actions; let actionItems = actionSequence.actions;
assert.array(actionItems, assert.array(actionItems,
error.pprint("Expected 'actionSequence.actions' to be an Array, " + pprint("Expected 'actionSequence.actions' to be an Array, " +
`got: ${actionSequence.actions}`)); `got: ${actionSequence.actions}`));
if (!action.inputStateMap.has(id)) { if (!action.inputStateMap.has(id)) {
action.inputStateMap.set(id, inputSourceState); action.inputStateMap.set(id, inputSourceState);
@ -818,7 +830,8 @@ action.Sequence = class extends Array {
* Represents parameters in an action for a pointer input source. * Represents parameters in an action for a pointer input source.
* *
* @param {string=} pointerType * @param {string=} pointerType
* Type of pointing device. If the parameter is undefined, "mouse" is used. * Type of pointing device. If the parameter is undefined, "mouse"
* is used.
*/ */
action.PointerParameters = class { action.PointerParameters = class {
constructor(pointerType = "mouse") { constructor(pointerType = "mouse") {
@ -839,9 +852,8 @@ action.PointerParameters = class {
static fromJson(parametersData) { static fromJson(parametersData) {
if (typeof parametersData == "undefined") { if (typeof parametersData == "undefined") {
return new action.PointerParameters(); return new action.PointerParameters();
} else {
return new action.PointerParameters(parametersData.pointerType);
} }
return new action.PointerParameters(parametersData.pointerType);
} }
}; };
@ -860,14 +872,16 @@ action.PointerParameters = class {
* If |id| is already mapped to an |action.InputState| that is * If |id| is already mapped to an |action.InputState| that is
* not compatible with |act.type| or |pointerParams.pointerType|. * not compatible with |act.type| or |pointerParams.pointerType|.
*/ */
action.processPointerAction = function processPointerAction(id, pointerParams, act) { action.processPointerAction = function(id, pointerParams, act) {
if (action.inputStateMap.has(id) && action.inputStateMap.get(id).type !== act.type) { if (action.inputStateMap.has(id) &&
action.inputStateMap.get(id).type !== act.type) {
throw new InvalidArgumentError( throw new InvalidArgumentError(
`Expected 'id' ${id} to be mapped to InputState whose type is ` + `Expected 'id' ${id} to be mapped to InputState whose type is ` +
`${action.inputStateMap.get(id).type}, got: ${act.type}`); `${action.inputStateMap.get(id).type}, got: ${act.type}`);
} }
let pointerType = pointerParams.pointerType; let pointerType = pointerParams.pointerType;
if (action.inputStateMap.has(id) && action.inputStateMap.get(id).subtype !== pointerType) { if (action.inputStateMap.has(id) &&
action.inputStateMap.get(id).subtype !== pointerType) {
throw new InvalidArgumentError( throw new InvalidArgumentError(
`Expected 'id' ${id} to be mapped to InputState whose subtype is ` + `Expected 'id' ${id} to be mapped to InputState whose subtype is ` +
`${action.inputStateMap.get(id).subtype}, got: ${pointerType}`); `${action.inputStateMap.get(id).subtype}, got: ${pointerType}`);
@ -930,9 +944,10 @@ action.Mouse = class {
/** /**
* Dispatch a chain of actions over |chain.length| ticks. * Dispatch a chain of actions over |chain.length| ticks.
* *
* This is done by creating a Promise for each tick that resolves once all the * This is done by creating a Promise for each tick that resolves once
* Promises for individual tick-actions are resolved. The next tick's actions are * all the Promises for individual tick-actions are resolved. The next
* not dispatched until the Promise for the current tick is resolved. * tick's actions are not dispatched until the Promise for the current
* tick is resolved.
* *
* @param {action.Chain} chain * @param {action.Chain} chain
* Actions grouped by tick; each element in |chain| is a sequence of * Actions grouped by tick; each element in |chain| is a sequence of
@ -949,7 +964,10 @@ action.dispatch = function(chain, seenEls, container) {
let chainEvents = Task.spawn(function*() { let chainEvents = Task.spawn(function*() {
for (let tickActions of chain) { for (let tickActions of chain) {
yield action.dispatchTickActions( yield action.dispatchTickActions(
tickActions, action.computeTickDuration(tickActions), seenEls, container); tickActions,
action.computeTickDuration(tickActions),
seenEls,
container);
} }
}); });
return chainEvents; return chainEvents;
@ -958,12 +976,13 @@ action.dispatch = function(chain, seenEls, container) {
/** /**
* Dispatch sequence of actions for one tick. * Dispatch sequence of actions for one tick.
* *
* This creates a Promise for one tick that resolves once the Promise for each * This creates a Promise for one tick that resolves once the Promise
* tick-action is resolved, which takes at least |tickDuration| milliseconds. * for each tick-action is resolved, which takes at least |tickDuration|
* The resolved set of events for each tick is followed by firing of pending DOM events. * milliseconds. The resolved set of events for each tick is followed by
* firing of pending DOM events.
* *
* Note that the tick-actions are dispatched in order, but they may have different * Note that the tick-actions are dispatched in order, but they may have
* durations and therefore may not end in the same order. * different durations and therefore may not end in the same order.
* *
* @param {Array.<action.Action>} tickActions * @param {Array.<action.Action>} tickActions
* List of actions for one tick. * List of actions for one tick.
@ -977,8 +996,10 @@ action.dispatch = function(chain, seenEls, container) {
* @return {Promise} * @return {Promise}
* Promise for dispatching all tick-actions and pending DOM events. * Promise for dispatching all tick-actions and pending DOM events.
*/ */
action.dispatchTickActions = function(tickActions, tickDuration, seenEls, container) { action.dispatchTickActions = function(
let pendingEvents = tickActions.map(toEvents(tickDuration, seenEls, container)); tickActions, tickDuration, seenEls, container) {
let pendingEvents = tickActions.map(
toEvents(tickDuration, seenEls, container));
return Promise.all(pendingEvents).then( return Promise.all(pendingEvents).then(
() => interaction.flushEventLoop(container.frame)); () => interaction.flushEventLoop(container.frame));
}; };
@ -1018,7 +1039,8 @@ action.computeTickDuration = function(tickActions) {
* @return {Map.<string, number>} * @return {Map.<string, number>}
* x and y coordinates of pointer destination. * x and y coordinates of pointer destination.
*/ */
action.computePointerDestination = function(a, inputState, center = undefined) { action.computePointerDestination = function(
a, inputState, center = undefined) {
let {x, y} = a; let {x, y} = a;
switch (a.origin) { switch (a.origin) {
case action.PointerOrigin.Viewport: case action.PointerOrigin.Viewport:
@ -1053,7 +1075,7 @@ action.computePointerDestination = function(a, inputState, center = undefined) {
* the event that corresponds to that action. * the event that corresponds to that action.
*/ */
function toEvents(tickDuration, seenEls, container) { function toEvents(tickDuration, seenEls, container) {
return function (a) { return a => {
let inputState = action.inputStateMap.get(a.id); let inputState = action.inputStateMap.get(a.id);
switch (a.subtype) { switch (a.subtype) {
case action.KeyUp: case action.KeyUp:
@ -1069,7 +1091,8 @@ function toEvents(tickDuration, seenEls, container) {
return dispatchPointerUp(a, inputState, container.frame); return dispatchPointerUp(a, inputState, container.frame);
case action.PointerMove: case action.PointerMove:
return dispatchPointerMove(a, inputState, tickDuration, seenEls, container); return dispatchPointerMove(
a, inputState, tickDuration, seenEls, container);
case action.PointerCancel: case action.PointerCancel:
throw new UnsupportedOperationError(); throw new UnsupportedOperationError();
@ -1077,6 +1100,7 @@ function toEvents(tickDuration, seenEls, container) {
case action.Pause: case action.Pause:
return dispatchPause(a, tickDuration); return dispatchPause(a, tickDuration);
} }
return undefined;
}; };
} }
@ -1091,7 +1115,8 @@ function toEvents(tickDuration, seenEls, container) {
* Current window. * Current window.
* *
* @return {Promise} * @return {Promise}
* Promise to dispatch at least a keydown event, and keypress if appropriate. * Promise to dispatch at least a keydown event, and keypress if
* appropriate.
*/ */
function dispatchKeyDown(a, inputState, win) { function dispatchKeyDown(a, inputState, win) {
return new Promise(resolve => { return new Promise(resolve => {
@ -1161,22 +1186,31 @@ function dispatchPointerDown(a, inputState, win) {
resolve(); resolve();
return; return;
} }
inputState.press(a.button); inputState.press(a.button);
// Append a copy of |a| with pointerUp subtype // Append a copy of |a| with pointerUp subtype
action.inputsToCancel.push(Object.assign({}, a, {subtype: action.PointerUp})); let copy = Object.assign({}, a, {subtype: action.PointerUp});
action.inputsToCancel.push(copy);
switch (inputState.subtype) { switch (inputState.subtype) {
case action.PointerType.Mouse: case action.PointerType.Mouse:
let mouseEvent = new action.Mouse("mousedown", a.button); let mouseEvent = new action.Mouse("mousedown", a.button);
mouseEvent.update(inputState); mouseEvent.update(inputState);
event.synthesizeMouseAtPoint(inputState.x, inputState.y, mouseEvent, win); event.synthesizeMouseAtPoint(
inputState.x,
inputState.y,
mouseEvent,
win);
break; break;
case action.PointerType.Pen: case action.PointerType.Pen:
case action.PointerType.Touch: case action.PointerType.Touch:
throw new UnsupportedOperationError("Only 'mouse' pointer type is supported"); throw new UnsupportedOperationError("Only 'mouse' pointer type is supported");
break;
default: default:
throw new TypeError(`Unknown pointer type: ${inputState.subtype}`); throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
} }
resolve(); resolve();
}); });
} }
@ -1220,12 +1254,13 @@ function dispatchPointerUp(a, inputState, win) {
} }
/** /**
* Dispatch a pointerMove action equivalent to moving pointer device in a line. * Dispatch a pointerMove action equivalent to moving pointer device in
* a line.
* *
* If the action duration is 0, the pointer jumps immediately to the target coordinates. * If the action duration is 0, the pointer jumps immediately to the
* Otherwise, events are synthesized to mimic a pointer travelling in a discontinuous, * target coordinates. Otherwise, events are synthesized to mimic a
* approximately straight line, with the pointer coordinates being updated around 60 * pointer travelling in a discontinuous, approximately straight line,
* times per second. * with the pointer coordinates being updated around 60 times per second.
* *
* @param {action.Action} a * @param {action.Action} a
* Action to dispatch. * Action to dispatch.
@ -1237,10 +1272,11 @@ function dispatchPointerUp(a, inputState, win) {
* Object with |frame| attribute of type |nsIDOMWindow|. * Object with |frame| attribute of type |nsIDOMWindow|.
* *
* @return {Promise} * @return {Promise}
* Promise to dispatch at least one pointermove event, as well as mousemove events * Promise to dispatch at least one pointermove event, as well as
* as appropriate. * mousemove events as appropriate.
*/ */
function dispatchPointerMove(a, inputState, tickDuration, seenEls, container) { function dispatchPointerMove(
a, inputState, tickDuration, seenEls, container) {
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
// interval between pointermove increments in ms, based on common vsync // interval between pointermove increments in ms, based on common vsync
const fps60 = 17; const fps60 = 17;
@ -1253,7 +1289,8 @@ function dispatchPointerMove(a, inputState, tickDuration, seenEls, container) {
if (!inViewPort(targetX, targetY, container.frame)) { if (!inViewPort(targetX, targetY, container.frame)) {
throw new MoveTargetOutOfBoundsError( throw new MoveTargetOutOfBoundsError(
`(${targetX}, ${targetY}) is out of bounds of viewport ` + `(${targetX}, ${targetY}) is out of bounds of viewport ` +
`width (${container.frame.innerWidth}) and height (${container.frame.innerHeight})`); `width (${container.frame.innerWidth}) ` +
`and height (${container.frame.innerHeight})`);
} }
const duration = typeof a.duration == "undefined" ? tickDuration : a.duration; const duration = typeof a.duration == "undefined" ? tickDuration : a.duration;
@ -1298,26 +1335,30 @@ function performOnePointerMove(inputState, targetX, targetY, win) {
if (targetX == inputState.x && targetY == inputState.y) { if (targetX == inputState.x && targetY == inputState.y) {
return; return;
} }
switch (inputState.subtype) { switch (inputState.subtype) {
case action.PointerType.Mouse: case action.PointerType.Mouse:
let mouseEvent = new action.Mouse("mousemove"); let mouseEvent = new action.Mouse("mousemove");
mouseEvent.update(inputState); mouseEvent.update(inputState);
//TODO both pointermove (if available) and mousemove // TODO both pointermove (if available) and mousemove
event.synthesizeMouseAtPoint(targetX, targetY, mouseEvent, win); event.synthesizeMouseAtPoint(targetX, targetY, mouseEvent, win);
break; break;
case action.PointerType.Pen: case action.PointerType.Pen:
case action.PointerType.Touch: case action.PointerType.Touch:
throw new UnsupportedOperationError("Only 'mouse' pointer type is supported"); throw new UnsupportedOperationError("Only 'mouse' pointer type is supported");
default: default:
throw new TypeError(`Unknown pointer type: ${inputState.subtype}`); throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
} }
inputState.x = targetX; inputState.x = targetX;
inputState.y = targetY; inputState.y = targetY;
} }
/** /**
* Dispatch a pause action equivalent waiting for |a.duration| milliseconds, or a * Dispatch a pause action equivalent waiting for |a.duration|
* default time interval of |tickDuration|. * milliseconds, or a default time interval of |tickDuration|.
* *
* @param {action.Action} a * @param {action.Action} a
* Action to dispatch. * Action to dispatch.
@ -1351,8 +1392,10 @@ function inViewPort(x, y, win) {
function getElementCenter(elementReference, seenEls, container) { function getElementCenter(elementReference, seenEls, container) {
if (element.isWebElementReference(elementReference)) { if (element.isWebElementReference(elementReference)) {
let uuid = elementReference[element.Key] || elementReference[element.LegacyKey]; let uuid = elementReference[element.Key] ||
elementReference[element.LegacyKey];
let el = seenEls.get(uuid, container); let el = seenEls.get(uuid, container);
return element.coordinates(el); return element.coordinates(el);
} }
return {};
} }

View File

@ -49,27 +49,27 @@ function lookupError(code) {
* @throws {UnknownError} * @throws {UnknownError}
* If there is a problem installing the addon. * If there is a problem installing the addon.
*/ */
addon.install = function (path, temporary = false) { addon.install = function(path, temporary = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let file = new FileUtils.File(path); let file = new FileUtils.File(path);
let listener = { let listener = {
onInstallEnded: function (install, addon) { onInstallEnded(install, addon) {
resolve(addon.id); resolve(addon.id);
}, },
onInstallFailed: function (install) { onInstallFailed(install) {
reject(lookupError(install.error)); reject(lookupError(install.error));
}, },
onInstalled: function (addon) { onInstalled(addon) {
AddonManager.removeAddonListener(listener); AddonManager.removeAddonListener(listener);
resolve(addon.id); resolve(addon.id);
} },
}; };
if (!temporary) { if (!temporary) {
AddonManager.getInstallForFile(file, function (aInstall) { AddonManager.getInstallForFile(file, function(aInstall) {
if (aInstall.error !== 0) { if (aInstall.error !== 0) {
reject(lookupError(aInstall.error)); reject(lookupError(aInstall.error));
} }
@ -94,9 +94,9 @@ addon.install = function (path, temporary = false) {
* *
* @return {Promise} * @return {Promise}
*/ */
addon.uninstall = function (id) { addon.uninstall = function(id) {
return new Promise(resolve => { return new Promise(resolve => {
AddonManager.getAddonByID(id, function (addon) { AddonManager.getAddonByID(id, function(addon) {
addon.uninstall(); addon.uninstall();
resolve(); resolve();
}); });

View File

@ -41,7 +41,7 @@ this.assert = {};
* @throws {InvalidSessionIDError} * @throws {InvalidSessionIDError}
* If |driver| does not have a session ID. * If |driver| does not have a session ID.
*/ */
assert.session = function (driver, msg = "") { assert.session = function(driver, msg = "") {
assert.that(sessionID => sessionID, assert.that(sessionID => sessionID,
msg, InvalidSessionIDError)(driver.sessionId); msg, InvalidSessionIDError)(driver.sessionId);
return driver.sessionId; return driver.sessionId;
@ -56,7 +56,7 @@ assert.session = function (driver, msg = "") {
* @throws {UnsupportedOperationError} * @throws {UnsupportedOperationError}
* If current browser is not Firefox. * If current browser is not Firefox.
*/ */
assert.firefox = function (msg = "") { assert.firefox = function(msg = "") {
msg = msg || "Only supported in Firefox"; msg = msg || "Only supported in Firefox";
assert.that(isFirefox, msg, UnsupportedOperationError)(); assert.that(isFirefox, msg, UnsupportedOperationError)();
}; };
@ -70,7 +70,7 @@ assert.firefox = function (msg = "") {
* @throws {UnsupportedOperationError} * @throws {UnsupportedOperationError}
* If current browser is not Fennec. * If current browser is not Fennec.
*/ */
assert.fennec = function (msg = "") { assert.fennec = function(msg = "") {
msg = msg || "Only supported in Fennec"; msg = msg || "Only supported in Fennec";
assert.that(isFennec, msg, UnsupportedOperationError)(); assert.that(isFennec, msg, UnsupportedOperationError)();
}; };
@ -89,7 +89,7 @@ assert.fennec = function (msg = "") {
* @throws {UnsupportedOperationError} * @throws {UnsupportedOperationError}
* If |context| is not content. * If |context| is not content.
*/ */
assert.content = function (context, msg = "") { assert.content = function(context, msg = "") {
msg = msg || "Only supported in content context"; msg = msg || "Only supported in content context";
assert.that(c => c.toString() == "content", msg, UnsupportedOperationError)(context); assert.that(c => c.toString() == "content", msg, UnsupportedOperationError)(context);
}; };
@ -108,7 +108,7 @@ assert.content = function (context, msg = "") {
* @throws {NoSuchWindowError} * @throws {NoSuchWindowError}
* If |win| has been closed. * If |win| has been closed.
*/ */
assert.window = function (win, msg = "") { assert.window = function(win, msg = "") {
msg = msg || "Unable to locate window"; msg = msg || "Unable to locate window";
return assert.that(w => w && !w.closed, return assert.that(w => w && !w.closed,
msg, msg,
@ -126,7 +126,7 @@ assert.window = function (win, msg = "") {
* @throws {NoSuchWindowError} * @throws {NoSuchWindowError}
* If |context| is invalid. * If |context| is invalid.
*/ */
assert.contentBrowser = function (context, msg = "") { assert.contentBrowser = function(context, msg = "") {
// TODO: The contentBrowser uses a cached tab, which is only updated when // TODO: The contentBrowser uses a cached tab, which is only updated when
// switchToTab is called. Because of that an additional check is needed to // switchToTab is called. Because of that an additional check is needed to
// make sure that the chrome window has not already been closed. // make sure that the chrome window has not already been closed.
@ -149,7 +149,7 @@ assert.contentBrowser = function (context, msg = "") {
* @throws {UnexpectedAlertOpenError} * @throws {UnexpectedAlertOpenError}
* If there is a user prompt. * If there is a user prompt.
*/ */
assert.noUserPrompt = function (dialog, msg = "") { assert.noUserPrompt = function(dialog, msg = "") {
assert.that(d => d === null || typeof d == "undefined", assert.that(d => d === null || typeof d == "undefined",
msg, msg,
UnexpectedAlertOpenError)(dialog); UnexpectedAlertOpenError)(dialog);
@ -169,7 +169,7 @@ assert.noUserPrompt = function (dialog, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not defined. * If |obj| is not defined.
*/ */
assert.defined = function (obj, msg = "") { assert.defined = function(obj, msg = "") {
msg = msg || error.pprint`Expected ${obj} to be defined`; msg = msg || error.pprint`Expected ${obj} to be defined`;
return assert.that(o => typeof o != "undefined", msg)(obj); return assert.that(o => typeof o != "undefined", msg)(obj);
}; };
@ -188,7 +188,7 @@ assert.defined = function (obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not a number. * If |obj| is not a number.
*/ */
assert.number = function (obj, msg = "") { assert.number = function(obj, msg = "") {
msg = msg || error.pprint`Expected ${obj} to be finite number`; msg = msg || error.pprint`Expected ${obj} to be finite number`;
return assert.that(Number.isFinite, msg)(obj); return assert.that(Number.isFinite, msg)(obj);
}; };
@ -207,7 +207,7 @@ assert.number = function (obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not callable. * If |obj| is not callable.
*/ */
assert.callable = function (obj, msg = "") { assert.callable = function(obj, msg = "") {
msg = msg || error.pprint`${obj} is not callable`; msg = msg || error.pprint`${obj} is not callable`;
return assert.that(o => typeof o == "function", msg)(obj); return assert.that(o => typeof o == "function", msg)(obj);
}; };
@ -226,7 +226,7 @@ assert.callable = function (obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not an integer. * If |obj| is not an integer.
*/ */
assert.integer = function (obj, msg = "") { assert.integer = function(obj, msg = "") {
msg = msg || error.pprint`Expected ${obj} to be an integer`; msg = msg || error.pprint`Expected ${obj} to be an integer`;
return assert.that(Number.isInteger, msg)(obj); return assert.that(Number.isInteger, msg)(obj);
}; };
@ -245,7 +245,7 @@ assert.integer = function (obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not a positive integer. * If |obj| is not a positive integer.
*/ */
assert.positiveInteger = function (obj, msg = "") { assert.positiveInteger = function(obj, msg = "") {
assert.integer(obj, msg); assert.integer(obj, msg);
msg = msg || error.pprint`Expected ${obj} to be >= 0`; msg = msg || error.pprint`Expected ${obj} to be >= 0`;
return assert.that(n => n >= 0, msg)(obj); return assert.that(n => n >= 0, msg)(obj);
@ -265,7 +265,7 @@ assert.positiveInteger = function (obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not a boolean. * If |obj| is not a boolean.
*/ */
assert.boolean = function (obj, msg = "") { assert.boolean = function(obj, msg = "") {
msg = msg || error.pprint`Expected ${obj} to be boolean`; msg = msg || error.pprint`Expected ${obj} to be boolean`;
return assert.that(b => typeof b == "boolean", msg)(obj); return assert.that(b => typeof b == "boolean", msg)(obj);
}; };
@ -284,7 +284,7 @@ assert.boolean = function (obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not a string. * If |obj| is not a string.
*/ */
assert.string = function (obj, msg = "") { assert.string = function(obj, msg = "") {
msg = msg || error.pprint`Expected ${obj} to be a string`; msg = msg || error.pprint`Expected ${obj} to be a string`;
return assert.that(s => typeof s == "string", msg)(obj); return assert.that(s => typeof s == "string", msg)(obj);
}; };
@ -303,7 +303,7 @@ assert.string = function (obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not an object. * If |obj| is not an object.
*/ */
assert.object = function (obj, msg = "") { assert.object = function(obj, msg = "") {
msg = msg || error.pprint`Expected ${obj} to be an object`; msg = msg || error.pprint`Expected ${obj} to be an object`;
return assert.that(o => { return assert.that(o => {
// unable to use instanceof because LHS and RHS may come from // unable to use instanceof because LHS and RHS may come from
@ -329,7 +329,7 @@ assert.object = function (obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |prop| is not in |obj|, or |obj| is not an object. * If |prop| is not in |obj|, or |obj| is not an object.
*/ */
assert.in = function (prop, obj, msg = "") { assert.in = function(prop, obj, msg = "") {
assert.object(obj, msg); assert.object(obj, msg);
msg = msg || error.pprint`Expected ${prop} in ${obj}`; msg = msg || error.pprint`Expected ${prop} in ${obj}`;
assert.that(p => obj.hasOwnProperty(p), msg)(prop); assert.that(p => obj.hasOwnProperty(p), msg)(prop);
@ -350,7 +350,7 @@ assert.in = function (prop, obj, msg = "") {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If |obj| is not an Array. * If |obj| is not an Array.
*/ */
assert.array = function (obj, msg = "") { assert.array = function(obj, msg = "") {
msg = msg || error.pprint`Expected ${obj} to be an Array`; msg = msg || error.pprint`Expected ${obj} to be an Array`;
return assert.that(Array.isArray, msg)(obj); return assert.that(Array.isArray, msg)(obj);
}; };
@ -372,7 +372,7 @@ assert.array = function (obj, msg = "") {
* which may throw |error| with |message| if |predicate| evaluates * which may throw |error| with |message| if |predicate| evaluates
* to false. * to false.
*/ */
assert.that = function ( assert.that = function(
predicate, message = "", error = InvalidArgumentError) { predicate, message = "", error = InvalidArgumentError) {
return obj => { return obj => {
if (!predicate(obj)) { if (!predicate(obj)) {

View File

@ -29,18 +29,17 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
* @return {<xul:browser>} * @return {<xul:browser>}
* The linked browser for the tab or null if no browser can be found. * The linked browser for the tab or null if no browser can be found.
*/ */
browser.getBrowserForTab = function (tab) { browser.getBrowserForTab = function(tab) {
// Fennec
if ("browser" in tab) { if ("browser" in tab) {
// Fennec
return tab.browser; return tab.browser;
// Firefox
} else if ("linkedBrowser" in tab) { } else if ("linkedBrowser" in tab) {
// Firefox
return tab.linkedBrowser; return tab.linkedBrowser;
} else {
return null;
} }
return null;
}; };
/** /**
@ -52,18 +51,17 @@ browser.getBrowserForTab = function (tab) {
* @return {<xul:tabbrowser>} * @return {<xul:tabbrowser>}
* Tab browser or null if it's not a browser window. * Tab browser or null if it's not a browser window.
*/ */
browser.getTabBrowser = function (win) { browser.getTabBrowser = function(win) {
// Fennec
if ("BrowserApp" in win) { if ("BrowserApp" in win) {
// Fennec
return win.BrowserApp; return win.BrowserApp;
// Firefox
} else if ("gBrowser" in win) { } else if ("gBrowser" in win) {
// Firefox
return win.gBrowser; return win.gBrowser;
} else {
return null;
} }
return null;
}; };
/** /**
@ -100,13 +98,13 @@ browser.Context = class {
this.seenEls = new element.Store(); this.seenEls = new element.Store();
// A reference to the tab corresponding to the current window handle, if any. // A reference to the tab corresponding to the current window handle,
// Specifically, this.tab refers to the last tab that Marionette switched // if any. Specifically, this.tab refers to the last tab that Marionette
// to in this browser window. Note that this may not equal the currently // switched to in this browser window. Note that this may not equal the
// selected tab. For example, if Marionette switches to tab A, and then // currently selected tab. For example, if Marionette switches to tab
// clicks on a button that opens a new tab B in the same browser window, // A, and then clicks on a button that opens a new tab B in the same
// this.tab will still point to tab A, despite tab B being the currently // browser window, this.tab will still point to tab A, despite tab B
// selected tab. // being the currently selected tab.
this.tab = null; this.tab = null;
this.pendingCommands = []; this.pendingCommands = [];
@ -130,7 +128,8 @@ browser.Context = class {
get contentBrowser() { get contentBrowser() {
if (this.tab) { if (this.tab) {
return browser.getBrowserForTab(this.tab); return browser.getBrowserForTab(this.tab);
} else if (this.tabBrowser && this.driver.isReftestBrowser(this.tabBrowser)) { } else if (this.tabBrowser &&
this.driver.isReftestBrowser(this.tabBrowser)) {
return this.tabBrowser; return this.tabBrowser;
} }
@ -144,7 +143,7 @@ browser.Context = class {
*/ */
get curFrameId() { get curFrameId() {
let rv = null; let rv = null;
if (this.tab || this.driver.isReftestBrowser(this.contentBrowser) ) { if (this.tab || this.driver.isReftestBrowser(this.contentBrowser)) {
rv = this.getIdForBrowser(this.contentBrowser); rv = this.getIdForBrowser(this.contentBrowser);
} }
return rv; return rv;
@ -164,10 +163,9 @@ browser.Context = class {
// initialization been finished // initialization been finished
if (this.contentBrowser) { if (this.contentBrowser) {
return this.contentBrowser.currentURI; return this.contentBrowser.currentURI;
} else {
throw new NoSuchWindowError(
"Current window does not have a content browser");
} }
throw new NoSuchWindowError(
"Current window does not have a content browser");
} }
/** /**
@ -234,7 +232,10 @@ browser.Context = class {
closeTab() { closeTab() {
// If the current window is not a browser then close it directly. Do the // If the current window is not a browser then close it directly. Do the
// same if only one remaining tab is open, or no tab selected at all. // same if only one remaining tab is open, or no tab selected at all.
if (!this.tabBrowser || !this.tabBrowser.tabs || this.tabBrowser.tabs.length === 1 || !this.tab) { if (!this.tabBrowser ||
!this.tabBrowser.tabs ||
this.tabBrowser.tabs.length === 1 ||
!this.tab) {
return this.closeWindow(); return this.closeWindow();
} }

View File

@ -34,7 +34,7 @@ capture.Format = {
* @return {HTMLCanvasElement} * @return {HTMLCanvasElement}
* The canvas element where the element has been painted on. * The canvas element where the element has been painted on.
*/ */
capture.element = function (node, highlights = []) { capture.element = function(node, highlights = []) {
let win = node.ownerGlobal; let win = node.ownerGlobal;
let rect = node.getBoundingClientRect(); let rect = node.getBoundingClientRect();
@ -61,7 +61,7 @@ capture.element = function (node, highlights = []) {
* @return {HTMLCanvasElement} * @return {HTMLCanvasElement}
* The canvas element where the viewport has been painted on. * The canvas element where the viewport has been painted on.
*/ */
capture.viewport = function (win, highlights = []) { capture.viewport = function(win, highlights = []) {
let rootNode = win.document.documentElement; let rootNode = win.document.documentElement;
return capture.canvas( return capture.canvas(
@ -100,9 +100,9 @@ capture.viewport = function (win, highlights = []) {
* The canvas on which the selection from the window's framebuffer * The canvas on which the selection from the window's framebuffer
* has been painted on. * has been painted on.
*/ */
capture.canvas = function (win, left, top, width, height, capture.canvas = function(win, left, top, width, height,
{highlights = [], canvas = null, flags = null} = {}) { {highlights = [], canvas = null, flags = null} = {}) {
let scale = win.devicePixelRatio; const scale = win.devicePixelRatio;
if (canvas === null) { if (canvas === null) {
canvas = win.document.createElementNS(XHTML_NS, "canvas"); canvas = win.document.createElementNS(XHTML_NS, "canvas");
@ -113,12 +113,19 @@ capture.canvas = function (win, left, top, width, height,
let ctx = canvas.getContext(CONTEXT_2D); let ctx = canvas.getContext(CONTEXT_2D);
if (flags === null) { if (flags === null) {
flags = ctx.DRAWWINDOW_DRAW_CARET; flags = ctx.DRAWWINDOW_DRAW_CARET;
// Disabled in bug 1243415 for webplatform-test failures due to out of view elements. // TODO(ato): https://bugzil.la/1377335
// Needs https://github.com/w3c/web-platform-tests/issues/4383 fixed. //
// ctx.DRAWWINDOW_DRAW_VIEW; // Disabled in bug 1243415 for webplatform-test
// failures due to out of view elements. Needs
// https://github.com/w3c/web-platform-tests/issues/4383 fixed.
/*
ctx.DRAWWINDOW_DRAW_VIEW;
*/
// Bug 1009762 - Crash in [@ mozilla::gl::ReadPixelsIntoDataSurface] // Bug 1009762 - Crash in [@ mozilla::gl::ReadPixelsIntoDataSurface]
// ctx.DRAWWINDOW_USE_WIDGET_LAYERS; /*
} ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
*/
}
ctx.scale(scale, scale); ctx.scale(scale, scale);
ctx.drawWindow(win, left, top, width, height, BG_COLOUR, flags); ctx.drawWindow(win, left, top, width, height, BG_COLOUR, flags);
@ -129,9 +136,9 @@ capture.canvas = function (win, left, top, width, height,
return canvas; return canvas;
}; };
capture.highlight_ = function (context, highlights, top = 0, left = 0) { capture.highlight_ = function(context, highlights, top = 0, left = 0) {
if (!highlights) { if (!highlights) {
return; throw new TypeError("Missing highlights");
} }
context.lineWidth = "2"; context.lineWidth = "2";
@ -162,7 +169,7 @@ capture.highlight_ = function (context, highlights, top = 0, left = 0) {
* @return {string} * @return {string}
* A Base64 encoded string. * A Base64 encoded string.
*/ */
capture.toBase64 = function (canvas) { capture.toBase64 = function(canvas) {
let u = canvas.toDataURL(PNG_MIME); let u = canvas.toDataURL(PNG_MIME);
return u.substring(u.indexOf(",") + 1); return u.substring(u.indexOf(",") + 1);
}; };
@ -176,7 +183,7 @@ capture.toBase64 = function (canvas) {
* @return {string} * @return {string}
* A hex digest of the SHA-256 hash of the base64 encoded string. * A hex digest of the SHA-256 hash of the base64 encoded string.
*/ */
capture.toHash = function (canvas) { capture.toHash = function(canvas) {
let u = capture.toBase64(canvas); let u = capture.toBase64(canvas);
let buffer = new TextEncoder("utf-8").encode(u); let buffer = new TextEncoder("utf-8").encode(u);
return crypto.subtle.digest("SHA-256", buffer).then(hash => hex(hash)); return crypto.subtle.digest("SHA-256", buffer).then(hash => hex(hash));
@ -197,9 +204,9 @@ function hex(buffer) {
for (let i = 0; i < view.byteLength; i += 4) { for (let i = 0; i < view.byteLength; i += 4) {
let value = view.getUint32(i); let value = view.getUint32(i);
let stringValue = value.toString(16); let stringValue = value.toString(16);
let padding = '00000000'; let padding = "00000000";
let paddedValue = (padding + stringValue).slice(-padding.length); let paddedValue = (padding + stringValue).slice(-padding.length);
hexCodes.push(paddedValue); hexCodes.push(paddedValue);
} }
return hexCodes.join(""); return hexCodes.join("");
}; }

View File

@ -53,7 +53,7 @@ this.cert = {
* @throws {Components.Exception} * @throws {Components.Exception}
* If unable to register or initialise |service|. * If unable to register or initialise |service|.
*/ */
cert.installOverride = function (service) { cert.installOverride = function(service) {
if (this.currentOverride) { if (this.currentOverride) {
return; return;
} }
@ -102,7 +102,7 @@ cert.InsecureSweepingOverride = function() {
// make your life miserable. // make your life miserable.
let service = function() {}; let service = function() {};
service.prototype = { service.prototype = {
hasMatchingOverride: function ( hasMatchingOverride(
aHostName, aPort, aCert, aOverrideBits, aIsTemporary) { aHostName, aPort, aCert, aOverrideBits, aIsTemporary) {
aIsTemporary.value = false; aIsTemporary.value = false;
aOverrideBits.value = aOverrideBits.value =
@ -116,7 +116,7 @@ cert.InsecureSweepingOverride = function() {
let factory = XPCOMUtils.generateSingletonFactory(service); let factory = XPCOMUtils.generateSingletonFactory(service);
return { return {
register: function() { register() {
// make it possible to register certificate overrides for domains // make it possible to register certificate overrides for domains
// that use HSTS or HPKP // that use HSTS or HPKP
Preferences.set(HSTS_PRELOAD_LIST_PREF, false); Preferences.set(HSTS_PRELOAD_LIST_PREF, false);
@ -125,7 +125,7 @@ cert.InsecureSweepingOverride = function() {
registrar.registerFactory(CID, DESC, CONTRACT_ID, factory); registrar.registerFactory(CID, DESC, CONTRACT_ID, factory);
}, },
unregister: function() { unregister() {
registrar.unregisterFactory(CID, factory); registrar.unregisterFactory(CID, factory);
Preferences.reset(HSTS_PRELOAD_LIST_PREF); Preferences.reset(HSTS_PRELOAD_LIST_PREF);

View File

@ -24,7 +24,7 @@ const PREF_LOG_LEVEL_FALLBACK = "marionette.logging";
const DEFAULT_LOG_LEVEL = "info"; const DEFAULT_LOG_LEVEL = "info";
const LOG_LEVELS = new class extends Map { const LOG_LEVELS = new class extends Map {
constructor () { constructor() {
super([ super([
["fatal", Log.Level.Fatal], ["fatal", Log.Level.Fatal],
["error", Log.Level.Error], ["error", Log.Level.Error],
@ -36,7 +36,7 @@ const LOG_LEVELS = new class extends Map {
]); ]);
} }
get (level) { get(level) {
let s = new String(level).toLowerCase(); let s = new String(level).toLowerCase();
if (!this.has(s)) { if (!this.has(s)) {
return DEFAULT_LOG_LEVEL; return DEFAULT_LOG_LEVEL;
@ -69,7 +69,7 @@ const ServerSocket = CC("@mozilla.org/network/server-socket;1",
// Get preference value of |preferred|, falling back to |fallback| // Get preference value of |preferred|, falling back to |fallback|
// if |preferred| is not user-modified and |fallback| exists. // if |preferred| is not user-modified and |fallback| exists.
function getPref (preferred, fallback) { function getPref(preferred, fallback) {
if (!Preferences.isSet(preferred) && Preferences.has(fallback)) { if (!Preferences.isSet(preferred) && Preferences.has(fallback)) {
return Preferences.get(fallback, Preferences.get(preferred)); return Preferences.get(fallback, Preferences.get(preferred));
} }
@ -82,16 +82,16 @@ function getPref (preferred, fallback) {
// //
// This shim can be removed when Firefox 55 ships. // This shim can be removed when Firefox 55 ships.
const prefs = { const prefs = {
get port () { get port() {
return getPref(PREF_PORT, PREF_PORT_FALLBACK); return getPref(PREF_PORT, PREF_PORT_FALLBACK);
}, },
get logLevel () { get logLevel() {
let s = getPref(PREF_LOG_LEVEL, PREF_LOG_LEVEL_FALLBACK); let s = getPref(PREF_LOG_LEVEL, PREF_LOG_LEVEL_FALLBACK);
return LOG_LEVELS.get(s); return LOG_LEVELS.get(s);
}, },
readFromEnvironment (key) { readFromEnvironment(key) {
const env = Cc["@mozilla.org/process/environment;1"] const env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment); .getService(Ci.nsIEnvironment);
@ -146,23 +146,14 @@ MarionetteComponent.prototype = {
helpInfo: " --marionette Enable remote control server.\n", helpInfo: " --marionette Enable remote control server.\n",
}; };
MarionetteComponent.prototype.onSocketAccepted = function (socket, transport) {
this.logger.info("onSocketAccepted for Marionette dummy socket");
};
MarionetteComponent.prototype.onStopListening = function (socket, status) {
this.logger.info(`onStopListening for Marionette dummy socket, code ${status}`);
socket.close();
};
// Handle -marionette flag // Handle -marionette flag
MarionetteComponent.prototype.handle = function (cmdLine) { MarionetteComponent.prototype.handle = function(cmdLine) {
if (cmdLine.handleFlag("marionette", false)) { if (cmdLine.handleFlag("marionette", false)) {
this.enabled = true; this.enabled = true;
} }
}; };
MarionetteComponent.prototype.observe = function (subject, topic, data) { MarionetteComponent.prototype.observe = function(subject, topic, data) {
switch (topic) { switch (topic) {
case "profile-after-change": case "profile-after-change":
// Using sessionstore-windows-restored as the xpcom category doesn't // Using sessionstore-windows-restored as the xpcom category doesn't
@ -227,14 +218,14 @@ MarionetteComponent.prototype.observe = function (subject, topic, data) {
} }
}; };
MarionetteComponent.prototype.setupLogger = function (level) { MarionetteComponent.prototype.setupLogger = function(level) {
let logger = Log.repository.getLogger("Marionette"); let logger = Log.repository.getLogger("Marionette");
logger.level = level; logger.level = level;
logger.addAppender(new Log.DumpAppender()); logger.addAppender(new Log.DumpAppender());
return logger; return logger;
}; };
MarionetteComponent.prototype.suppressSafeModeDialog = function (win) { MarionetteComponent.prototype.suppressSafeModeDialog = function(win) {
win.addEventListener("load", () => { win.addEventListener("load", () => {
if (win.document.getElementById("safeModeDialog")) { if (win.document.getElementById("safeModeDialog")) {
// accept the dialog to start in safe-mode // accept the dialog to start in safe-mode
@ -245,7 +236,7 @@ MarionetteComponent.prototype.suppressSafeModeDialog = function (win) {
}, {once: true}); }, {once: true});
}; };
MarionetteComponent.prototype.init = function () { MarionetteComponent.prototype.init = function() {
if (this.running || !this.enabled || !this.finalUIStartup) { if (this.running || !this.enabled || !this.finalUIStartup) {
return; return;
} }
@ -270,7 +261,7 @@ MarionetteComponent.prototype.init = function () {
}); });
}; };
MarionetteComponent.prototype.uninit = function () { MarionetteComponent.prototype.uninit = function() {
if (!this.running) { if (!this.running) {
return; return;
} }

View File

@ -41,7 +41,7 @@ this.cookie = {
* @throws {InvalidArgumentError} * @throws {InvalidArgumentError}
* If any of the properties are invalid. * If any of the properties are invalid.
*/ */
cookie.fromJSON = function (json) { cookie.fromJSON = function(json) {
let newCookie = {}; let newCookie = {};
assert.object(json, error.pprint`Expected cookie object, got ${json}`); assert.object(json, error.pprint`Expected cookie object, got ${json}`);
@ -85,7 +85,7 @@ cookie.fromJSON = function (json) {
* @throws {InvalidCookieDomainError} * @throws {InvalidCookieDomainError}
* If |restrictToHost| is set and |newCookie|'s domain does not match. * If |restrictToHost| is set and |newCookie|'s domain does not match.
*/ */
cookie.add = function (newCookie, opts = {}) { cookie.add = function(newCookie, opts = {}) {
assert.string(newCookie.name, "Cookie name must be string"); assert.string(newCookie.name, "Cookie name must be string");
assert.string(newCookie.value, "Cookie value must be string"); assert.string(newCookie.value, "Cookie value must be string");
assert.string(newCookie.domain, "Cookie domain must be string"); assert.string(newCookie.domain, "Cookie domain must be string");
@ -108,7 +108,8 @@ cookie.add = function (newCookie, opts = {}) {
if (newCookie.domain !== opts.restrictToHost) { if (newCookie.domain !== opts.restrictToHost) {
throw new InvalidCookieDomainError( throw new InvalidCookieDomainError(
`Cookies may only be set for the current domain (${opts.restrictToHost})`); `Cookies may only be set ` +
` for the current domain (${opts.restrictToHost})`);
} }
} }
@ -135,7 +136,7 @@ cookie.add = function (newCookie, opts = {}) {
* @param {Map.<string, (string|number|boolean)} toDelete * @param {Map.<string, (string|number|boolean)} toDelete
* Cookie to remove. * Cookie to remove.
*/ */
cookie.remove = function (toDelete) { cookie.remove = function(toDelete) {
cookie.manager.remove( cookie.manager.remove(
toDelete.domain, toDelete.domain,
toDelete.name, toDelete.name,
@ -158,7 +159,7 @@ cookie.remove = function (toDelete) {
* @return {[Symbol.Iterator]} * @return {[Symbol.Iterator]}
* Iterator. * Iterator.
*/ */
cookie.iter = function* (host, currentPath = "/") { cookie.iter = function*(host, currentPath = "/") {
assert.string(host, "host must be string"); assert.string(host, "host must be string");
assert.string(currentPath, "currentPath must be string"); assert.string(currentPath, "currentPath must be string");

File diff suppressed because it is too large Load Diff

View File

@ -247,7 +247,7 @@ element.Store = class {
* If a single element is requested, this error will throw if the * If a single element is requested, this error will throw if the
* element is not found. * element is not found.
*/ */
element.find = function (container, strategy, selector, opts = {}) { element.find = function(container, strategy, selector, opts = {}) {
opts.all = !!opts.all; opts.all = !!opts.all;
opts.timeout = opts.timeout || 0; opts.timeout = opts.timeout || 0;
@ -275,7 +275,8 @@ element.find = function (container, strategy, selector, opts = {}) {
let msg; let msg;
switch (strategy) { switch (strategy) {
case element.Strategy.AnonAttribute: case element.Strategy.AnonAttribute:
msg = "Unable to locate anonymous element: " + JSON.stringify(selector); msg = "Unable to locate anonymous element: " +
JSON.stringify(selector);
break; break;
default: default:
@ -301,8 +302,8 @@ function find_(container, strategy, selector, searchFn, opts) {
startNode = opts.startNode; startNode = opts.startNode;
} else { } else {
switch (strategy) { switch (strategy) {
// For anonymous nodes the start node needs to be of type DOMElement, which // For anonymous nodes the start node needs to be of type
// will refer to :root in case of a DOMDocument. // DOMElement, which will refer to :root in case of a DOMDocument.
case element.Strategy.Anon: case element.Strategy.Anon:
case element.Strategy.AnonAttribute: case element.Strategy.AnonAttribute:
if (rootNode instanceof Ci.nsIDOMDocument) { if (rootNode instanceof Ci.nsIDOMDocument) {
@ -345,7 +346,7 @@ function find_(container, strategy, selector, searchFn, opts) {
* @return {DOMElement} * @return {DOMElement}
* First element matching expression. * First element matching expression.
*/ */
element.findByXPath = function (root, startNode, expr) { element.findByXPath = function(root, startNode, expr) {
let iter = root.evaluate(expr, startNode, null, let iter = root.evaluate(expr, startNode, null,
Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null); Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
return iter.singleNodeValue; return iter.singleNodeValue;
@ -364,7 +365,7 @@ element.findByXPath = function (root, startNode, expr) {
* @return {Array.<DOMElement>} * @return {Array.<DOMElement>}
* Sequence of found elements matching expression. * Sequence of found elements matching expression.
*/ */
element.findByXPathAll = function (root, startNode, expr) { element.findByXPathAll = function(root, startNode, expr) {
let rv = []; let rv = [];
let iter = root.evaluate(expr, startNode, null, let iter = root.evaluate(expr, startNode, null,
Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null); Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
@ -387,7 +388,7 @@ element.findByXPathAll = function (root, startNode, expr) {
* @return {Array.<DOMAnchorElement>} * @return {Array.<DOMAnchorElement>}
* Sequence of link elements which text is |s|. * Sequence of link elements which text is |s|.
*/ */
element.findByLinkText = function (node, s) { element.findByLinkText = function(node, s) {
return filterLinks(node, link => link.text.trim() === s); return filterLinks(node, link => link.text.trim() === s);
}; };
@ -402,7 +403,7 @@ element.findByLinkText = function (node, s) {
* @return {Array.<DOMAnchorElement>} * @return {Array.<DOMAnchorElement>}
* Sequence of link elements which text containins |s|. * Sequence of link elements which text containins |s|.
*/ */
element.findByPartialLinkText = function (node, s) { element.findByPartialLinkText = function(node, s) {
return filterLinks(node, link => link.text.indexOf(s) != -1); return filterLinks(node, link => link.text.indexOf(s) != -1);
}; };
@ -547,7 +548,8 @@ function findElements(using, value, rootNode, startNode) {
if (startNode.getElementsByName) { if (startNode.getElementsByName) {
return startNode.getElementsByName(value); return startNode.getElementsByName(value);
} }
return element.findByXPathAll(rootNode, startNode, `.//*[@name="${value}"]`); return element.findByXPathAll(
rootNode, startNode, `.//*[@name="${value}"]`);
case element.Strategy.ClassName: case element.Strategy.ClassName:
return startNode.getElementsByClassName(value); return startNode.getElementsByClassName(value);
@ -569,7 +571,8 @@ function findElements(using, value, rootNode, startNode) {
case element.Strategy.AnonAttribute: case element.Strategy.AnonAttribute:
let attr = Object.keys(value)[0]; let attr = Object.keys(value)[0];
let el = rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]); let el = rootNode.getAnonymousElementByAttribute(
startNode, attr, value[attr]);
if (el) { if (el) {
return [el]; return [el];
} }
@ -581,7 +584,7 @@ function findElements(using, value, rootNode, startNode) {
} }
/** Determines if |obj| is an HTML or JS collection. */ /** Determines if |obj| is an HTML or JS collection. */
element.isCollection = function (seq) { element.isCollection = function(seq) {
switch (Object.prototype.toString.call(seq)) { switch (Object.prototype.toString.call(seq)) {
case "[object Arguments]": case "[object Arguments]":
case "[object Array]": case "[object Array]":
@ -598,7 +601,7 @@ element.isCollection = function (seq) {
} }
}; };
element.makeWebElement = function (uuid) { element.makeWebElement = function(uuid) {
return { return {
[element.Key]: uuid, [element.Key]: uuid,
[element.LegacyKey]: uuid, [element.LegacyKey]: uuid,
@ -606,16 +609,18 @@ element.makeWebElement = function (uuid) {
}; };
/** /**
* Checks if |ref| has either |element.Key| or |element.LegacyKey| as properties. * Checks if |ref| has either |element.Key| or |element.LegacyKey|
* as properties.
* *
* @param {?} ref * @param {?} ref
* Object that represents a web element reference. * Object that represents a web element reference.
* @return {boolean} * @return {boolean}
* True if |ref| has either expected property. * True if |ref| has either expected property.
*/ */
element.isWebElementReference = function (ref) { element.isWebElementReference = function(ref) {
let properties = Object.getOwnPropertyNames(ref); let properties = Object.getOwnPropertyNames(ref);
return properties.includes(element.Key) || properties.includes(element.LegacyKey); return properties.includes(element.Key) ||
properties.includes(element.LegacyKey);
}; };
element.generateUUID = function() { element.generateUUID = function() {
@ -636,11 +641,11 @@ element.generateUUID = function() {
* @return {boolean} * @return {boolean}
* Flag indicating that the element is disconnected. * Flag indicating that the element is disconnected.
*/ */
element.isDisconnected = function (el, container = {}) { element.isDisconnected = function(el, container = {}) {
const {frame, shadowRoot} = container; const {frame, shadowRoot} = container;
assert.defined(frame); assert.defined(frame);
// shadow dom // shadow DOM
if (frame.ShadowRoot && shadowRoot) { if (frame.ShadowRoot && shadowRoot) {
if (el.compareDocumentPosition(shadowRoot) & if (el.compareDocumentPosition(shadowRoot) &
DOCUMENT_POSITION_DISCONNECTED) { DOCUMENT_POSITION_DISCONNECTED) {
@ -654,14 +659,13 @@ element.isDisconnected = function (el, container = {}) {
} }
return element.isDisconnected( return element.isDisconnected(
shadowRoot.host, shadowRoot.host,
{frame: frame, shadowRoot: parent}); {frame, shadowRoot: parent});
// outside shadow dom
} else {
let docEl = frame.document.documentElement;
return el.compareDocumentPosition(docEl) &
DOCUMENT_POSITION_DISCONNECTED;
} }
// outside shadow DOM
let docEl = frame.document.documentElement;
return el.compareDocumentPosition(docEl) &
DOCUMENT_POSITION_DISCONNECTED;
}; };
/** /**
@ -684,7 +688,7 @@ element.isDisconnected = function (el, container = {}) {
* @throws TypeError * @throws TypeError
* If |xOffset| or |yOffset| are not numbers. * If |xOffset| or |yOffset| are not numbers.
*/ */
element.coordinates = function ( element.coordinates = function(
node, xOffset = undefined, yOffset = undefined) { node, xOffset = undefined, yOffset = undefined) {
let box = node.getBoundingClientRect(); let box = node.getBoundingClientRect();
@ -721,14 +725,14 @@ element.coordinates = function (
* @return {boolean} * @return {boolean}
* True if if |el| is in viewport, false otherwise. * True if if |el| is in viewport, false otherwise.
*/ */
element.inViewport = function (el, x = undefined, y = undefined) { element.inViewport = function(el, x = undefined, y = undefined) {
let win = el.ownerGlobal; let win = el.ownerGlobal;
let c = element.coordinates(el, x, y); let c = element.coordinates(el, x, y);
let vp = { let vp = {
top: win.pageYOffset, top: win.pageYOffset,
left: win.pageXOffset, left: win.pageXOffset,
bottom: (win.pageYOffset + win.innerHeight), bottom: (win.pageYOffset + win.innerHeight),
right: (win.pageXOffset + win.innerWidth) right: (win.pageXOffset + win.innerWidth),
}; };
return (vp.left <= c.x + win.pageXOffset && return (vp.left <= c.x + win.pageXOffset &&
@ -754,7 +758,7 @@ element.inViewport = function (el, x = undefined, y = undefined) {
* @return {Element} * @return {Element}
* Container element of |el|. * Container element of |el|.
*/ */
element.getContainer = function (el) { element.getContainer = function(el) {
if (el.localName != "option") { if (el.localName != "option") {
return el; return el;
} }
@ -796,7 +800,7 @@ element.getContainer = function (el) {
* @return {boolean} * @return {boolean}
* True if |el| is inside the viewport, or false otherwise. * True if |el| is inside the viewport, or false otherwise.
*/ */
element.isInView = function (el) { element.isInView = function(el) {
let originalPointerEvents = el.style.pointerEvents; let originalPointerEvents = el.style.pointerEvents;
try { try {
el.style.pointerEvents = "auto"; el.style.pointerEvents = "auto";
@ -823,7 +827,7 @@ element.isInView = function (el) {
* @return {boolean} * @return {boolean}
* True if visible, false otherwise. * True if visible, false otherwise.
*/ */
element.isVisible = function (el, x = undefined, y = undefined) { element.isVisible = function(el, x = undefined, y = undefined) {
let win = el.ownerGlobal; let win = el.ownerGlobal;
// Bug 1094246: webdriver's isShown doesn't work with content xul // Bug 1094246: webdriver's isShown doesn't work with content xul
@ -860,7 +864,7 @@ element.isVisible = function (el, x = undefined, y = undefined) {
* @return {boolean} * @return {boolean}
* True if element is obscured, false otherwise. * True if element is obscured, false otherwise.
*/ */
element.isObscured = function (el) { element.isObscured = function(el) {
let tree = element.getPointerInteractablePaintTree(el); let tree = element.getPointerInteractablePaintTree(el);
return !el.contains(tree[0]); return !el.contains(tree[0]);
}; };
@ -878,7 +882,7 @@ element.isObscured = function (el) {
* @return {Map.<string, number>} * @return {Map.<string, number>}
* X and Y coordinates that denotes the in-view centre point of |rect|. * X and Y coordinates that denotes the in-view centre point of |rect|.
*/ */
element.getInViewCentrePoint = function (rect, win) { element.getInViewCentrePoint = function(rect, win) {
const {max, min} = Math; const {max, min} = Math;
let x = { let x = {
@ -909,7 +913,7 @@ element.getInViewCentrePoint = function (rect, win) {
* @return {Array.<DOMElement>} * @return {Array.<DOMElement>}
* Sequence of elements in paint order. * Sequence of elements in paint order.
*/ */
element.getPointerInteractablePaintTree = function (el) { element.getPointerInteractablePaintTree = function(el) {
const doc = el.ownerDocument; const doc = el.ownerDocument;
const win = doc.defaultView; const win = doc.defaultView;
const container = {frame: win}; const container = {frame: win};
@ -941,7 +945,7 @@ element.getPointerInteractablePaintTree = function (el) {
// TODO(ato): Not implemented. // TODO(ato): Not implemented.
// In fact, it's not defined in the spec. // In fact, it's not defined in the spec.
element.isKeyboardInteractable = function (el) { element.isKeyboardInteractable = function(el) {
return true; return true;
}; };
@ -951,13 +955,13 @@ element.isKeyboardInteractable = function (el) {
* @param {DOMElement} el * @param {DOMElement} el
* Element to scroll into view. * Element to scroll into view.
*/ */
element.scrollIntoView = function (el) { element.scrollIntoView = function(el) {
if (el.scrollIntoView) { if (el.scrollIntoView) {
el.scrollIntoView({block: "end", inline: "nearest", behavior: "instant"}); el.scrollIntoView({block: "end", inline: "nearest", behavior: "instant"});
} }
}; };
element.isXULElement = function (el) { element.isXULElement = function(el) {
let ns = atom.getElementAttribute(el, "namespaceURI"); let ns = atom.getElementAttribute(el, "namespaceURI");
return ns.indexOf("there.is.only.xul") >= 0; return ns.indexOf("there.is.only.xul") >= 0;
}; };
@ -971,7 +975,15 @@ const boolEls = {
form: ["novalidate"], form: ["novalidate"],
iframe: ["allowfullscreen"], iframe: ["allowfullscreen"],
img: ["ismap"], img: ["ismap"],
input: ["autofocus", "checked", "disabled", "formnovalidate", "multiple", "readonly", "required"], input: [
"autofocus",
"checked",
"disabled",
"formnovalidate",
"multiple",
"readonly",
"required",
],
keygen: ["autofocus", "disabled"], keygen: ["autofocus", "disabled"],
menuitem: ["checked", "default", "disabled"], menuitem: ["checked", "default", "disabled"],
object: ["typemustmatch"], object: ["typemustmatch"],
@ -996,14 +1008,15 @@ const boolEls = {
* @return {boolean} * @return {boolean}
* True if the attribute is boolean, false otherwise. * True if the attribute is boolean, false otherwise.
*/ */
element.isBooleanAttribute = function (el, attr) { element.isBooleanAttribute = function(el, attr) {
if (el.namespaceURI !== XMLNS) { if (el.namespaceURI !== XMLNS) {
return false; return false;
} }
// global boolean attributes that apply to all HTML elements, // global boolean attributes that apply to all HTML elements,
// except for custom elements // except for custom elements
if ((attr == "hidden" || attr == "itemscope") && !el.localName.includes("-")) { const customElement = !el.localName.includes("-");
if ((attr == "hidden" || attr == "itemscope") && customElement) {
return true; return true;
} }

View File

@ -45,7 +45,7 @@ const BUILTIN_ERRORS = new Set([
"URIError", "URIError",
]); ]);
this.EXPORTED_SYMBOLS = ["error"].concat(Array.from(ERRORS)); this.EXPORTED_SYMBOLS = ["error", "error.pprint"].concat(Array.from(ERRORS));
this.error = {}; this.error = {};
@ -71,26 +71,26 @@ this.error = {};
* @return {boolean} * @return {boolean}
* True if error, false otherwise. * True if error, false otherwise.
*/ */
error.isError = function (val) { error.isError = function(val) {
if (val === null || typeof val != "object") { if (val === null || typeof val != "object") {
return false; return false;
} else if (val instanceof Ci.nsIException) { } else if (val instanceof Ci.nsIException) {
return true; return true;
} else { }
// DOMRectList errors on string comparison
try { // DOMRectList errors on string comparison
let proto = Object.getPrototypeOf(val); try {
return BUILTIN_ERRORS.has(proto.toString()); let proto = Object.getPrototypeOf(val);
} catch (e) { return BUILTIN_ERRORS.has(proto.toString());
return false; } catch (e) {
} return false;
} }
}; };
/** /**
* Checks if obj is an object in the WebDriverError prototypal chain. * Checks if obj is an object in the WebDriverError prototypal chain.
*/ */
error.isWebDriverError = function (obj) { error.isWebDriverError = function(obj) {
return error.isError(obj) && return error.isError(obj) &&
("name" in obj && ERRORS.has(obj.name)); ("name" in obj && ERRORS.has(obj.name));
}; };
@ -109,7 +109,7 @@ error.isWebDriverError = function (obj) {
* If |err| is a WebDriverError, it is returned unmodified. * If |err| is a WebDriverError, it is returned unmodified.
* Otherwise an UnknownError type is returned. * Otherwise an UnknownError type is returned.
*/ */
error.wrap = function (err) { error.wrap = function(err) {
if (error.isWebDriverError(err)) { if (error.isWebDriverError(err)) {
return err; return err;
} }
@ -120,7 +120,7 @@ error.wrap = function (err) {
* Unhandled error reporter. Dumps the error and its stacktrace to console, * Unhandled error reporter. Dumps the error and its stacktrace to console,
* and reports error to the Browser Console. * and reports error to the Browser Console.
*/ */
error.report = function (err) { error.report = function(err) {
let msg = "Marionette threw an error: " + error.stringify(err); let msg = "Marionette threw an error: " + error.stringify(err);
dump(msg + "\n"); dump(msg + "\n");
if (Cu.reportError) { if (Cu.reportError) {
@ -131,7 +131,7 @@ error.report = function (err) {
/** /**
* Prettifies an instance of Error and its stacktrace to a string. * Prettifies an instance of Error and its stacktrace to a string.
*/ */
error.stringify = function (err) { error.stringify = function(err) {
try { try {
let s = err.toString(); let s = err.toString();
if ("stack" in err) { if ("stack" in err) {
@ -148,16 +148,17 @@ error.stringify = function (err) {
* *
* Usage: * Usage:
* *
* const {pprint} = Cu.import("chrome://marionette/content/error.js", {});
* let bool = {value: true}; * let bool = {value: true};
* error.pprint`Expected boolean, got ${bool}`; * pprint`Expected boolean, got ${bool}`;
* => 'Expected boolean, got [object Object] {"value": true}' * => 'Expected boolean, got [object Object] {"value": true}'
* *
* let htmlElement = document.querySelector("input#foo"); * let htmlElement = document.querySelector("input#foo");
* error.pprint`Expected element ${htmlElement}`; * pprint`Expected element ${htmlElement}`;
* => 'Expected element <input id="foo" class="bar baz">' * => 'Expected element <input id="foo" class="bar baz">'
*/ */
error.pprint = function (ss, ...values) { error.pprint = function(ss, ...values) {
function prettyObject (obj) { function prettyObject(obj) {
let proto = Object.prototype.toString.call(obj); let proto = Object.prototype.toString.call(obj);
let s = ""; let s = "";
try { try {
@ -172,7 +173,7 @@ error.pprint = function (ss, ...values) {
return proto + " " + s; return proto + " " + s;
} }
function prettyElement (el) { function prettyElement(el) {
let ident = []; let ident = [];
if (el.id) { if (el.id) {
ident.push(`id="${el.id}"`); ident.push(`id="${el.id}"`);
@ -194,7 +195,6 @@ error.pprint = function (ss, ...values) {
res.push(ss[i]); res.push(ss[i]);
if (i < values.length) { if (i < values.length) {
let val = values[i]; let val = values[i];
let typ = Object.prototype.toString.call(val);
let s; let s;
try { try {
if (val && val.nodeType === 1) { if (val && val.nodeType === 1) {
@ -222,7 +222,7 @@ class WebDriverError extends Error {
* Optional string describing error situation or Error instance * Optional string describing error situation or Error instance
* to propagate. * to propagate.
*/ */
constructor (x) { constructor(x) {
super(x); super(x);
this.name = this.constructor.name; this.name = this.constructor.name;
this.status = "webdriver error"; this.status = "webdriver error";
@ -233,7 +233,7 @@ class WebDriverError extends Error {
} }
} }
toJSON () { toJSON() {
return { return {
error: this.status, error: this.status,
message: this.message || "", message: this.message || "",
@ -241,7 +241,7 @@ class WebDriverError extends Error {
} }
} }
static fromJSON (json) { static fromJSON(json) {
if (typeof json.error == "undefined") { if (typeof json.error == "undefined") {
let s = JSON.stringify(json); let s = JSON.stringify(json);
throw new TypeError("Undeserialisable error type: " + s); throw new TypeError("Undeserialisable error type: " + s);
@ -263,7 +263,7 @@ class WebDriverError extends Error {
} }
class ElementNotAccessibleError extends WebDriverError { class ElementNotAccessibleError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "element not accessible"; this.status = "element not accessible";
} }
@ -281,7 +281,7 @@ class ElementNotAccessibleError extends WebDriverError {
* will produce a nicer error message. * will produce a nicer error message.
*/ */
class ElementClickInterceptedError extends WebDriverError { class ElementClickInterceptedError extends WebDriverError {
constructor (obscuredEl = undefined, coords = undefined) { constructor(obscuredEl = undefined, coords = undefined) {
let msg = ""; let msg = "";
if (obscuredEl && coords) { if (obscuredEl && coords) {
const doc = obscuredEl.ownerDocument; const doc = obscuredEl.ownerDocument;
@ -311,49 +311,49 @@ class ElementClickInterceptedError extends WebDriverError {
} }
class ElementNotInteractableError extends WebDriverError { class ElementNotInteractableError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "element not interactable"; this.status = "element not interactable";
} }
} }
class InsecureCertificateError extends WebDriverError { class InsecureCertificateError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "insecure certificate"; this.status = "insecure certificate";
} }
} }
class InvalidArgumentError extends WebDriverError { class InvalidArgumentError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "invalid argument"; this.status = "invalid argument";
} }
} }
class InvalidCookieDomainError extends WebDriverError { class InvalidCookieDomainError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "invalid cookie domain"; this.status = "invalid cookie domain";
} }
} }
class InvalidElementStateError extends WebDriverError { class InvalidElementStateError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "invalid element state"; this.status = "invalid element state";
} }
} }
class InvalidSelectorError extends WebDriverError { class InvalidSelectorError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "invalid selector"; this.status = "invalid selector";
} }
} }
class InvalidSessionIDError extends WebDriverError { class InvalidSessionIDError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "invalid session id"; this.status = "invalid session id";
} }
@ -412,98 +412,98 @@ class JavaScriptError extends WebDriverError {
} }
class MoveTargetOutOfBoundsError extends WebDriverError { class MoveTargetOutOfBoundsError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "move target out of bounds"; this.status = "move target out of bounds";
} }
} }
class NoAlertOpenError extends WebDriverError { class NoAlertOpenError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "no such alert"; this.status = "no such alert";
} }
} }
class NoSuchElementError extends WebDriverError { class NoSuchElementError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "no such element"; this.status = "no such element";
} }
} }
class NoSuchFrameError extends WebDriverError { class NoSuchFrameError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "no such frame"; this.status = "no such frame";
} }
} }
class NoSuchWindowError extends WebDriverError { class NoSuchWindowError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "no such window"; this.status = "no such window";
} }
} }
class ScriptTimeoutError extends WebDriverError { class ScriptTimeoutError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "script timeout"; this.status = "script timeout";
} }
} }
class SessionNotCreatedError extends WebDriverError { class SessionNotCreatedError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "session not created"; this.status = "session not created";
} }
} }
class StaleElementReferenceError extends WebDriverError { class StaleElementReferenceError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "stale element reference"; this.status = "stale element reference";
} }
} }
class TimeoutError extends WebDriverError { class TimeoutError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "timeout"; this.status = "timeout";
} }
} }
class UnableToSetCookieError extends WebDriverError { class UnableToSetCookieError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "unable to set cookie"; this.status = "unable to set cookie";
} }
} }
class UnexpectedAlertOpenError extends WebDriverError { class UnexpectedAlertOpenError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "unexpected alert open"; this.status = "unexpected alert open";
} }
} }
class UnknownCommandError extends WebDriverError { class UnknownCommandError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "unknown command"; this.status = "unknown command";
} }
} }
class UnknownError extends WebDriverError { class UnknownError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "unknown error"; this.status = "unknown error";
} }
} }
class UnsupportedOperationError extends WebDriverError { class UnsupportedOperationError extends WebDriverError {
constructor (message) { constructor(message) {
super(message); super(message);
this.status = "unsupported operation"; this.status = "unsupported operation";
} }

View File

@ -70,7 +70,7 @@ this.evaluate = {};
* *
* @param {nsISandbox) sb * @param {nsISandbox) sb
* The sandbox the script will be evaluted in. * The sandbox the script will be evaluted in.
* @param {string} script * @param {string} script
* The script to evaluate. * The script to evaluate.
* @param {Array.<?>=} args * @param {Array.<?>=} args
* A sequence of arguments to call the script with. * A sequence of arguments to call the script with.
@ -99,12 +99,12 @@ this.evaluate = {};
* the script. Note that the return value requires serialisation before * the script. Note that the return value requires serialisation before
* it can be sent to the client. * it can be sent to the client.
* *
* @throws JavaScriptError * @throws {JavaScriptError}
* If an Error was thrown whilst evaluating the script. * If an Error was thrown whilst evaluating the script.
* @throws ScriptTimeoutError * @throws {ScriptTimeoutError}
* If the script was interrupted due to script timeout. * If the script was interrupted due to script timeout.
*/ */
evaluate.sandbox = function (sb, script, args = [], opts = {}) { evaluate.sandbox = function(sb, script, args = [], opts = {}) {
let scriptTimeoutID, timeoutHandler, unloadHandler; let scriptTimeoutID, timeoutHandler, unloadHandler;
let promise = new Promise((resolve, reject) => { let promise = new Promise((resolve, reject) => {
@ -112,7 +112,7 @@ evaluate.sandbox = function (sb, script, args = [], opts = {}) {
sb[COMPLETE] = resolve; sb[COMPLETE] = resolve;
timeoutHandler = () => reject(new ScriptTimeoutError("Timed out")); timeoutHandler = () => reject(new ScriptTimeoutError("Timed out"));
unloadHandler = sandbox.cloneInto( unloadHandler = sandbox.cloneInto(
() => reject(new JavaScriptError("Document was unloaded during execution")), () => reject(new JavaScriptError("Document was unloaded")),
sb); sb);
// wrap in function // wrap in function
@ -151,12 +151,16 @@ evaluate.sandbox = function (sb, script, args = [], opts = {}) {
} }
// timeout and unload handlers // timeout and unload handlers
scriptTimeoutID = setTimeout(timeoutHandler, opts.timeout || DEFAULT_TIMEOUT); const timeout = opts.timeout || DEFAULT_TIMEOUT;
scriptTimeoutID = setTimeout(timeoutHandler, timeout);
sb.window.onunload = unloadHandler; sb.window.onunload = unloadHandler;
const file = opts.filename || "dummy file";
const line = opts.line || 0;
let res; let res;
try { try {
res = Cu.evalInSandbox(src, sb, "1.8", opts.filename || "dummy file", 0); res = Cu.evalInSandbox(src, sb, "1.8", file, 0);
} catch (e) { } catch (e) {
let err = new JavaScriptError(e, { let err = new JavaScriptError(e, {
fnName: "execute_script", fnName: "execute_script",
@ -196,42 +200,42 @@ evaluate.sandbox = function (sb, script, args = [], opts = {}) {
* Same object as provided by |obj| with the web elements replaced * Same object as provided by |obj| with the web elements replaced
* by DOM elements. * by DOM elements.
*/ */
evaluate.fromJSON = function (obj, seenEls, win, shadowRoot = undefined) { evaluate.fromJSON = function(obj, seenEls, win, shadowRoot = undefined) {
switch (typeof obj) { switch (typeof obj) {
case "boolean": case "boolean":
case "number": case "number":
case "string": case "string":
default:
return obj; return obj;
case "object": case "object":
if (obj === null) { if (obj === null) {
return obj; return obj;
}
// arrays // arrays
else if (Array.isArray(obj)) { } else if (Array.isArray(obj)) {
return obj.map(e => evaluate.fromJSON(e, seenEls, win, shadowRoot)); return obj.map(e => evaluate.fromJSON(e, seenEls, win, shadowRoot));
}
// web elements // web elements
else if (Object.keys(obj).includes(element.Key) || } else if (Object.keys(obj).includes(element.Key) ||
Object.keys(obj).includes(element.LegacyKey)) { Object.keys(obj).includes(element.LegacyKey)) {
/* eslint-disable */
let uuid = obj[element.Key] || obj[element.LegacyKey]; let uuid = obj[element.Key] || obj[element.LegacyKey];
let el = seenEls.get(uuid, {frame: win, shadowRoot: shadowRoot}); let el = seenEls.get(uuid, {frame: win, shadowRoot: shadowRoot});
/* eslint-enable */
if (!el) { if (!el) {
throw new WebDriverError(`Unknown element: ${uuid}`); throw new WebDriverError(`Unknown element: ${uuid}`);
} }
return el; return el;
} }
// arbitrary objects // arbitrary objects
else { let rv = {};
let rv = {}; for (let prop in obj) {
for (let prop in obj) { rv[prop] = evaluate.fromJSON(obj[prop], seenEls, win, shadowRoot);
rv[prop] = evaluate.fromJSON(obj[prop], seenEls, win, shadowRoot);
}
return rv;
} }
return rv;
} }
}; };
@ -251,32 +255,30 @@ evaluate.fromJSON = function (obj, seenEls, win, shadowRoot = undefined) {
* Same object as provided by |obj| with the elements replaced by * Same object as provided by |obj| with the elements replaced by
* web elements. * web elements.
*/ */
evaluate.toJSON = function (obj, seenEls) { evaluate.toJSON = function(obj, seenEls) {
const t = Object.prototype.toString.call(obj); const t = Object.prototype.toString.call(obj);
// null // null
if (t == "[object Undefined]" || t == "[object Null]") { if (t == "[object Undefined]" || t == "[object Null]") {
return null; return null;
}
// literals // literals
else if (t == "[object Boolean]" || t == "[object Number]" || t == "[object String]") { } else if (t == "[object Boolean]" ||
t == "[object Number]" ||
t == "[object String]") {
return obj; return obj;
}
// Array, NodeList, HTMLCollection, et al. // Array, NodeList, HTMLCollection, et al.
else if (element.isCollection(obj)) { } else if (element.isCollection(obj)) {
return [...obj].map(el => evaluate.toJSON(el, seenEls)); return [...obj].map(el => evaluate.toJSON(el, seenEls));
}
// HTMLElement // HTMLElement
else if ("nodeType" in obj && obj.nodeType == obj.ELEMENT_NODE) { } else if ("nodeType" in obj && obj.nodeType == obj.ELEMENT_NODE) {
let uuid = seenEls.add(obj); let uuid = seenEls.add(obj);
return element.makeWebElement(uuid); return element.makeWebElement(uuid);
}
// custom JSON representation // custom JSON representation
else if (typeof obj["toJSON"] == "function") { } else if (typeof obj["toJSON"] == "function") {
let unsafeJSON = obj.toJSON(); let unsafeJSON = obj.toJSON();
return evaluate.toJSON(unsafeJSON, seenEls); return evaluate.toJSON(unsafeJSON, seenEls);
} }
@ -293,8 +295,8 @@ evaluate.toJSON = function (obj, seenEls) {
throw e; throw e;
} }
} }
return rv;
} }
return rv;
}; };
this.sandbox = {}; this.sandbox = {};
@ -307,7 +309,7 @@ this.sandbox = {};
* Unlike for |Components.utils.cloneInto|, |obj| may contain functions * Unlike for |Components.utils.cloneInto|, |obj| may contain functions
* and DOM elemnets. * and DOM elemnets.
*/ */
sandbox.cloneInto = function (obj, sb) { sandbox.cloneInto = function(obj, sb) {
return Cu.cloneInto(obj, sb, {cloneFunctions: true, wrapReflectors: true}); return Cu.cloneInto(obj, sb, {cloneFunctions: true, wrapReflectors: true});
}; };
@ -316,7 +318,7 @@ sandbox.cloneInto = function (obj, sb) {
* map property, or a normal map, of function names and function * map property, or a normal map, of function names and function
* references. * references.
* *
* @param {Sandbox} sb * @param {Sandbox} sb
* The sandbox to augment. * The sandbox to augment.
* @param {Object} adapter * @param {Object} adapter
* Object that holds an {@code exports} property, or a map, of * Object that holds an {@code exports} property, or a map, of
@ -325,11 +327,11 @@ sandbox.cloneInto = function (obj, sb) {
* @return {Sandbox} * @return {Sandbox}
* The augmented sandbox. * The augmented sandbox.
*/ */
sandbox.augment = function (sb, adapter) { sandbox.augment = function(sb, adapter) {
function* entries(obj) { function* entries(obj) {
for (let key of Object.keys(obj)) { for (let key of Object.keys(obj)) {
yield [key, obj[key]]; yield [key, obj[key]];
} }
} }
let funcs = adapter.exports || entries(adapter); let funcs = adapter.exports || entries(adapter);
@ -352,7 +354,7 @@ sandbox.augment = function (sb, adapter) {
* @return {Sandbox} * @return {Sandbox}
* The created sandbox. * The created sandbox.
*/ */
sandbox.create = function (window, principal = null, opts = {}) { sandbox.create = function(window, principal = null, opts = {}) {
let p = principal || window; let p = principal || window;
opts = Object.assign({ opts = Object.assign({
sameZoneAs: window, sameZoneAs: window,
@ -373,7 +375,7 @@ sandbox.create = function (window, principal = null, opts = {}) {
* @return {Sandbox} * @return {Sandbox}
* The created sandbox. * The created sandbox.
*/ */
sandbox.createMutable = function (window) { sandbox.createMutable = function(window) {
let opts = { let opts = {
wantComponents: false, wantComponents: false,
wantXrays: false, wantXrays: false,
@ -381,13 +383,13 @@ sandbox.createMutable = function (window) {
return sandbox.create(window, null, opts); return sandbox.create(window, null, opts);
}; };
sandbox.createSystemPrincipal = function (window) { sandbox.createSystemPrincipal = function(window) {
let principal = Cc["@mozilla.org/systemprincipal;1"] let principal = Cc["@mozilla.org/systemprincipal;1"]
.createInstance(Ci.nsIPrincipal); .createInstance(Ci.nsIPrincipal);
return sandbox.create(window, principal); return sandbox.create(window, principal);
}; };
sandbox.createSimpleTest = function (window, harness) { sandbox.createSimpleTest = function(window, harness) {
let sb = sandbox.create(window); let sb = sandbox.create(window);
sb = sandbox.augment(sb, harness); sb = sandbox.augment(sb, harness);
sb[FINISH] = () => sb[COMPLETE](harness.generate_results()); sb[FINISH] = () => sb[COMPLETE](harness.generate_results());

View File

@ -2,7 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Provides functionality for creating and sending DOM events. /** Provides functionality for creating and sending DOM events. */
this.event = {};
"use strict"; "use strict";
/* global content, is */ /* global content, is */
@ -37,8 +38,6 @@ function getDOMWindowUtils(win) {
.getInterface(Ci.nsIDOMWindowUtils); .getInterface(Ci.nsIDOMWindowUtils);
} }
this.event = {};
event.MouseEvents = { event.MouseEvents = {
click: 0, click: 0,
dblclick: 1, dblclick: 1,
@ -68,13 +67,14 @@ event.Modifiers = {
* @throws {TypeError} * @throws {TypeError}
* If the event is unsupported. * If the event is unsupported.
*/ */
event.sendMouseEvent = function (mouseEvent, target, window = undefined) { event.sendMouseEvent = function(mouseEvent, target, window = undefined) {
if (!event.MouseEvents.hasOwnProperty(mouseEvent.type)) { if (!event.MouseEvents.hasOwnProperty(mouseEvent.type)) {
throw new TypeError("Unsupported event type: " + mouseEvent.type); throw new TypeError("Unsupported event type: " + mouseEvent.type);
} }
if (!target.nodeType && typeof target != "string") { if (!target.nodeType && typeof target != "string") {
throw new TypeError("Target can only be a DOM element or a string: " + target); throw new TypeError(
"Target can only be a DOM element or a string: " + target);
} }
if (!target.nodeType) { if (!target.nodeType) {
@ -85,7 +85,6 @@ event.sendMouseEvent = function (mouseEvent, target, window = undefined) {
let ev = window.document.createEvent("MouseEvent"); let ev = window.document.createEvent("MouseEvent");
let type = mouseEvent.type;
let view = window; let view = window;
let detail = mouseEvent.detail; let detail = mouseEvent.detail;
@ -138,7 +137,7 @@ event.sendMouseEvent = function (mouseEvent, target, window = undefined) {
* For now this method only works for English letters (lower and upper * For now this method only works for English letters (lower and upper
* case) and the digits 0-9. * case) and the digits 0-9.
*/ */
event.sendChar = function (char, window = undefined) { event.sendChar = function(char, window = undefined) {
// DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9 // DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9
let hasShift = (char == char.toUpperCase()); let hasShift = (char == char.toUpperCase());
event.synthesizeKey(char, {shiftKey: hasShift}, window); event.synthesizeKey(char, {shiftKey: hasShift}, window);
@ -150,7 +149,7 @@ event.sendChar = function (char, window = undefined) {
* For now this method only works for English letters (lower and upper * For now this method only works for English letters (lower and upper
* case) and the digits 0-9. * case) and the digits 0-9.
*/ */
event.sendString = function (string, window = undefined) { event.sendString = function(string, window = undefined) {
for (let i = 0; i < string.length; ++i) { for (let i = 0; i < string.length; ++i) {
event.sendChar(string.charAt(i), window); event.sendChar(string.charAt(i), window);
} }
@ -163,14 +162,14 @@ event.sendString = function (string, window = undefined) {
* in the nsIDOMKeyEvent constant name for this key. No modifiers are * in the nsIDOMKeyEvent constant name for this key. No modifiers are
* handled at this point. * handled at this point.
*/ */
event.sendKey = function (key, window = undefined) { event.sendKey = function(key, window = undefined) {
let keyName = "VK_" + key.toUpperCase(); let keyName = "VK_" + key.toUpperCase();
event.synthesizeKey(keyName, {shiftKey: false}, window); event.synthesizeKey(keyName, {shiftKey: false}, window);
}; };
// TODO(ato): Unexpose this when action.Chain#emitMouseEvent // TODO(ato): Unexpose this when action.Chain#emitMouseEvent
// no longer emits its own events // no longer emits its own events
event.parseModifiers_ = function (modifiers) { event.parseModifiers_ = function(modifiers) {
let mval = 0; let mval = 0;
if (modifiers.shiftKey) { if (modifiers.shiftKey) {
mval |= Ci.nsIDOMNSEvent.SHIFT_MASK; mval |= Ci.nsIDOMNSEvent.SHIFT_MASK;
@ -217,7 +216,7 @@ event.parseModifiers_ = function (modifiers) {
* @param {Window=} window * @param {Window=} window
* Window object. Defaults to the current window. * Window object. Defaults to the current window.
*/ */
event.synthesizeMouse = function ( event.synthesizeMouse = function(
element, offsetX, offsetY, opts, window = undefined) { element, offsetX, offsetY, opts, window = undefined) {
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
event.synthesizeMouseAtPoint( event.synthesizeMouseAtPoint(
@ -241,7 +240,7 @@ event.synthesizeMouse = function (
* @param {Window=} window * @param {Window=} window
* Window object. Defaults to the current window. * Window object. Defaults to the current window.
*/ */
event.synthesizeMouseAtPoint = function ( event.synthesizeMouseAtPoint = function(
left, top, opts, window = undefined) { left, top, opts, window = undefined) {
let domutils = getDOMWindowUtils(window); let domutils = getDOMWindowUtils(window);
@ -254,21 +253,60 @@ event.synthesizeMouseAtPoint = function (
Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE; Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE;
let isDOMEventSynthesized = let isDOMEventSynthesized =
("isSynthesized" in opts) ? opts.isSynthesized : true; ("isSynthesized" in opts) ? opts.isSynthesized : true;
let isWidgetEventSynthesized = let isWidgetEventSynthesized;
("isWidgetEventSynthesized" in opts) ? opts.isWidgetEventSynthesized : false; if ("isWidgetEventSynthesized" in opts) {
let buttons = ("buttons" in opts) ? opts.buttons : domutils.MOUSE_BUTTONS_NOT_SPECIFIED; isWidgetEventSynthesized = opts.isWidgetEventSynthesized;
} else {
isWidgetEventSynthesized = false;
}
let buttons;
if ("buttons" in opts) {
buttons = opts.buttons;
} else {
buttons = domutils.MOUSE_BUTTONS_NOT_SPECIFIED;
}
if (("type" in opts) && opts.type) { if (("type" in opts) && opts.type) {
domutils.sendMouseEvent( domutils.sendMouseEvent(
opts.type, left, top, button, clickCount, modifiers, false, pressure, inputSource, opts.type,
isDOMEventSynthesized, isWidgetEventSynthesized, buttons); left,
top,
button,
clickCount,
modifiers,
false,
pressure,
inputSource,
isDOMEventSynthesized,
isWidgetEventSynthesized,
buttons);
} else { } else {
domutils.sendMouseEvent( domutils.sendMouseEvent(
"mousedown", left, top, button, clickCount, modifiers, false, pressure, inputSource, "mousedown",
isDOMEventSynthesized, isWidgetEventSynthesized, buttons); left,
top,
button,
clickCount,
modifiers,
false,
pressure,
inputSource,
isDOMEventSynthesized,
isWidgetEventSynthesized,
buttons);
domutils.sendMouseEvent( domutils.sendMouseEvent(
"mouseup", left, top, button, clickCount, modifiers, false, pressure, inputSource, "mouseup",
isDOMEventSynthesized, isWidgetEventSynthesized, buttons); left,
top,
button,
clickCount,
modifiers,
false,
pressure,
inputSource,
isDOMEventSynthesized,
isWidgetEventSynthesized,
buttons);
} }
}; };
@ -276,7 +314,7 @@ event.synthesizeMouseAtPoint = function (
* Call event.synthesizeMouse with coordinates at the centre of the * Call event.synthesizeMouse with coordinates at the centre of the
* target. * target.
*/ */
event.synthesizeMouseAtCenter = function (element, event, window) { event.synthesizeMouseAtCenter = function(element, event, window) {
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
event.synthesizeMouse( event.synthesizeMouse(
element, element,
@ -286,6 +324,7 @@ event.synthesizeMouseAtCenter = function (element, event, window) {
window); window);
}; };
/* eslint-disable */
function computeKeyCodeFromChar_(char) { function computeKeyCodeFromChar_(char) {
if (char.length != 1) { if (char.length != 1) {
return 0; return 0;
@ -388,6 +427,7 @@ function computeKeyCodeFromChar_(char) {
return 0; return 0;
} }
} }
/* eslint-enable */
/** /**
* Returns true if the given key should cause keypress event when widget * Returns true if the given key should cause keypress event when widget
@ -396,7 +436,7 @@ function computeKeyCodeFromChar_(char) {
* The key code should be one of consts of nsIDOMKeyEvent.DOM_VK_*, * The key code should be one of consts of nsIDOMKeyEvent.DOM_VK_*,
* or a key name begins with "VK_", or a character. * or a key name begins with "VK_", or a character.
*/ */
event.isKeypressFiredKey = function (key) { event.isKeypressFiredKey = function(key) {
if (typeof key == "string") { if (typeof key == "string") {
if (key.indexOf("VK_") === 0) { if (key.indexOf("VK_") === 0) {
key = Ci.nsIDOMKeyEvent["DOM_" + key]; key = Ci.nsIDOMKeyEvent["DOM_" + key];
@ -445,8 +485,7 @@ event.isKeypressFiredKey = function (key) {
* @throws {TypeError} * @throws {TypeError}
* If unknown key. * If unknown key.
*/ */
event.synthesizeKey = function (key, event, win = undefined) event.synthesizeKey = function(key, event, win = undefined) {
{
var TIP = getTIP_(win); var TIP = getTIP_(win);
if (!TIP) { if (!TIP) {
return; return;
@ -458,7 +497,7 @@ event.synthesizeKey = function (key, event, win = undefined)
var dispatchKeydown = var dispatchKeydown =
!("type" in event) || event.type === "keydown" || !event.type; !("type" in event) || event.type === "keydown" || !event.type;
var dispatchKeyup = var dispatchKeyup =
!("type" in event) || event.type === "keyup" || !event.type; !("type" in event) || event.type === "keyup" || !event.type;
try { try {
if (dispatchKeydown) { if (dispatchKeydown) {
@ -481,8 +520,7 @@ event.synthesizeKey = function (key, event, win = undefined)
var TIPMap = new WeakMap(); var TIPMap = new WeakMap();
function getTIP_(win, callback) function getTIP_(win, callback) {
{
if (!win) { if (!win) {
win = window; win = window;
} }
@ -502,12 +540,12 @@ function getTIP_(win, callback)
return tip; return tip;
} }
function getKeyboardEvent_(win = window) function getKeyboardEvent_(win = window) {
{
if (typeof KeyboardEvent != "undefined") { if (typeof KeyboardEvent != "undefined") {
try { try {
// See if the object can be instantiated; sometimes this yields // See if the object can be instantiated; sometimes this yields
// 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'. // 'TypeError: can't access dead object' or 'KeyboardEvent is not
// a constructor'.
new KeyboardEvent("", {}); new KeyboardEvent("", {});
return KeyboardEvent; return KeyboardEvent;
} catch (ex) {} } catch (ex) {}
@ -519,8 +557,9 @@ function getKeyboardEvent_(win = window)
} }
function createKeyboardEventDictionary_(key, keyEvent, win = window) { function createKeyboardEventDictionary_(key, keyEvent, win = window) {
var result = { dictionary: null, flags: 0 }; var result = {dictionary: null, flags: 0};
var keyCodeIsDefined = "keyCode" in keyEvent && keyEvent.keyCode != undefined; var keyCodeIsDefined = "keyCode" in keyEvent &&
keyEvent.keyCode != undefined;
var keyCode = var keyCode =
(keyCodeIsDefined && keyEvent.keyCode >= 0 && keyEvent.keyCode <= 255) ? (keyCodeIsDefined && keyEvent.keyCode >= 0 && keyEvent.keyCode <= 255) ?
keyEvent.keyCode : 0; keyEvent.keyCode : 0;
@ -535,7 +574,7 @@ function createKeyboardEventDictionary_(key, keyEvent, win = window) {
} }
keyName = guessKeyNameFromKeyCode_(keyCode, win); keyName = guessKeyNameFromKeyCode_(keyCode, win);
if (!isPrintable(keyCode, win)) { if (!isPrintable(keyCode, win)) {
result.flags |= Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; result.flags |= Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
} }
} else if (key != "") { } else if (key != "") {
keyName = key; keyName = key;
@ -559,92 +598,90 @@ function createKeyboardEventDictionary_(key, keyEvent, win = window) {
code: "code" in keyEvent ? keyEvent.code : "", code: "code" in keyEvent ? keyEvent.code : "",
location: locationIsDefined ? keyEvent.location : 0, location: locationIsDefined ? keyEvent.location : 0,
repeat: "repeat" in keyEvent ? keyEvent.repeat === true : false, repeat: "repeat" in keyEvent ? keyEvent.repeat === true : false,
keyCode: keyCode, keyCode,
}; };
return result; return result;
} }
function emulateToActivateModifiers_(TIP, keyEvent, win = window) function emulateToActivateModifiers_(TIP, keyEvent, win = window) {
{
if (!keyEvent) { if (!keyEvent) {
return null; return null;
} }
var KeyboardEvent = getKeyboardEvent_(win); var KeyboardEvent = getKeyboardEvent_(win);
var navigator = getNavigator_(win);
var modifiers = { var modifiers = {
normal: [ normal: [
{ key: "Alt", attr: "altKey" }, {key: "Alt", attr: "altKey"},
{ key: "AltGraph", attr: "altGraphKey" }, {key: "AltGraph", attr: "altGraphKey"},
{ key: "Control", attr: "ctrlKey" }, {key: "Control", attr: "ctrlKey"},
{ key: "Fn", attr: "fnKey" }, {key: "Fn", attr: "fnKey"},
{ key: "Meta", attr: "metaKey" }, {key: "Meta", attr: "metaKey"},
{ key: "OS", attr: "osKey" }, {key: "OS", attr: "osKey"},
{ key: "Shift", attr: "shiftKey" }, {key: "Shift", attr: "shiftKey"},
{ key: "Symbol", attr: "symbolKey" }, {key: "Symbol", attr: "symbolKey"},
{ key: isMac_(win) ? "Meta" : "Control", {key: isMac_(win) ? "Meta" : "Control", attr: "accelKey"},
attr: "accelKey" },
], ],
lockable: [ lockable: [
{ key: "CapsLock", attr: "capsLockKey" }, {key: "CapsLock", attr: "capsLockKey"},
{ key: "FnLock", attr: "fnLockKey" }, {key: "FnLock", attr: "fnLockKey"},
{ key: "NumLock", attr: "numLockKey" }, {key: "NumLock", attr: "numLockKey"},
{ key: "ScrollLock", attr: "scrollLockKey" }, {key: "ScrollLock", attr: "scrollLockKey"},
{ key: "SymbolLock", attr: "symbolLockKey" }, {key: "SymbolLock", attr: "symbolLockKey"},
] ],
} }
for (var i = 0; i < modifiers.normal.length; i++) { for (let i = 0; i < modifiers.normal.length; i++) {
if (!keyEvent[modifiers.normal[i].attr]) { if (!keyEvent[modifiers.normal[i].attr]) {
continue; continue;
} }
if (TIP.getModifierState(modifiers.normal[i].key)) { if (TIP.getModifierState(modifiers.normal[i].key)) {
continue; // already activated. continue; // already activated.
} }
var event = new KeyboardEvent("", { key: modifiers.normal[i].key }); let event = new KeyboardEvent("", {key: modifiers.normal[i].key});
TIP.keydown(event, TIP.keydown(event,
TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
modifiers.normal[i].activated = true; modifiers.normal[i].activated = true;
} }
for (var i = 0; i < modifiers.lockable.length; i++) {
if (!keyEvent[modifiers.lockable[i].attr]) { for (let j = 0; j < modifiers.lockable.length; j++) {
if (!keyEvent[modifiers.lockable[j].attr]) {
continue; continue;
} }
if (TIP.getModifierState(modifiers.lockable[i].key)) { if (TIP.getModifierState(modifiers.lockable[j].key)) {
continue; // already activated. continue; // already activated.
} }
var event = new KeyboardEvent("", { key: modifiers.lockable[i].key }); let event = new KeyboardEvent("", {key: modifiers.lockable[j].key});
TIP.keydown(event, TIP.keydown(event,
TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
TIP.keyup(event, TIP.keyup(event,
TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
modifiers.lockable[i].activated = true; modifiers.lockable[j].activated = true;
} }
return modifiers; return modifiers;
} }
function emulateToInactivateModifiers_(TIP, modifiers, win = window) function emulateToInactivateModifiers_(TIP, modifiers, win = window) {
{
if (!modifiers) { if (!modifiers) {
return; return;
} }
var KeyboardEvent = getKeyboardEvent_(win); let KeyboardEvent = getKeyboardEvent_(win);
for (var i = 0; i < modifiers.normal.length; i++) { for (let i = 0; i < modifiers.normal.length; i++) {
if (!modifiers.normal[i].activated) { if (!modifiers.normal[i].activated) {
continue; continue;
} }
var event = new KeyboardEvent("", { key: modifiers.normal[i].key }); let event = new KeyboardEvent("", {key: modifiers.normal[i].key});
TIP.keyup(event, TIP.keyup(event,
TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
} }
for (var i = 0; i < modifiers.lockable.length; i++) { for (let j = 0; j < modifiers.lockable.length; j++) {
if (!modifiers.lockable[i].activated) { if (!modifiers.lockable[j].activated) {
continue; continue;
} }
if (!TIP.getModifierState(modifiers.lockable[i].key)) { if (!TIP.getModifierState(modifiers.lockable[j].key)) {
continue; // who already inactivated this? continue; // who already inactivated this?
} }
var event = new KeyboardEvent("", { key: modifiers.lockable[i].key }); let event = new KeyboardEvent("", {key: modifiers.lockable[j].key});
TIP.keydown(event, TIP.keydown(event,
TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
TIP.keyup(event, TIP.keyup(event,
@ -661,8 +698,8 @@ function isMac_(win = window) {
return navigator.platform.indexOf("Mac") > -1; return navigator.platform.indexOf("Mac") > -1;
} }
function guessKeyNameFromKeyCode_(aKeyCode, win = window) /* eslint-disable */
{ function guessKeyNameFromKeyCode_(aKeyCode, win = window) {
var KeyboardEvent = getKeyboardEvent_(win); var KeyboardEvent = getKeyboardEvent_(win);
switch (aKeyCode) { switch (aKeyCode) {
case KeyboardEvent.DOM_VK_CANCEL: case KeyboardEvent.DOM_VK_CANCEL:
@ -807,11 +844,13 @@ function guessKeyNameFromKeyCode_(aKeyCode, win = window)
return "Unidentified"; return "Unidentified";
} }
} }
/* eslint-enable */
/** /**
* Indicate that an event with an original target and type is expected * Indicate that an event with an original target and type is expected
* to be fired, or not expected to be fired. * to be fired, or not expected to be fired.
*/ */
/* eslint-disable */
function expectEvent_(expectedTarget, expectedEvent, testName) { function expectEvent_(expectedTarget, expectedEvent, testName) {
if (!expectedTarget || !expectedEvent) { if (!expectedTarget || !expectedEvent) {
return null; return null;
@ -835,6 +874,7 @@ function expectEvent_(expectedTarget, expectedEvent, testName) {
expectedTarget.addEventListener(type, handler); expectedTarget.addEventListener(type, handler);
return handler; return handler;
} }
/* eslint-enable */
/** /**
* Check if the event was fired or not. The provided event handler will * Check if the event was fired or not. The provided event handler will
@ -887,7 +927,7 @@ function checkExpectedEvent_(
* @param {Window=} window * @param {Window=} window
* Window object. Defaults to the current window. * Window object. Defaults to the current window.
*/ */
event.synthesizeMouseExpectEvent = function ( event.synthesizeMouseExpectEvent = function(
target, offsetX, offsetY, ev, expectedTarget, expectedEvent, target, offsetX, offsetY, ev, expectedTarget, expectedEvent,
testName, window = undefined) { testName, window = undefined) {
@ -926,7 +966,7 @@ event.synthesizeMouseExpectEvent = function (
* *
* aWindow is optional, and defaults to the current window object. * aWindow is optional, and defaults to the current window object.
*/ */
event.synthesizeKeyExpectEvent = function ( event.synthesizeKeyExpectEvent = function(
key, ev, expectedTarget, expectedEvent, testName, key, ev, expectedTarget, expectedEvent, testName,
window = undefined) { window = undefined) {
@ -955,7 +995,7 @@ event.synthesizeKeyExpectEvent = function (
* @param {Window=} window * @param {Window=} window
* Window object. Defaults to the current window. * Window object. Defaults to the current window.
*/ */
event.synthesizeComposition = function (ev, window = undefined) { event.synthesizeComposition = function(ev, window = undefined) {
let domutils = getDOMWindowUtils(window); let domutils = getDOMWindowUtils(window);
domutils.sendCompositionEvent(ev.type, ev.data || "", ev.locale || ""); domutils.sendCompositionEvent(ev.type, ev.data || "", ev.locale || "");
}; };
@ -1000,7 +1040,7 @@ event.synthesizeComposition = function (ev, window = undefined) {
* @param {Window=} window * @param {Window=} window
* Window object. Defaults to the current window. * Window object. Defaults to the current window.
*/ */
event.synthesizeText = function (ev, window = undefined) { event.synthesizeText = function(ev, window = undefined) {
let domutils = getDOMWindowUtils(window); let domutils = getDOMWindowUtils(window);
if (!ev.composition || if (!ev.composition ||
@ -1052,7 +1092,7 @@ event.synthesizeText = function (ev, window = undefined) {
* @return {(nsIQueryContentEventResult|null)} * @return {(nsIQueryContentEventResult|null)}
* Event's result, or null if it failed. * Event's result, or null if it failed.
*/ */
event.synthesizeQuerySelectedText = function (window = undefined) { event.synthesizeQuerySelectedText = function(window = undefined) {
let domutils = getDOMWindowUtils(window); let domutils = getDOMWindowUtils(window);
return domutils.sendQueryContentEvent( return domutils.sendQueryContentEvent(
domutils.QUERY_SELECTED_TEXT, 0, 0, 0, 0); domutils.QUERY_SELECTED_TEXT, 0, 0, 0, 0);
@ -1075,7 +1115,7 @@ event.synthesizeQuerySelectedText = function (window = undefined) {
* *
* @return True, if succeeded. Otherwise false. * @return True, if succeeded. Otherwise false.
*/ */
event.synthesizeSelectionSet = function ( event.synthesizeSelectionSet = function(
offset, length, reverse, window = undefined) { offset, length, reverse, window = undefined) {
let domutils = getDOMWindowUtils(window); let domutils = getDOMWindowUtils(window);
return domutils.sendSelectionSetEvent(offset, length, reverse); return domutils.sendSelectionSetEvent(offset, length, reverse);
@ -1228,19 +1268,21 @@ function isPrintable(c, win = window) {
return !(NON_PRINT_KEYS.includes(c)); return !(NON_PRINT_KEYS.includes(c));
} }
event.sendKeyDown = function (keyToSend, modifiers, document) { event.sendKeyDown = function(keyToSend, modifiers, document) {
modifiers.type = "keydown"; modifiers.type = "keydown";
event.sendSingleKey(keyToSend, modifiers, document); event.sendSingleKey(keyToSend, modifiers, document);
// TODO This doesn't do anything since |synthesizeKeyEvent| ignores explicit // TODO: This doesn't do anything since |synthesizeKeyEvent| ignores
// keypress request, and instead figures out itself when to send keypress // explicit keypress request, and instead figures out itself when to
if (["VK_SHIFT", "VK_CONTROL", "VK_ALT", "VK_META"].indexOf(getKeyCode(keyToSend)) < 0) { // send keypress.
if (["VK_SHIFT", "VK_CONTROL", "VK_ALT", "VK_META"]
.indexOf(getKeyCode(keyToSend)) < 0) {
modifiers.type = "keypress"; modifiers.type = "keypress";
event.sendSingleKey(keyToSend, modifiers, document); event.sendSingleKey(keyToSend, modifiers, document);
} }
delete modifiers.type; delete modifiers.type;
}; };
event.sendKeyUp = function (keyToSend, modifiers, window = undefined) { event.sendKeyUp = function(keyToSend, modifiers, window = undefined) {
modifiers.type = "keyup"; modifiers.type = "keyup";
event.sendSingleKey(keyToSend, modifiers, window); event.sendSingleKey(keyToSend, modifiers, window);
delete modifiers.type; delete modifiers.type;
@ -1253,17 +1295,18 @@ event.sendKeyUp = function (keyToSend, modifiers, window = undefined) {
* Code point or normalized key value * Code point or normalized key value
* @param {?} modifiers * @param {?} modifiers
* Object with properties used in KeyboardEvent (shiftkey, repeat, ...) * Object with properties used in KeyboardEvent (shiftkey, repeat, ...)
* as well as, the event |type| such as keydown. All properties are optional. * as well as, the event |type| such as keydown. All properties
* are optional.
* @param {Window=} window * @param {Window=} window
* Window object. If |window| is undefined, the event is synthesized in * Window object. If |window| is undefined, the event is synthesized
* current window. * in current window.
*/ */
event.sendSingleKey = function (keyToSend, modifiers, window = undefined) { event.sendSingleKey = function(keyToSend, modifiers, window = undefined) {
let keyName = getKeyCode(keyToSend); let keyName = getKeyCode(keyToSend);
if (keyName in KEYCODES_LOOKUP) { if (keyName in KEYCODES_LOOKUP) {
// We assume that if |keyToSend| is a raw code point (like "\uE009") then // We assume that if |keyToSend| is a raw code point (like "\uE009")
// |modifiers| does not already have correct value for corresponding // then |modifiers| does not already have correct value for corresponding
// |modName| attribute (like ctrlKey), so that value needs to be flipped // |modName| attribute (like ctrlKey), so that value needs to be flipped.
let modName = KEYCODES_LOOKUP[keyName]; let modName = KEYCODES_LOOKUP[keyName];
modifiers[modName] = !modifiers[modName]; modifiers[modName] = !modifiers[modName];
} else if (modifiers.shiftKey && keyName != "Shift") { } else if (modifiers.shiftKey && keyName != "Shift") {
@ -1296,7 +1339,7 @@ function focusElement(element) {
* @param {Object.<string, boolean>=} opts * @param {Object.<string, boolean>=} opts
* @param {Window=} window * @param {Window=} window
*/ */
event.sendKeysToElement = function ( event.sendKeysToElement = function(
keyString, el, opts = {}, window = undefined) { keyString, el, opts = {}, window = undefined) {
if (opts.ignoreVisibility || element.isVisible(el)) { if (opts.ignoreVisibility || element.isVisible(el)) {
@ -1318,7 +1361,7 @@ event.sendKeysToElement = function (
} }
}; };
event.sendEvent = function (eventType, el, modifiers = {}, opts = {}) { event.sendEvent = function(eventType, el, modifiers = {}, opts = {}) {
opts.canBubble = opts.canBubble || true; opts.canBubble = opts.canBubble || true;
let doc = el.ownerDocument || el.document; let doc = el.ownerDocument || el.document;
@ -1333,7 +1376,7 @@ event.sendEvent = function (eventType, el, modifiers = {}, opts = {}) {
el.dispatchEvent(ev); el.dispatchEvent(ev);
}; };
event.focus = function (el, opts = {}) { event.focus = function(el, opts = {}) {
opts.canBubble = opts.canBubble || true; opts.canBubble = opts.canBubble || true;
let doc = el.ownerDocument || el.document; let doc = el.ownerDocument || el.document;
let win = doc.defaultView; let win = doc.defaultView;
@ -1343,30 +1386,30 @@ event.focus = function (el, opts = {}) {
el.dispatchEvent(ev); el.dispatchEvent(ev);
}; };
event.mouseover = function (el, modifiers = {}, opts = {}) { event.mouseover = function(el, modifiers = {}, opts = {}) {
return event.sendEvent("mouseover", el, modifiers, opts); return event.sendEvent("mouseover", el, modifiers, opts);
}; };
event.mousemove = function (el, modifiers = {}, opts = {}) { event.mousemove = function(el, modifiers = {}, opts = {}) {
return event.sendEvent("mousemove", el, modifiers, opts); return event.sendEvent("mousemove", el, modifiers, opts);
}; };
event.mousedown = function (el, modifiers = {}, opts = {}) { event.mousedown = function(el, modifiers = {}, opts = {}) {
return event.sendEvent("mousedown", el, modifiers, opts); return event.sendEvent("mousedown", el, modifiers, opts);
}; };
event.mouseup = function (el, modifiers = {}, opts = {}) { event.mouseup = function(el, modifiers = {}, opts = {}) {
return event.sendEvent("mouseup", el, modifiers, opts); return event.sendEvent("mouseup", el, modifiers, opts);
}; };
event.click = function (el, modifiers = {}, opts = {}) { event.click = function(el, modifiers = {}, opts = {}) {
return event.sendEvent("click", el, modifiers, opts); return event.sendEvent("click", el, modifiers, opts);
}; };
event.change = function (el, modifiers = {}, opts = {}) { event.change = function(el, modifiers = {}, opts = {}) {
return event.sendEvent("change", el, modifiers, opts); return event.sendEvent("change", el, modifiers, opts);
}; };
event.input = function (el, modifiers = {}, opts = {}) { event.input = function(el, modifiers = {}, opts = {}) {
return event.sendEvent("input", el, modifiers, opts); return event.sendEvent("input", el, modifiers, opts);
}; };

View File

@ -22,7 +22,7 @@ var remoteFrames = [];
* An object representing a frame that Marionette has loaded a * An object representing a frame that Marionette has loaded a
* frame script in. * frame script in.
*/ */
frame.RemoteFrame = function (windowId, frameId) { frame.RemoteFrame = function(windowId, frameId) {
// outerWindowId relative to main process // outerWindowId relative to main process
this.windowId = windowId; this.windowId = windowId;
// actual frame relative to the windowId's frames list // actual frame relative to the windowId's frames list
@ -61,6 +61,7 @@ frame.Manager = class {
/** /**
* Receives all messages from content messageManager. * Receives all messages from content messageManager.
*/ */
/*eslint-disable*/
receiveMessage(message) { receiveMessage(message) {
switch (message.name) { switch (message.name) {
case "MarionetteFrame:getInterruptedState": case "MarionetteFrame:getInterruptedState":
@ -123,6 +124,7 @@ frame.Manager = class {
} }
} }
} }
/*eslint-enable*/
getOopFrame(winId, frameId) { getOopFrame(winId, frameId) {
// get original frame window // get original frame window
@ -247,7 +249,8 @@ frame.Manager = class {
mm.removeWeakMessageListener("Marionette:shareData", this.driver); mm.removeWeakMessageListener("Marionette:shareData", this.driver);
mm.removeWeakMessageListener("Marionette:switchedToFrame", this.driver); mm.removeWeakMessageListener("Marionette:switchedToFrame", this.driver);
mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.driver); mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.driver);
mm.removeWeakMessageListener("Marionette:getImportedScripts", this.driver.importedScripts); mm.removeWeakMessageListener(
"Marionette:getImportedScripts", this.driver.importedScripts);
mm.removeWeakMessageListener("Marionette:listenersAttached", this.driver); mm.removeWeakMessageListener("Marionette:listenersAttached", this.driver);
mm.removeWeakMessageListener("Marionette:register", this.driver); mm.removeWeakMessageListener("Marionette:register", this.driver);
mm.removeWeakMessageListener("MarionetteFrame:handleModal", this); mm.removeWeakMessageListener("MarionetteFrame:handleModal", this);

View File

@ -145,7 +145,8 @@ this.interaction = {};
* @throws {InvalidElementStateError} * @throws {InvalidElementStateError}
* If |el| is not enabled. * If |el| is not enabled.
*/ */
interaction.clickElement = function* (el, strict = false, specCompat = false) { interaction.clickElement = function* (
el, strict = false, specCompat = false) {
const a11y = accessibility.get(strict); const a11y = accessibility.get(strict);
if (element.isXULElement(el)) { if (element.isXULElement(el)) {
yield chromeClick(el, a11y); yield chromeClick(el, a11y);
@ -156,9 +157,8 @@ interaction.clickElement = function* (el, strict = false, specCompat = false) {
} }
}; };
function* webdriverClickElement (el, a11y) { function* webdriverClickElement(el, a11y) {
const win = getWindow(el); const win = getWindow(el);
const doc = win.document;
// step 3 // step 3
if (el.localName == "input" && el.type == "file") { if (el.localName == "input" && el.type == "file") {
@ -213,7 +213,7 @@ function* webdriverClickElement (el, a11y) {
// handled by the load listener in listener.js // handled by the load listener in listener.js
} }
function* chromeClick (el, a11y) { function* chromeClick(el, a11y) {
if (!atom.isElementEnabled(el)) { if (!atom.isElementEnabled(el)) {
throw new InvalidElementStateError("Element is not enabled"); throw new InvalidElementStateError("Element is not enabled");
} }
@ -231,7 +231,7 @@ function* chromeClick (el, a11y) {
} }
} }
function* seleniumClickElement (el, a11y) { function* seleniumClickElement(el, a11y) {
let win = getWindow(el); let win = getWindow(el);
let visibilityCheckEl = el; let visibilityCheckEl = el;
@ -261,7 +261,7 @@ function* seleniumClickElement (el, a11y) {
let opts = {}; let opts = {};
event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win); event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win);
} }
}; }
/** /**
* Select <option> element in a <select> list. * Select <option> element in a <select> list.
@ -280,7 +280,7 @@ function* seleniumClickElement (el, a11y) {
* @throws Error * @throws Error
* If unable to find |el|'s parent <select> element. * If unable to find |el|'s parent <select> element.
*/ */
interaction.selectOption = function (el) { interaction.selectOption = function(el) {
if (element.isXULElement(el)) { if (element.isXULElement(el)) {
throw new Error("XUL dropdowns not supported"); throw new Error("XUL dropdowns not supported");
} }
@ -288,7 +288,6 @@ interaction.selectOption = function (el) {
throw new TypeError("Invalid elements"); throw new TypeError("Invalid elements");
} }
let win = getWindow(el);
let containerEl = element.getContainer(el); let containerEl = element.getContainer(el);
event.mouseover(containerEl); event.mouseover(containerEl);
@ -333,7 +332,7 @@ interaction.flushEventLoop = function* (win) {
return; return;
} }
win.addEventListener("beforeunload", handleEvent, false); win.addEventListener("beforeunload", handleEvent);
win.requestAnimationFrame(handleEvent); win.requestAnimationFrame(handleEvent);
}); });
}; };
@ -413,7 +412,8 @@ interaction.setFormControlValue = function* (el, value) {
* @param {boolean=} strict * @param {boolean=} strict
* Enforce strict accessibility tests. * Enforce strict accessibility tests.
*/ */
interaction.sendKeysToElement = function (el, value, ignoreVisibility, strict = false) { interaction.sendKeysToElement = function(
el, value, ignoreVisibility, strict = false) {
let win = getWindow(el); let win = getWindow(el);
let a11y = accessibility.get(strict); let a11y = accessibility.get(strict);
return a11y.getAccessible(el, true).then(acc => { return a11y.getAccessible(el, true).then(acc => {
@ -433,7 +433,7 @@ interaction.sendKeysToElement = function (el, value, ignoreVisibility, strict =
* @return {boolean} * @return {boolean}
* True if element is displayed, false otherwise. * True if element is displayed, false otherwise.
*/ */
interaction.isElementDisplayed = function (el, strict = false) { interaction.isElementDisplayed = function(el, strict = false) {
let win = getWindow(el); let win = getWindow(el);
let displayed = atom.isElementDisplayed(el, win); let displayed = atom.isElementDisplayed(el, win);
@ -453,7 +453,7 @@ interaction.isElementDisplayed = function (el, strict = false) {
* @return {boolean} * @return {boolean}
* True if enabled, false otherwise. * True if enabled, false otherwise.
*/ */
interaction.isElementEnabled = function (el, strict = false) { interaction.isElementEnabled = function(el, strict = false) {
let enabled = true; let enabled = true;
let win = getWindow(el); let win = getWindow(el);
@ -490,7 +490,7 @@ interaction.isElementEnabled = function (el, strict = false) {
* @return {boolean} * @return {boolean}
* True if element is selected, false otherwise. * True if element is selected, false otherwise.
*/ */
interaction.isElementSelected = function (el, strict = false) { interaction.isElementSelected = function(el, strict = false) {
let selected = true; let selected = true;
let win = getWindow(el); let win = getWindow(el);

View File

@ -7,11 +7,12 @@
/** /**
* An API which allows Marionette to handle localized content. * An API which allows Marionette to handle localized content.
* *
* The localization (https://mzl.la/2eUMjyF) of UI elements in Gecko based * The localization (https://mzl.la/2eUMjyF) of UI elements in Gecko
* applications is done via entities and properties. For static values entities * based applications is done via entities and properties. For static
* are used, which are located in .dtd files. Whereby for dynamically updated * values entities are used, which are located in .dtd files. Whereby for
* content the values come from .property files. Both types of elements can be * dynamically updated content the values come from .property files. Both
* identifed via a unique id, and the translated content retrieved. * types of elements can be identifed via a unique id, and the translated
* content retrieved.
*/ */
const {classes: Cc, interfaces: Ci, utils: Cu} = Components; const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
@ -43,7 +44,7 @@ this.l10n = {};
* @return {string} * @return {string}
* The localized string for the requested entity. * The localized string for the requested entity.
*/ */
l10n.localizeEntity = function (urls, id) { l10n.localizeEntity = function(urls, id) {
// Build a string which contains all possible entity locations // Build a string which contains all possible entity locations
let locations = []; let locations = [];
urls.forEach((url, index) => { urls.forEach((url, index) => {
@ -67,7 +68,9 @@ l10n.localizeEntity = function (urls, id) {
* Retrieve the localized string for the specified property id. * Retrieve the localized string for the specified property id.
* *
* Example: * Example:
* localizeProperty(["chrome://global/locale/findbar.properties"], "FastFind") *
* localizeProperty(
* ["chrome://global/locale/findbar.properties"], "FastFind");
* *
* @param {Array.<string>} urls * @param {Array.<string>} urls
* Array of .properties URLs. * Array of .properties URLs.
@ -77,7 +80,7 @@ l10n.localizeEntity = function (urls, id) {
* @return {string} * @return {string}
* The localized string for the requested property. * The localized string for the requested property.
*/ */
l10n.localizeProperty = function (urls, id) { l10n.localizeProperty = function(urls, id) {
let property = null; let property = null;
for (let url of urls) { for (let url of urls) {
@ -86,10 +89,11 @@ l10n.localizeProperty = function (urls, id) {
property = bundle.GetStringFromName(id); property = bundle.GetStringFromName(id);
break; break;
} catch (e) {} } catch (e) {}
}; }
if (property === null) { if (property === null) {
throw new NoSuchElementError(`Property with id='${id}' hasn't been found`); throw new NoSuchElementError(
`Property with ID '${id}' hasn't been found`);
} }
return property; return property;

View File

@ -18,6 +18,7 @@ var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -52,7 +53,7 @@ var marionetteTestName;
var winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor) var winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils); .getInterface(Ci.nsIDOMWindowUtils);
var listenerId = null; // unique ID of this listener var listenerId = null; // unique ID of this listener
var curContainer = { frame: content, shadowRoot: null }; var curContainer = {frame: content, shadowRoot: null};
var isRemoteBrowser = () => curContainer.frame.contentWindow !== null; var isRemoteBrowser = () => curContainer.frame.contentWindow !== null;
var previousContainer = null; var previousContainer = null;
@ -101,8 +102,10 @@ if (logger.ownAppenders.length == 0) {
} }
var modalHandler = function() { var modalHandler = function() {
// This gets called on the system app only since it receives the mozbrowserprompt event // This gets called on the system app only since it receives the
sendSyncMessage("Marionette:switchedToFrame", {frameValue: null, storePrevious: true}); // mozbrowserprompt event
sendSyncMessage("Marionette:switchedToFrame",
{frameValue: null, storePrevious: true});
let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value; let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value;
if (isLocal) { if (isLocal) {
previousContainer = curContainer; previousContainer = curContainer;
@ -115,10 +118,10 @@ var sandboxes = new Sandboxes(() => curContainer.frame);
var sandboxName = "default"; var sandboxName = "default";
/** /**
* The load listener singleton helps to keep track of active page load activities, * The load listener singleton helps to keep track of active page
* and can be used by any command which might cause a navigation to happen. In the * load activities, and can be used by any command which might cause a
* specific case of remoteness changes it allows to continue observing the current * navigation to happen. In the specific case of remoteness changes it
* page load. * allows to continue observing the current page load.
*/ */
var loadListener = { var loadListener = {
command_id: null, command_id: null,
@ -132,22 +135,26 @@ var loadListener = {
* Start listening for page unload/load events. * Start listening for page unload/load events.
* *
* @param {number} command_id * @param {number} command_id
* ID of the currently handled message between the driver and listener. * ID of the currently handled message between the driver and
* listener.
* @param {number} timeout * @param {number} timeout
* Timeout in seconds the method has to wait for the page being finished loading. * Timeout in seconds the method has to wait for the page being
* finished loading.
* @param {number} startTime * @param {number} startTime
* Unix timestap when the navitation request got triggered. * Unix timestap when the navitation request got triggered.
* @param {boolean=} waitForUnloaded * @param {boolean=} waitForUnloaded
* If `true` wait for page unload events, otherwise only for page load events. * If true wait for page unload events, otherwise only for page
* load events.
*/ */
start: function (command_id, timeout, startTime, waitForUnloaded = true) { start(command_id, timeout, startTime, waitForUnloaded = true) {
this.command_id = command_id; this.command_id = command_id;
this.timeout = timeout; this.timeout = timeout;
this.seenBeforeUnload = false; this.seenBeforeUnload = false;
this.seenUnload = false; this.seenUnload = false;
this.timerPageLoad = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this.timerPageLoad = Cc["@mozilla.org/timer;1"]
.createInstance(Ci.nsITimer);
this.timerPageUnload = null; this.timerPageUnload = null;
// In case of a remoteness change, only wait the remaining time // In case of a remoteness change, only wait the remaining time
@ -164,22 +171,23 @@ var loadListener = {
// The events can only be received when the event listeners are // The events can only be received when the event listeners are
// added to the currently selected frame. // added to the currently selected frame.
curContainer.frame.addEventListener("beforeunload", this, false); curContainer.frame.addEventListener("beforeunload", this);
curContainer.frame.addEventListener("unload", this, false); curContainer.frame.addEventListener("unload", this);
Services.obs.addObserver(this, "outer-window-destroyed"); Services.obs.addObserver(this, "outer-window-destroyed");
} else { } else {
addEventListener("DOMContentLoaded", loadListener, false); addEventListener("DOMContentLoaded", loadListener);
addEventListener("pageshow", loadListener, false); addEventListener("pageshow", loadListener);
} }
this.timerPageLoad.initWithCallback(this, timeout, Ci.nsITimer.TYPE_ONE_SHOT); this.timerPageLoad.initWithCallback(
this, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
}, },
/** /**
* Stop listening for page unload/load events. * Stop listening for page unload/load events.
*/ */
stop: function () { stop() {
if (this.timerPageLoad) { if (this.timerPageLoad) {
this.timerPageLoad.cancel(); this.timerPageLoad.cancel();
} }
@ -214,7 +222,7 @@ var loadListener = {
/** /**
* Callback for registered DOM events. * Callback for registered DOM events.
*/ */
handleEvent: function (event) { handleEvent(event) {
let location = event.target.baseURI || event.target.location.href; let location = event.target.baseURI || event.target.location.href;
logger.debug(`Received DOM event "${event.type}" for "${location}"`); logger.debug(`Received DOM event "${event.type}" for "${location}"`);
@ -257,10 +265,11 @@ var loadListener = {
sendError(new UnknownError("Reached error page: " + sendError(new UnknownError("Reached error page: " +
event.target.baseURI), this.command_id); event.target.baseURI), this.command_id);
// Return early with a page load strategy of eager, and also special-case // Return early with a page load strategy of eager, and also
// about:blocked pages which should be treated as non-error pages but do // special-case about:blocked pages which should be treated as
// not raise a pageshow event. // non-error pages but do not raise a pageshow event.
} else if (capabilities.get("pageLoadStrategy") === session.PageLoadStrategy.Eager || } else if ((capabilities.get("pageLoadStrategy") ===
session.PageLoadStrategy.Eager) ||
/about:blocked\?/.exec(event.target.baseURI)) { /about:blocked\?/.exec(event.target.baseURI)) {
this.stop(); this.stop();
sendOk(this.command_id); sendOk(this.command_id);
@ -279,22 +288,24 @@ var loadListener = {
/** /**
* Callback for navigation timeout timer. * Callback for navigation timeout timer.
*/ */
notify: function (timer) { notify(timer) {
switch (timer) { switch (timer) {
case this.timerPageUnload: case this.timerPageUnload:
// In the case when a document has a beforeunload handler registered, // In the case when a document has a beforeunload handler
// the currently active command will return immediately due to the // registered, the currently active command will return immediately
// modal dialog observer in proxy.js. // due to the modal dialog observer in proxy.js.
// Otherwise the timeout waiting for the document to start navigating //
// is increased by 5000 ms to ensure a possible load event is not missed. // Otherwise the timeout waiting for the document to start
// In the common case such an event should occur pretty soon after // navigating is increased by 5000 ms to ensure a possible load
// beforeunload, and we optimise for this. // event is not missed. In the common case such an event should
// occur pretty soon after beforeunload, and we optimise for this.
if (this.seenBeforeUnload) { if (this.seenBeforeUnload) {
this.seenBeforeUnload = null; this.seenBeforeUnload = null;
this.timerPageUnload.initWithCallback(this, 5000, Ci.nsITimer.TYPE_ONE_SHOT) this.timerPageUnload.initWithCallback(
this, 5000, Ci.nsITimer.TYPE_ONE_SHOT)
// If no page unload has been detected, ensure to properly stop the load // If no page unload has been detected, ensure to properly stop
// listener, and return from the currently active command. // the load listener, and return from the currently active command.
} else if (!this.seenUnload) { } else if (!this.seenUnload) {
logger.debug("Canceled page load listener because no navigation " + logger.debug("Canceled page load listener because no navigation " +
"has been detected"); "has been detected");
@ -303,17 +314,19 @@ var loadListener = {
} }
break; break;
case this.timerPageLoad: case this.timerPageLoad:
this.stop(); this.stop();
sendError(new TimeoutError(`Timeout loading page after ${this.timeout}ms`), sendError(
this.command_id); new TimeoutError(`Timeout loading page after ${this.timeout}ms`),
break; this.command_id);
break;
} }
}, },
observe: function (subject, topic, data) { observe(subject, topic, data) {
const winID = subject.QueryInterface(Components.interfaces.nsISupportsPRUint64).data; const win = curContainer.frame;
const curWinID = curContainer.frame.QueryInterface(Ci.nsIInterfaceRequestor) const winID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
const curWinID = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).outerWindowID; .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
logger.debug(`Received observer notification "${topic}" for "${winID}"`); logger.debug(`Received observer notification "${topic}" for "${winID}"`);
@ -331,16 +344,19 @@ var loadListener = {
}, },
/** /**
* Continue to listen for page load events after a remoteness change happened. * Continue to listen for page load events after a remoteness change
* happened.
* *
* @param {number} command_id * @param {number} command_id
* ID of the currently handled message between the driver and listener. * ID of the currently handled message between the driver and
* listener.
* @param {number} timeout * @param {number} timeout
* Timeout in milliseconds the method has to wait for the page being finished loading. * Timeout in milliseconds the method has to wait for the page
* being finished loading.
* @param {number} startTime * @param {number} startTime
* Unix timestap when the navitation request got triggered. * Unix timestap when the navitation request got triggered.
*/ */
waitForLoadAfterRemotenessChange: function (command_id, timeout, startTime) { waitForLoadAfterRemotenessChange(command_id, timeout, startTime) {
this.start(command_id, timeout, startTime, false); this.start(command_id, timeout, startTime, false);
}, },
@ -353,19 +369,21 @@ var loadListener = {
* @param {number} command_id * @param {number} command_id
* ID of the currently handled message between the driver and listener. * ID of the currently handled message between the driver and listener.
* @param {number} pageTimeout * @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page finished loading. * Timeout in milliseconds the method has to wait for the page
* finished loading.
* @param {boolean=} loadEventExpected * @param {boolean=} loadEventExpected
* Optional flag, which indicates that navigate has to wait for the page * Optional flag, which indicates that navigate has to wait for the page
* finished loading. * finished loading.
* @param {string=} url * @param {string=} url
* Optional URL, which is used to check if a page load is expected. * Optional URL, which is used to check if a page load is expected.
*/ */
navigate: function (trigger, command_id, timeout, loadEventExpected = true, navigate(trigger, command_id, timeout, loadEventExpected = true,
useUnloadTimer = false) { useUnloadTimer = false) {
// Only wait if the page load strategy is not `none` // Only wait if the page load strategy is not `none`
loadEventExpected = loadEventExpected && loadEventExpected = loadEventExpected &&
capabilities.get("pageLoadStrategy") !== session.PageLoadStrategy.None; (capabilities.get("pageLoadStrategy") !==
session.PageLoadStrategy.None);
if (loadEventExpected) { if (loadEventExpected) {
let startTime = new Date().getTime(); let startTime = new Date().getTime();
@ -376,15 +394,17 @@ var loadListener = {
yield trigger(); yield trigger();
}).then(val => { }).then(val => {
if (!loadEventExpected) { if (!loadEventExpected) {
sendOk(command_id); sendOk(command_id);
return; return;
} }
// If requested setup a timer to detect a possible page load // If requested setup a timer to detect a possible page load
if (useUnloadTimer) { if (useUnloadTimer) {
this.timerPageUnload = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this.timerPageUnload = Cc["@mozilla.org/timer;1"]
this.timerPageUnload.initWithCallback(this, 200, Ci.nsITimer.TYPE_ONE_SHOT); .createInstance(Ci.nsITimer);
this.timerPageUnload.initWithCallback(
this, 200, Ci.nsITimer.TYPE_ONE_SHOT);
} }
}).catch(err => { }).catch(err => {
@ -393,21 +413,22 @@ var loadListener = {
} }
sendError(err, command_id); sendError(err, command_id);
return;
}); });
}, },
} }
/** /**
* Called when listener is first started up. * Called when listener is first started up. The listener sends its
* The listener sends its unique window ID and its current URI to the actor. * unique window ID and its current URI to the actor. If the actor returns
* If the actor returns an ID, we start the listeners. Otherwise, nothing happens. * an ID, we start the listeners. Otherwise, nothing happens.
*/ */
function registerSelf() { function registerSelf() {
let msg = {value: winUtil.outerWindowID}; let msg = {value: winUtil.outerWindowID};
logger.debug(`Register listener.js for window ${msg.value}`); logger.debug(`Register listener.js for window ${msg.value}`);
// register will have the ID and a boolean describing if this is the main process or not // register will have the ID and a boolean describing if this is the
// main process or not
let register = sendSyncMessage("Marionette:register", msg); let register = sendSyncMessage("Marionette:register", msg);
if (register[0]) { if (register[0]) {
@ -435,15 +456,14 @@ function dispatch(fn) {
throw new TypeError("Provided dispatch handler is not a function"); throw new TypeError("Provided dispatch handler is not a function");
} }
return function (msg) { return function(msg) {
let id = msg.json.command_id; let id = msg.json.command_id;
let req = Task.spawn(function* () { let req = Task.spawn(function* () {
if (typeof msg.json == "undefined" || msg.json instanceof Array) { if (typeof msg.json == "undefined" || msg.json instanceof Array) {
return yield fn.apply(null, msg.json); return yield fn.apply(null, msg.json);
} else {
return yield fn(msg.json);
} }
return yield fn(msg.json);
}); });
let okOrValueResponse = rv => { let okOrValueResponse = rv => {
@ -521,15 +541,19 @@ function startListeners() {
addMessageListenerId("Marionette:goForward", goForward); addMessageListenerId("Marionette:goForward", goForward);
addMessageListenerId("Marionette:refresh", refresh); addMessageListenerId("Marionette:refresh", refresh);
addMessageListenerId("Marionette:findElementContent", findElementContentFn); addMessageListenerId("Marionette:findElementContent", findElementContentFn);
addMessageListenerId("Marionette:findElementsContent", findElementsContentFn); addMessageListenerId(
"Marionette:findElementsContent", findElementsContentFn);
addMessageListenerId("Marionette:getActiveElement", getActiveElementFn); addMessageListenerId("Marionette:getActiveElement", getActiveElementFn);
addMessageListenerId("Marionette:clickElement", clickElement); addMessageListenerId("Marionette:clickElement", clickElement);
addMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn); addMessageListenerId(
"Marionette:getElementAttribute", getElementAttributeFn);
addMessageListenerId("Marionette:getElementProperty", getElementPropertyFn); addMessageListenerId("Marionette:getElementProperty", getElementPropertyFn);
addMessageListenerId("Marionette:getElementText", getElementTextFn); addMessageListenerId("Marionette:getElementText", getElementTextFn);
addMessageListenerId("Marionette:getElementTagName", getElementTagNameFn); addMessageListenerId("Marionette:getElementTagName", getElementTagNameFn);
addMessageListenerId("Marionette:isElementDisplayed", isElementDisplayedFn); addMessageListenerId("Marionette:isElementDisplayed", isElementDisplayedFn);
addMessageListenerId("Marionette:getElementValueOfCssProperty", getElementValueOfCssPropertyFn); addMessageListenerId(
"Marionette:getElementValueOfCssProperty",
getElementValueOfCssPropertyFn);
addMessageListenerId("Marionette:getElementRect", getElementRectFn); addMessageListenerId("Marionette:getElementRect", getElementRectFn);
addMessageListenerId("Marionette:isElementEnabled", isElementEnabledFn); addMessageListenerId("Marionette:isElementEnabled", isElementEnabledFn);
addMessageListenerId("Marionette:isElementSelected", isElementSelectedFn); addMessageListenerId("Marionette:isElementSelected", isElementSelectedFn);
@ -591,24 +615,37 @@ function deleteSession(msg) {
removeMessageListenerId("Marionette:goBack", goBack); removeMessageListenerId("Marionette:goBack", goBack);
removeMessageListenerId("Marionette:goForward", goForward); removeMessageListenerId("Marionette:goForward", goForward);
removeMessageListenerId("Marionette:refresh", refresh); removeMessageListenerId("Marionette:refresh", refresh);
removeMessageListenerId("Marionette:findElementContent", findElementContentFn); removeMessageListenerId(
removeMessageListenerId("Marionette:findElementsContent", findElementsContentFn); "Marionette:findElementContent", findElementContentFn);
removeMessageListenerId(
"Marionette:findElementsContent", findElementsContentFn);
removeMessageListenerId("Marionette:getActiveElement", getActiveElementFn); removeMessageListenerId("Marionette:getActiveElement", getActiveElementFn);
removeMessageListenerId("Marionette:clickElement", clickElement); removeMessageListenerId("Marionette:clickElement", clickElement);
removeMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn); removeMessageListenerId(
removeMessageListenerId("Marionette:getElementProperty", getElementPropertyFn); "Marionette:getElementAttribute", getElementAttributeFn);
removeMessageListenerId("Marionette:getElementText", getElementTextFn); removeMessageListenerId(
removeMessageListenerId("Marionette:getElementTagName", getElementTagNameFn); "Marionette:getElementProperty", getElementPropertyFn);
removeMessageListenerId("Marionette:isElementDisplayed", isElementDisplayedFn); removeMessageListenerId(
removeMessageListenerId("Marionette:getElementValueOfCssProperty", getElementValueOfCssPropertyFn); "Marionette:getElementText", getElementTextFn);
removeMessageListenerId(
"Marionette:getElementTagName", getElementTagNameFn);
removeMessageListenerId(
"Marionette:isElementDisplayed", isElementDisplayedFn);
removeMessageListenerId(
"Marionette:getElementValueOfCssProperty",
getElementValueOfCssPropertyFn);
removeMessageListenerId("Marionette:getElementRect", getElementRectFn); removeMessageListenerId("Marionette:getElementRect", getElementRectFn);
removeMessageListenerId("Marionette:isElementEnabled", isElementEnabledFn); removeMessageListenerId("Marionette:isElementEnabled", isElementEnabledFn);
removeMessageListenerId("Marionette:isElementSelected", isElementSelectedFn); removeMessageListenerId(
removeMessageListenerId("Marionette:sendKeysToElement", sendKeysToElementFn); "Marionette:isElementSelected", isElementSelectedFn);
removeMessageListenerId(
"Marionette:sendKeysToElement", sendKeysToElementFn);
removeMessageListenerId("Marionette:clearElement", clearElementFn); removeMessageListenerId("Marionette:clearElement", clearElementFn);
removeMessageListenerId("Marionette:switchToFrame", switchToFrame); removeMessageListenerId("Marionette:switchToFrame", switchToFrame);
removeMessageListenerId("Marionette:switchToParentFrame", switchToParentFrame); removeMessageListenerId(
removeMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn); "Marionette:switchToParentFrame", switchToParentFrame);
removeMessageListenerId(
"Marionette:switchToShadowRoot", switchToShadowRootFn);
removeMessageListenerId("Marionette:deleteSession", deleteSession); removeMessageListenerId("Marionette:deleteSession", deleteSession);
removeMessageListenerId("Marionette:sleepSession", sleepSession); removeMessageListenerId("Marionette:sleepSession", sleepSession);
removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus); removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
@ -616,7 +653,7 @@ function deleteSession(msg) {
seenEls.clear(); seenEls.clear();
// reset container frame to the top-most frame // reset container frame to the top-most frame
curContainer = { frame: content, shadowRoot: null }; curContainer = {frame: content, shadowRoot: null};
curContainer.frame.focus(); curContainer.frame.focus();
legacyactions.touchIds = {}; legacyactions.touchIds = {};
if (action.inputStateMap !== undefined) { if (action.inputStateMap !== undefined) {
@ -690,35 +727,32 @@ function resetValues() {
action.inputsToCancel = []; action.inputsToCancel = [];
} }
/**
* Check if our context was interrupted
*/
function wasInterrupted() { function wasInterrupted() {
if (previousContainer) { if (previousContainer) {
let element = content.document.elementFromPoint((content.innerWidth/2), (content.innerHeight/2)); let element = content.document.elementFromPoint(
(content.innerWidth / 2), (content.innerHeight / 2));
if (element.id.indexOf("modal-dialog") == -1) { if (element.id.indexOf("modal-dialog") == -1) {
return true; return true;
} }
else { return false;
return false;
}
} }
return sendSyncMessage("MarionetteFrame:getInterruptedState", {})[0].value; return sendSyncMessage("MarionetteFrame:getInterruptedState", {})[0].value;
} }
function checkForInterrupted() { function checkForInterrupted() {
if (wasInterrupted()) { if (wasInterrupted()) {
if (previousContainer) { // if previousContainer is set, then we're in a single process
// if previousContainer is set, then we're in a single process environment // environment
curContainer = legacyactions.container = previousContainer; if (previousContainer) {
previousContainer = null; curContainer = legacyactions.container = previousContainer;
} previousContainer = null;
else { // else we're in OOP environment, so we'll switch to the original
//else we're in OOP environment, so we'll switch to the original OOP frame // OOP frame
sendSyncMessage("Marionette:switchToModalOrigin"); } else {
} sendSyncMessage("Marionette:switchToModalOrigin");
sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true });
} }
sendSyncMessage("Marionette:switchedToFrame", {restorePrevious: true});
}
} }
function* execute(script, args, timeout, opts) { function* execute(script, args, timeout, opts) {
@ -744,36 +778,58 @@ function* executeInSandbox(script, args, timeout, opts) {
return evaluate.toJSON(res, seenEls); return evaluate.toJSON(res, seenEls);
} }
/**
* This function creates a touch event given a touch type and a touch
*/
function emitTouchEvent(type, touch) { function emitTouchEvent(type, touch) {
if (!wasInterrupted()) { if (!wasInterrupted()) {
logger.info(`Emitting Touch event of type ${type} to element with id: ${touch.target.id} ` + logger.info(`Emitting Touch event of type ${type} ` +
`and tag name: ${touch.target.tagName} at coordinates (${touch.clientX}, ` + `to element with id: ${touch.target.id} ` +
`${touch.clientY}) relative to the viewport`); `and tag name: ${touch.target.tagName} ` +
`at coordinates (${touch.clientX}), ` +
`${touch.clientY}) relative to the viewport`);
var docShell = curContainer.frame.document.defaultView. const win = curContainer.frame;
QueryInterface(Components.interfaces.nsIInterfaceRequestor). let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
getInterface(Components.interfaces.nsIWebNavigation). .getInterface(Ci.nsIWebNavigation)
QueryInterface(Components.interfaces.nsIDocShell); .QueryInterface(Ci.nsIDocShell);
if (docShell.asyncPanZoomEnabled && legacyactions.scrolling) { if (docShell.asyncPanZoomEnabled && legacyactions.scrolling) {
// if we're in APZ and we're scrolling, we must use sendNativeTouchPoint to dispatch our touchmove events // if we're in APZ and we're scrolling, we must use
// sendNativeTouchPoint to dispatch our touchmove events
let index = sendSyncMessage("MarionetteFrame:getCurrentFrameId"); let index = sendSyncMessage("MarionetteFrame:getCurrentFrameId");
// only call emitTouchEventForIFrame if we're inside an iframe. // only call emitTouchEventForIFrame if we're inside an iframe.
if (index != null) { if (index != null) {
sendSyncMessage("Marionette:emitTouchEvent", let ev = {
{ index: index, type: type, id: touch.identifier, index,
clientX: touch.clientX, clientY: touch.clientY, type,
screenX: touch.screenX, screenY: touch.screenY, id: touch.identifier,
radiusX: touch.radiusX, radiusY: touch.radiusY, clientX: touch.clientX,
rotation: touch.rotationAngle, force: touch.force }); clientY: touch.clientY,
screenX: touch.screenX,
screenY: touch.screenY,
radiusX: touch.radiusX,
radiusY: touch.radiusY,
rotation: touch.rotationAngle,
force: touch.force,
};
sendSyncMessage("Marionette:emitTouchEvent", ev);
return; return;
} }
} }
// we get here if we're not in asyncPacZoomEnabled land, or if we're the main process
let domWindowUtils = curContainer.frame.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils); // we get here if we're not in asyncPacZoomEnabled land, or if we're
domWindowUtils.sendTouchEvent(type, [touch.identifier], [touch.clientX], [touch.clientY], [touch.radiusX], [touch.radiusY], [touch.rotationAngle], [touch.force], 1, 0); // the main process
let domWindowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
domWindowUtils.sendTouchEvent(
type,
[touch.identifier],
[touch.clientX],
[touch.clientY],
[touch.radiusX],
[touch.radiusY],
[touch.rotationAngle],
[touch.force],
1,
0);
} }
} }
@ -785,7 +841,8 @@ function singleTap(id, corx, cory) {
// after this block, the element will be scrolled into view // after this block, the element will be scrolled into view
let visible = element.isVisible(el, corx, cory); let visible = element.isVisible(el, corx, cory);
if (!visible) { if (!visible) {
throw new ElementNotInteractableError("Element is not currently visible and may not be manipulated"); throw new ElementNotInteractableError(
"Element is not currently visible and may not be manipulated");
} }
let a11y = accessibility.get(capabilities.get("moz:accessibilityChecks")); let a11y = accessibility.get(capabilities.get("moz:accessibilityChecks"));
@ -799,8 +856,8 @@ function singleTap(id, corx, cory) {
if (!legacyactions.mouseEventsOnly) { if (!legacyactions.mouseEventsOnly) {
let touchId = legacyactions.nextTouchId++; let touchId = legacyactions.nextTouchId++;
let touch = createATouch(el, c.x, c.y, touchId); let touch = createATouch(el, c.x, c.y, touchId);
emitTouchEvent('touchstart', touch); emitTouchEvent("touchstart", touch);
emitTouchEvent('touchend', touch); emitTouchEvent("touchend", touch);
} }
legacyactions.mouseTap(el.ownerDocument, c.x, c.y); legacyactions.mouseTap(el.ownerDocument, c.x, c.y);
}); });
@ -814,8 +871,17 @@ function createATouch(el, corx, cory, touchId) {
let doc = el.ownerDocument; let doc = el.ownerDocument;
let win = doc.defaultView; let win = doc.defaultView;
let [clientX, clientY, pageX, pageY, screenX, screenY] = let [clientX, clientY, pageX, pageY, screenX, screenY] =
legacyactions.getCoordinateInfo(el, corx, cory); legacyactions.getCoordinateInfo(el, corx, cory);
let atouch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY); let atouch = doc.createTouch(
win,
el,
touchId,
pageX,
pageY,
screenX,
screenY,
clientX,
clientY);
return atouch; return atouch;
} }
@ -833,12 +899,13 @@ function* performActions(msg) {
/** /**
* The Release Actions command is used to release all the keys and pointer * The Release Actions command is used to release all the keys and pointer
* buttons that are currently depressed. This causes events to be fired as if * buttons that are currently depressed. This causes events to be fired
* the state was released by an explicit series of actions. It also clears all * as if the state was released by an explicit series of actions. It also
* the internal state of the virtual devices. * clears all the internal state of the virtual devices.
*/ */
function* releaseActions() { function* releaseActions() {
yield action.dispatchTickActions(action.inputsToCancel.reverse(), 0, seenEls, curContainer); yield action.dispatchTickActions(
action.inputsToCancel.reverse(), 0, seenEls, curContainer);
action.inputsToCancel.length = 0; action.inputsToCancel.length = 0;
action.inputStateMap.clear(); action.inputStateMap.clear();
} }
@ -859,26 +926,23 @@ function actionChain(chain, touchId) {
touchProvider); touchProvider);
} }
/**
* Function to emit touch events which allow multi touch on the screen
* @param type represents the type of event, touch represents the current touch,touches are all pending touches
*/
function emitMultiEvents(type, touch, touches) { function emitMultiEvents(type, touch, touches) {
let target = touch.target; let target = touch.target;
let doc = target.ownerDocument; let doc = target.ownerDocument;
let win = doc.defaultView; let win = doc.defaultView;
// touches that are in the same document // touches that are in the same document
let documentTouches = doc.createTouchList(touches.filter(function (t) { let documentTouches = doc.createTouchList(touches.filter(function(t) {
return ((t.target.ownerDocument === doc) && (type != 'touchcancel')); return ((t.target.ownerDocument === doc) && (type != "touchcancel"));
})); }));
// touches on the same target // touches on the same target
let targetTouches = doc.createTouchList(touches.filter(function (t) { let targetTouches = doc.createTouchList(touches.filter(function(t) {
return ((t.target === target) && ((type != 'touchcancel') || (type != 'touchend'))); return ((t.target === target) &&
((type != "touchcancel") || (type != "touchend")));
})); }));
// Create changed touches // Create changed touches
let changedTouches = doc.createTouchList(touch); let changedTouches = doc.createTouchList(touch);
// Create the event object // Create the event object
let event = doc.createEvent('TouchEvent'); let event = doc.createEvent("TouchEvent");
event.initTouchEvent(type, event.initTouchEvent(type,
true, true,
true, true,
@ -891,11 +955,7 @@ function emitMultiEvents(type, touch, touches) {
target.dispatchEvent(event); target.dispatchEvent(event);
} }
/** function setDispatch(batches, touches, batchIndex = 0) {
* Function to dispatch one set of actions
* @param touches represents all pending touches, batchIndex represents the batch we are dispatching right now
*/
function setDispatch(batches, touches, batchIndex=0) {
// check if all the sets have been fired // check if all the sets have been fired
if (batchIndex >= batches.length) { if (batchIndex >= batches.length) {
multiLast = {}; multiLast = {};
@ -912,8 +972,6 @@ function setDispatch(batches, touches, batchIndex=0) {
let command; let command;
// touch that will be created for the finger // touch that will be created for the finger
let el; let el;
let corx;
let cory;
let touch; let touch;
let lastTouch; let lastTouch;
let touchIndex; let touchIndex;
@ -940,7 +998,8 @@ function setDispatch(batches, touches, batchIndex=0) {
case "release": case "release":
touch = multiLast[touchId]; touch = multiLast[touchId];
// the index of the previous touch for the finger may change in the touches array // the index of the previous touch for the finger may change in
// the touches array
touchIndex = touches.indexOf(touch); touchIndex = touches.indexOf(touch);
touches.splice(touchIndex, 1); touches.splice(touchIndex, 1);
emitMultiEvents("touchend", touch, touches); emitMultiEvents("touchend", touch, touches);
@ -962,14 +1021,24 @@ function setDispatch(batches, touches, batchIndex=0) {
touchIndex = touches.indexOf(lastTouch); touchIndex = touches.indexOf(lastTouch);
let doc = el.ownerDocument; let doc = el.ownerDocument;
let win = doc.defaultView; let win = doc.defaultView;
// since x and y are relative to the last touch, therefore, it's relative to the position of the last touch // since x and y are relative to the last touch, therefore,
let clientX = lastTouch.clientX + pack[2], // it's relative to the position of the last touch
clientY = lastTouch.clientY + pack[3]; let clientX = lastTouch.clientX + pack[2];
let pageX = clientX + win.pageXOffset, let clientY = lastTouch.clientY + pack[3];
pageY = clientY + win.pageYOffset; let pageX = clientX + win.pageXOffset;
let screenX = clientX + win.mozInnerScreenX, let pageY = clientY + win.pageYOffset;
screenY = clientY + win.mozInnerScreenY; let screenX = clientX + win.mozInnerScreenX;
touch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY); let screenY = clientY + win.mozInnerScreenY;
touch = doc.createTouch(
win,
el,
touchId,
pageX,
pageY,
screenX,
screenY,
clientX,
clientY);
touches[touchIndex] = touch; touches[touchIndex] = touch;
multiLast[touchId] = touch; multiLast[touchId] = touch;
emitMultiEvents("touchmove", touch, touches); emitMultiEvents("touchmove", touch, touches);
@ -1012,7 +1081,8 @@ function multiAction(args, maxLen) {
let row = []; let row = [];
for (let j = 0; j < commandArray.length; j++) { for (let j = 0; j < commandArray.length; j++) {
if (typeof commandArray[j][i] != "undefined") { if (typeof commandArray[j][i] != "undefined") {
// add finger id to the front of each action, i.e. [finger_id, action, element] // add finger id to the front of each action,
// i.e. [finger_id, action, element]
temp = commandArray[j][i]; temp = commandArray[j][i];
temp.unshift(j); temp.unshift(j);
row.push(temp); row.push(temp);
@ -1021,9 +1091,11 @@ function multiAction(args, maxLen) {
concurrentEvent.push(row); concurrentEvent.push(row);
} }
// now concurrent event is made of sets where each set contain a list of actions that need to be fired. // Now concurrent event is made of sets where each set contain a list
// note: each action belongs to a different finger // of actions that need to be fired.
// pendingTouches keeps track of current touches that's on the screen //
// But note that each action belongs to a different finger
// pendingTouches keeps track of current touches that's on the screen.
let pendingTouches = []; let pendingTouches = [];
setDispatch(concurrentEvent, pendingTouches); setDispatch(concurrentEvent, pendingTouches);
} }
@ -1038,31 +1110,35 @@ function cancelRequest() {
} }
/** /**
* This implements the latter part of a get request (for the case we need to resume one * This implements the latter part of a get request (for the case we
* when a remoteness update happens in the middle of a navigate request). This is most of * need to resume one when a remoteness update happens in the middle of a
* of the work of a navigate request, but doesn't assume DOMContentLoaded is yet to fire. * navigate request). This is most of of the work of a navigate request,
* but doesn't assume DOMContentLoaded is yet to fire.
* *
* @param {number} command_id * @param {number} command_id
* ID of the currently handled message between the driver and listener. * ID of the currently handled message between the driver and
* listener.
* @param {number} pageTimeout * @param {number} pageTimeout
* Timeout in seconds the method has to wait for the page being finished loading. * Timeout in seconds the method has to wait for the page being
* finished loading.
* @param {number} startTime * @param {number} startTime
* Unix timestap when the navitation request got triggred. * Unix timestap when the navitation request got triggred.
*/ */
function waitForPageLoaded(msg) { function waitForPageLoaded(msg) {
let {command_id, pageTimeout, startTime} = msg.json; let {command_id, pageTimeout, startTime} = msg.json;
loadListener.waitForLoadAfterRemotenessChange(command_id, pageTimeout, startTime); loadListener.waitForLoadAfterRemotenessChange(
command_id, pageTimeout, startTime);
} }
/** /**
* Navigate to the given URL. The operation will be performed on the * Navigate to the given URL. The operation will be performed on the
* current browsing context, which means it handles the case where we * current browsing context, which means it handles the case where we
* navigate within an iframe. All other navigation is handled by the * navigate within an iframe. All other navigation is handled by the driver
* driver (in chrome space). * (in chrome space).
*/ */
function get(msg) { function get(msg) {
let {command_id, pageTimeout, url, loadEventExpected=null} = msg.json; let {command_id, pageTimeout, url, loadEventExpected = null} = msg.json;
try { try {
if (typeof url == "string") { if (typeof url == "string") {
@ -1072,7 +1148,8 @@ function get(msg) {
loadEventExpected = navigate.isLoadEventExpected(requestedURL); loadEventExpected = navigate.isLoadEventExpected(requestedURL);
} }
} catch (e) { } catch (e) {
sendError(new InvalidArgumentError("Malformed URL: " + e.message), command_id); let err = new InvalidArgumentError("Malformed URL: " + e.message);
sendError(err, command_id);
return; return;
} }
} }
@ -1095,9 +1172,11 @@ function get(msg) {
* of the current browsing context. * of the current browsing context.
* *
* @param {number} command_id * @param {number} command_id
* ID of the currently handled message between the driver and listener. * ID of the currently handled message between the driver and
* listener.
* @param {number} pageTimeout * @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page being finished loading. * Timeout in milliseconds the method has to wait for the page being
* finished loading.
*/ */
function goBack(msg) { function goBack(msg) {
let {command_id, pageTimeout} = msg.json; let {command_id, pageTimeout} = msg.json;
@ -1117,9 +1196,11 @@ function goBack(msg) {
* of the current browsing context. * of the current browsing context.
* *
* @param {number} command_id * @param {number} command_id
* ID of the currently handled message between the driver and listener. * ID of the currently handled message between the driver and
* listener.
* @param {number} pageTimeout * @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page being finished loading. * Timeout in milliseconds the method has to wait for the page being
* finished loading.
*/ */
function goForward(msg) { function goForward(msg) {
let {command_id, pageTimeout} = msg.json; let {command_id, pageTimeout} = msg.json;
@ -1135,12 +1216,15 @@ function goForward(msg) {
} }
/** /**
* Causes the browser to reload the page in in current top-level browsing context. * Causes the browser to reload the page in in current top-level browsing
* context.
* *
* @param {number} command_id * @param {number} command_id
* ID of the currently handled message between the driver and listener. * ID of the currently handled message between the driver and
* listener.
* @param {number} pageTimeout * @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page being finished loading. * Timeout in milliseconds the method has to wait for the page being
* finished loading.
*/ */
function refresh(msg) { function refresh(msg) {
let {command_id, pageTimeout} = msg.json; let {command_id, pageTimeout} = msg.json;
@ -1223,11 +1307,13 @@ function getActiveElement() {
* Send click event to element. * Send click event to element.
* *
* @param {number} command_id * @param {number} command_id
* ID of the currently handled message between the driver and listener. * ID of the currently handled message between the driver and
* listener.
* @param {WebElement} id * @param {WebElement} id
* Reference to the web element to click. * Reference to the web element to click.
* @param {number} pageTimeout * @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page being finished loading. * Timeout in milliseconds the method has to wait for the page being
* finished loading.
*/ */
function clickElement(msg) { function clickElement(msg) {
let {command_id, id, pageTimeout} = msg.json; let {command_id, id, pageTimeout} = msg.json;
@ -1259,12 +1345,10 @@ function getElementAttribute(id, name) {
if (element.isBooleanAttribute(el, name)) { if (element.isBooleanAttribute(el, name)) {
if (el.hasAttribute(name)) { if (el.hasAttribute(name)) {
return "true"; return "true";
} else {
return null;
} }
} else { return null;
return el.getAttribute(name);
} }
return el.getAttribute(name);
} }
function getElementProperty(id, name) { function getElementProperty(id, name) {
@ -1344,9 +1428,9 @@ function getElementRect(id) {
let clientRect = el.getBoundingClientRect(); let clientRect = el.getBoundingClientRect();
return { return {
x: clientRect.x + curContainer.frame.pageXOffset, x: clientRect.x + curContainer.frame.pageXOffset,
y: clientRect.y + curContainer.frame.pageYOffset, y: clientRect.y + curContainer.frame.pageYOffset,
width: clientRect.width, width: clientRect.width,
height: clientRect.height height: clientRect.height,
}; };
} }
@ -1414,13 +1498,14 @@ function clearElement(id) {
/** /**
* Switch the current context to the specified host's Shadow DOM. * Switch the current context to the specified host's Shadow DOM.
*
* @param {WebElement} id * @param {WebElement} id
* Reference to web element. * Reference to web element.
*/ */
function switchToShadowRoot(id) { function switchToShadowRoot(id) {
if (!id) { if (!id) {
// If no host element is passed, attempt to find a parent shadow root or, if // If no host element is passed, attempt to find a parent shadow
// none found, unset the current shadow root // root or, if none found, unset the current shadow root
if (curContainer.shadowRoot) { if (curContainer.shadowRoot) {
let parent; let parent;
try { try {
@ -1443,25 +1528,24 @@ function switchToShadowRoot(id) {
let hostEl = seenEls.get(id, curContainer); let hostEl = seenEls.get(id, curContainer);
foundShadowRoot = hostEl.shadowRoot; foundShadowRoot = hostEl.shadowRoot;
if (!foundShadowRoot) { if (!foundShadowRoot) {
throw new NoSuchElementError('Unable to locate shadow root: ' + id); throw new NoSuchElementError("Unable to locate shadow root: " + id);
} }
curContainer.shadowRoot = foundShadowRoot; curContainer.shadowRoot = foundShadowRoot;
} }
/** /**
* Switch to the parent frame of the current Frame. If the frame is the top most * Switch to the parent frame of the current frame. If the frame is the
* is the current frame then no action will happen. * top most is the current frame then no action will happen.
*/ */
function switchToParentFrame(msg) { function switchToParentFrame(msg) {
let command_id = msg.json.command_id; curContainer.frame = curContainer.frame.parent;
curContainer.frame = curContainer.frame.parent; let parentElement = seenEls.add(curContainer.frame);
let parentElement = seenEls.add(curContainer.frame);
sendSyncMessage( sendSyncMessage(
"Marionette:switchedToFrame", {frameValue: parentElement}); "Marionette:switchedToFrame", {frameValue: parentElement});
sendOk(msg.json.command_id); sendOk(msg.json.command_id);
} }
/** /**
* Switch to frame given either the server-assigned element id, * Switch to frame given either the server-assigned element id,
@ -1476,24 +1560,27 @@ function switchToFrame(msg) {
// Check of the curContainer.frame reference is dead // Check of the curContainer.frame reference is dead
try { try {
frames = curContainer.frame.frames; frames = curContainer.frame.frames;
//Until Bug 761935 lands, we won't have multiple nested OOP iframes. We will only have one. // Until Bug 761935 lands, we won't have multiple nested OOP
//parWindow will refer to the iframe above the nested OOP frame. // iframes. We will only have one. parWindow will refer to the iframe
// above the nested OOP frame.
parWindow = curContainer.frame.QueryInterface(Ci.nsIInterfaceRequestor) parWindow = curContainer.frame.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).outerWindowID; .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
} catch (e) { } catch (e) {
// We probably have a dead compartment so accessing it is going to make Firefox // We probably have a dead compartment so accessing it is going to
// very upset. Let's now try redirect everything to the top frame even if the // make Firefox very upset. Let's now try redirect everything to the
// user has given us a frame since search doesnt look up. // top frame even if the user has given us a frame since search doesnt
// look up.
msg.json.id = null; msg.json.id = null;
msg.json.element = null; msg.json.element = null;
} }
if ((msg.json.id === null || msg.json.id === undefined) && (msg.json.element == null)) { if ((msg.json.id === null || msg.json.id === undefined) &&
(msg.json.element == null)) {
// returning to root frame // returning to root frame
sendSyncMessage("Marionette:switchedToFrame", { frameValue: null }); sendSyncMessage("Marionette:switchedToFrame", {frameValue: null});
curContainer.frame = content; curContainer.frame = content;
if(msg.json.focus == true) { if (msg.json.focus == true) {
curContainer.frame.focus(); curContainer.frame.focus();
} }
@ -1513,8 +1600,11 @@ function switchToFrame(msg) {
if (frames.length > 0) { if (frames.length > 0) {
for (let i = 0; i < frames.length; i++) { for (let i = 0; i < frames.length; i++) {
// use XPCNativeWrapper to compare elements; see bug 834266 // use XPCNativeWrapper to compare elements; see bug 834266
if (XPCNativeWrapper(frames[i].frameElement) == XPCNativeWrapper(wantedFrame)) { let frameEl = frames[i].frameElement;
curContainer.frame = frames[i].frameElement; let wrappedItem = new XPCNativeWrapper(frameEl);
let wrappedWanted = new XPCNativeWrapper(wantedFrame);
if (wrappedItem == wrappedWanted) {
curContainer.frame = frameEl;
foundFrame = i; foundFrame = i;
} }
} }
@ -1524,9 +1614,13 @@ function switchToFrame(msg) {
// Either the frame has been removed or we have a OOP frame // Either the frame has been removed or we have a OOP frame
// so lets just get all the iframes and do a quick loop before // so lets just get all the iframes and do a quick loop before
// throwing in the towel // throwing in the towel
let iframes = curContainer.frame.document.getElementsByTagName("iframe"); const doc = curContainer.frame.document;
let iframes = doc.getElementsByTagName("iframe");
for (var i = 0; i < iframes.length; i++) { for (var i = 0; i < iframes.length; i++) {
if (XPCNativeWrapper(iframes[i]) == XPCNativeWrapper(wantedFrame)) { let frameEl = iframes[i];
let wrappedEl = new XPCNativeWrapper(frameEl);
let wrappedWanted = new XPCNativeWrapper(wantedFrame);
if (wrappedEl == wrappedWanted) {
curContainer.frame = iframes[i]; curContainer.frame = iframes[i];
foundFrame = i; foundFrame = i;
} }
@ -1535,20 +1629,19 @@ function switchToFrame(msg) {
} }
if (foundFrame === null) { if (foundFrame === null) {
if (typeof(msg.json.id) === 'number') { if (typeof(msg.json.id) === "number") {
try { try {
foundFrame = frames[msg.json.id].frameElement; foundFrame = frames[msg.json.id].frameElement;
if (foundFrame !== null) { if (foundFrame !== null) {
curContainer.frame = foundFrame; curContainer.frame = foundFrame;
foundFrame = seenEls.add(curContainer.frame); foundFrame = seenEls.add(curContainer.frame);
} } else {
else { // If foundFrame is null at this point then we have the top
// If foundFrame is null at this point then we have the top level browsing // level browsing context so should treat it accordingly.
// context so should treat it accordingly. sendSyncMessage("Marionette:switchedToFrame", {frameValue: null});
sendSyncMessage("Marionette:switchedToFrame", { frameValue: null});
curContainer.frame = content; curContainer.frame = content;
if(msg.json.focus == true) { if (msg.json.focus == true) {
curContainer.frame.focus(); curContainer.frame.focus();
} }
@ -1559,7 +1652,8 @@ function switchToFrame(msg) {
// Since window.frames does not return OOP frames it will throw // Since window.frames does not return OOP frames it will throw
// and we land up here. Let's not give up and check if there are // and we land up here. Let's not give up and check if there are
// iframes and switch to the indexed frame there // iframes and switch to the indexed frame there
let iframes = curContainer.frame.document.getElementsByTagName("iframe"); let doc = curContainer.frame.document;
let iframes = doc.getElementsByTagName("iframe");
if (msg.json.id >= 0 && msg.json.id < iframes.length) { if (msg.json.id >= 0 && msg.json.id < iframes.length) {
curContainer.frame = iframes[msg.json.id]; curContainer.frame = iframes[msg.json.id];
foundFrame = msg.json.id; foundFrame = msg.json.id;
@ -1569,15 +1663,17 @@ function switchToFrame(msg) {
} }
if (foundFrame === null) { if (foundFrame === null) {
sendError(new NoSuchFrameError("Unable to locate frame: " + (msg.json.id || msg.json.element)), command_id); let failedFrame = msg.json.id || msg.json.element;
return true; let err = new NoSuchFrameError(`Unable to locate frame: ${failedFrame}`);
sendError(err, command_id);
return;
} }
// send a synchronous message to let the server update the currently active // send a synchronous message to let the server update the currently active
// frame element (for getActiveFrame) // frame element (for getActiveFrame)
let frameValue = evaluate.toJSON( let frameValue = evaluate.toJSON(
curContainer.frame.wrappedJSObject, seenEls)[element.Key]; curContainer.frame.wrappedJSObject, seenEls)[element.Key];
sendSyncMessage("Marionette:switchedToFrame", {frameValue: frameValue}); sendSyncMessage("Marionette:switchedToFrame", {"frameValue": frameValue});
if (curContainer.frame.contentWindow === null) { if (curContainer.frame.contentWindow === null) {
// The frame we want to switch to is a remote/OOP frame; // The frame we want to switch to is a remote/OOP frame;
@ -1740,7 +1836,7 @@ function* reftestWait(url, remote) {
// so that setTimeout(fn, 0) in the load event has run // so that setTimeout(fn, 0) in the load event has run
reftestWait = document.documentElement.classList.contains("reftest-wait"); reftestWait = document.documentElement.classList.contains("reftest-wait");
yield new Promise(resolve => win.setTimeout(resolve, 0)); yield new Promise(resolve => win.setTimeout(resolve, 0));
}; }
let root = document.documentElement; let root = document.documentElement;
if (reftestWait) { if (reftestWait) {

View File

@ -21,7 +21,7 @@ this.EXPORTED_SYMBOLS = [
const logger = Log.repository.getLogger("Marionette"); const logger = Log.repository.getLogger("Marionette");
this.MessageOrigin = { const MessageOrigin = {
Client: 0, Client: 0,
Server: 1, Server: 1,
}; };
@ -42,7 +42,7 @@ this.Message = {};
* @throws {TypeError} * @throws {TypeError}
* If the message type is not recognised. * If the message type is not recognised.
*/ */
Message.fromMsg = function (data) { Message.fromMsg = function(data) {
switch (data[0]) { switch (data[0]) {
case Command.TYPE: case Command.TYPE:
return Command.fromMsg(data); return Command.fromMsg(data);
@ -98,7 +98,7 @@ Message.fromMsg = function (data) {
* @param {Object<string, ?>} params * @param {Object<string, ?>} params
* Command parameters. * Command parameters.
*/ */
this.Command = class { class Command {
constructor(msgID, name, params = {}) { constructor(msgID, name, params = {}) {
this.id = assert.integer(msgID); this.id = assert.integer(msgID);
this.name = assert.string(name); this.name = assert.string(name);
@ -148,11 +148,10 @@ this.Command = class {
return new Command(msgID, name, params); return new Command(msgID, name, params);
} }
}; }
Command.TYPE = 0; Command.TYPE = 0;
const validator = { const validator = {
exclusionary: { exclusionary: {
"capabilities": ["error", "value"], "capabilities": ["error", "value"],
@ -161,7 +160,7 @@ const validator = {
"value": ["error", "sessionId", "capabilities"], "value": ["error", "sessionId", "capabilities"],
}, },
set: function (obj, prop, val) { set(obj, prop, val) {
let tests = this.exclusionary[prop]; let tests = this.exclusionary[prop];
if (tests) { if (tests) {
for (let t of tests) { for (let t of tests) {
@ -174,7 +173,7 @@ const validator = {
obj[prop] = val; obj[prop] = val;
return true; return true;
}, },
}; }
/** /**
* The response body is exposed as an argument to commands. * The response body is exposed as an argument to commands.
@ -188,7 +187,7 @@ const validator = {
* {@code value}, {@code sessionId}, or {@code capabilities} have been * {@code value}, {@code sessionId}, or {@code capabilities} have been
* set previously will cause an error. * set previously will cause an error.
*/ */
this.ResponseBody = () => new Proxy({}, validator); const ResponseBody = () => new Proxy({}, validator);
/** /**
* Represents the response returned from the remote end after execution * Represents the response returned from the remote end after execution
@ -209,7 +208,7 @@ this.ResponseBody = () => new Proxy({}, validator);
* @param {function(Response|Message)} respHandler * @param {function(Response|Message)} respHandler
* Function callback called on sending the response. * Function callback called on sending the response.
*/ */
this.Response = class { class Response {
constructor(msgID, respHandler = () => {}) { constructor(msgID, respHandler = () => {}) {
this.id = assert.integer(msgID); this.id = assert.integer(msgID);
this.respHandler_ = assert.callable(respHandler); this.respHandler_ = assert.callable(respHandler);
@ -292,6 +291,6 @@ this.Response = class {
resp.body = body; resp.body = body;
return resp; return resp;
} }
}; }
Response.TYPE = 1; Response.TYPE = 1;

View File

@ -10,6 +10,8 @@ Cu.import("resource://gre/modules/Services.jsm");
this.EXPORTED_SYMBOLS = ["modal"]; this.EXPORTED_SYMBOLS = ["modal"];
const COMMON_DIALOG = "chrome://global/content/commonDialog.xul";
const isFirefox = () => Services.appinfo.name == "Firefox"; const isFirefox = () => Services.appinfo.name == "Firefox";
this.modal = {}; this.modal = {};
@ -18,8 +20,8 @@ modal = {
TABMODAL_DIALOG_LOADED: "tabmodal-dialog-loaded", TABMODAL_DIALOG_LOADED: "tabmodal-dialog-loaded",
handlers: { handlers: {
"common-dialog-loaded": new Set(), "common-dialog-loaded": new Set(),
"tabmodal-dialog-loaded": new Set() "tabmodal-dialog-loaded": new Set(),
} },
}; };
/** /**
@ -37,7 +39,7 @@ modal = {
* {@code modal.COMMON_DIALOG_LOADED} or * {@code modal.COMMON_DIALOG_LOADED} or
* {@code modal.TABMODAL_DIALOG_LOADED}. * {@code modal.TABMODAL_DIALOG_LOADED}.
*/ */
modal.addHandler = function (handler) { modal.addHandler = function(handler) {
if (!isFirefox()) { if (!isFirefox()) {
return; return;
} }
@ -55,28 +57,32 @@ modal.addHandler = function (handler) {
* Reference to the browser context to check for existent dialogs. * Reference to the browser context to check for existent dialogs.
* *
* @return {modal.Dialog} * @return {modal.Dialog}
* Returns instance of the Dialog class, or `null` if no modal dialog is present. * Returns instance of the Dialog class, or `null` if no modal dialog
* is present.
*/ */
modal.findModalDialogs = function (context) { modal.findModalDialogs = function(context) {
// First check if there is a modal dialog already present for the current browser window. // First check if there is a modal dialog already present for the
// current browser window.
let winEn = Services.wm.getEnumerator(null); let winEn = Services.wm.getEnumerator(null);
while (winEn.hasMoreElements()) { while (winEn.hasMoreElements()) {
let win = winEn.getNext(); let win = winEn.getNext();
// Modal dialogs which do not have an opener set, we cannot detect as long // Modal dialogs which do not have an opener set, we cannot detect
// as GetZOrderDOMWindowEnumerator doesn't work on Linux (Bug 156333). // as long as GetZOrderDOMWindowEnumerator doesn't work on Linux
if (win.document.documentURI === "chrome://global/content/commonDialog.xul" && // (Bug 156333).
if (win.document.documentURI === COMMON_DIALOG &&
win.opener && win.opener === context.window) { win.opener && win.opener === context.window) {
return new modal.Dialog(() => context, Cu.getWeakReference(win)); return new modal.Dialog(() => context, Cu.getWeakReference(win));
} }
} }
// If no modal dialog has been found, also check if there is an open tab modal // If no modal dialog has been found, also check if there is an open
// dialog present for the current tab. // tab modal dialog present for the current tab.
// TODO: Find an adequate implementation for Fennec. // TODO: Find an adequate implementation for Fennec.
if (context.tab && context.tabBrowser.getTabModalPromptBox) { if (context.tab && context.tabBrowser.getTabModalPromptBox) {
let contentBrowser = context.contentBrowser; let contentBrowser = context.contentBrowser;
let promptManager = context.tabBrowser.getTabModalPromptBox(contentBrowser); let promptManager =
context.tabBrowser.getTabModalPromptBox(contentBrowser);
let prompts = promptManager.listPrompts(); let prompts = promptManager.listPrompts();
if (prompts.length) { if (prompts.length) {
@ -96,7 +102,7 @@ modal.findModalDialogs = function (context) {
* The handler previously passed to modal.addHandler which will now * The handler previously passed to modal.addHandler which will now
* be removed. * be removed.
*/ */
modal.removeHandler = function (toRemove) { modal.removeHandler = function(toRemove) {
if (!isFirefox()) { if (!isFirefox()) {
return; return;
} }

View File

@ -25,7 +25,7 @@ this.navigate = {};
* @throws TypeError * @throws TypeError
* If |url| is an invalid URL. * If |url| is an invalid URL.
*/ */
navigate.isLoadEventExpected = function (url) { navigate.isLoadEventExpected = function(url) {
// assume we will go somewhere exciting // assume we will go somewhere exciting
if (typeof url == "undefined") { if (typeof url == "undefined") {
throw TypeError("Expected destination URL"); throw TypeError("Expected destination URL");

View File

@ -25,27 +25,28 @@
*/ */
const {classes: Cc, interfaces: Ci, utils: Cu} = Components; const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const { StreamUtils } = Cu.import("chrome://marionette/content/stream-utils.js"); const {StreamUtils} =
Cu.import("chrome://marionette/content/stream-utils.js", {});
const unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] const unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter); .createInstance(Ci.nsIScriptableUnicodeConverter);
unicodeConverter.charset = "UTF-8"; unicodeConverter.charset = "UTF-8";
const defer = function () { const defer = function() {
let deferred = { let deferred = {
promise: new Promise((resolve, reject) => { promise: new Promise((resolve, reject) => {
deferred.resolve = resolve; deferred.resolve = resolve;
deferred.reject = reject; deferred.reject = reject;
}) }),
}; };
return deferred; return deferred;
}; };
this.EXPORTED_SYMBOLS = ["RawPacket", "Packet", "JSONPacket", "BulkPacket"]; this.EXPORTED_SYMBOLS = ["RawPacket", "Packet", "JSONPacket", "BulkPacket"];
// The transport's previous check ensured the header length did not exceed 20 // The transport's previous check ensured the header length did not
// characters. Here, we opt for the somewhat smaller, but still large limit of // exceed 20 characters. Here, we opt for the somewhat smaller, but still
// 1 TiB. // large limit of 1 TiB.
const PACKET_LENGTH_MAX = Math.pow(2, 40); const PACKET_LENGTH_MAX = Math.pow(2, 40);
/** /**
@ -57,17 +58,19 @@ function Packet(transport) {
} }
/** /**
* Attempt to initialize a new Packet based on the incoming packet header we've * Attempt to initialize a new Packet based on the incoming packet header
* received so far. We try each of the types in succession, trying JSON packets * we've received so far. We try each of the types in succession, trying
* first since they are much more common. * JSON packets first since they are much more common.
* @param header string *
* The packet header string to attempt parsing. * @param {string} header
* @param transport DebuggerTransport * Packet header string to attempt parsing.
* The transport instance that will own the packet. * @param {DebuggerTransport} transport
* @return Packet * Transport instance that will own the packet.
* The parsed packet of the matching type, or null if no types matched. *
* @return {Packet}
* Parsed packet of the matching type, or null if no types matched.
*/ */
Packet.fromHeader = function (header, transport) { Packet.fromHeader = function(header, transport) {
return JSONPacket.fromHeader(header, transport) || return JSONPacket.fromHeader(header, transport) ||
BulkPacket.fromHeader(header, transport); BulkPacket.fromHeader(header, transport);
}; };
@ -86,20 +89,20 @@ Packet.prototype = {
this._length = length; this._length = length;
}, },
destroy: function () { destroy() {
this._transport = null; this._transport = null;
} },
}; };
/** /**
* With a JSON packet (the typical packet type sent via the transport), data is * With a JSON packet (the typical packet type sent via the transport),
* transferred as a JSON packet serialized into a string, with the string length * data is transferred as a JSON packet serialized into a string,
* prepended to the packet, followed by a colon ([length]:[packet]). The * with the string length prepended to the packet, followed by a colon
* contents of the JSON packet are specified in the Remote Debugging Protocol * ([length]:[packet]). The contents of the JSON packet are specified in
* specification. * the Remote Debugging Protocol specification.
* @param transport DebuggerTransport *
* The transport instance that will own the packet. * @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*/ */
function JSONPacket(transport) { function JSONPacket(transport) {
Packet.call(this, transport); Packet.call(this, transport);
@ -108,16 +111,18 @@ function JSONPacket(transport) {
} }
/** /**
* Attempt to initialize a new JSONPacket based on the incoming packet header * Attempt to initialize a new JSONPacket based on the incoming packet
* we've received so far. * header we've received so far.
* @param header string *
* The packet header string to attempt parsing. * @param {string} header
* @param transport DebuggerTransport * Packet header string to attempt parsing.
* The transport instance that will own the packet. * @param {DebuggerTransport} transport
* @return JSONPacket * Transport instance that will own the packet.
* The parsed packet, or null if it's not a match. *
* @return {JSONPacket}
* Parsed packet, or null if it's not a match.
*/ */
JSONPacket.fromHeader = function (header, transport) { JSONPacket.fromHeader = function(header, transport) {
let match = this.HEADER_PATTERN.exec(header); let match = this.HEADER_PATTERN.exec(header);
if (!match) { if (!match) {
@ -137,22 +142,22 @@ Object.defineProperty(JSONPacket.prototype, "object", {
/** /**
* Gets the object (not the serialized string) being read or written. * Gets the object (not the serialized string) being read or written.
*/ */
get: function () { get() {
return this._object; return this._object;
}, },
/** /**
* Sets the object to be sent when write() is called. * Sets the object to be sent when write() is called.
*/ */
set: function (object) { set(object) {
this._object = object; this._object = object;
let data = JSON.stringify(object); let data = JSON.stringify(object);
this._data = unicodeConverter.ConvertFromUnicode(data); this._data = unicodeConverter.ConvertFromUnicode(data);
this.length = this._data.length; this.length = this._data.length;
} },
}); });
JSONPacket.prototype.read = function (stream, scriptableStream) { JSONPacket.prototype.read = function(stream, scriptableStream) {
// Read in more packet data. // Read in more packet data.
this._readData(stream, scriptableStream); this._readData(stream, scriptableStream);
@ -177,14 +182,15 @@ JSONPacket.prototype.read = function (stream, scriptableStream) {
this._transport._onJSONObjectReady(this._object); this._transport._onJSONObjectReady(this._object);
}; };
JSONPacket.prototype._readData = function (stream, scriptableStream) { JSONPacket.prototype._readData = function(stream, scriptableStream) {
let bytesToRead = Math.min(this.length - this._data.length, let bytesToRead = Math.min(
stream.available()); this.length - this._data.length,
stream.available());
this._data += scriptableStream.readBytes(bytesToRead); this._data += scriptableStream.readBytes(bytesToRead);
this._done = this._data.length === this.length; this._done = this._data.length === this.length;
}; };
JSONPacket.prototype.write = function (stream) { JSONPacket.prototype.write = function(stream) {
if (this._outgoing === undefined) { if (this._outgoing === undefined) {
// Format the serialized packet to a buffer // Format the serialized packet to a buffer
@ -197,29 +203,30 @@ JSONPacket.prototype.write = function (stream) {
}; };
Object.defineProperty(JSONPacket.prototype, "done", { Object.defineProperty(JSONPacket.prototype, "done", {
get: function () { get() {
return this._done; return this._done;
} },
}); });
JSONPacket.prototype.toString = function () { JSONPacket.prototype.toString = function() {
return JSON.stringify(this._object, null, 2); return JSON.stringify(this._object, null, 2);
}; };
/** /**
* With a bulk packet, data is transferred by temporarily handing over the * With a bulk packet, data is transferred by temporarily handing over
* transport's input or output stream to the application layer for writing data * the transport's input or output stream to the application layer for
* directly. This can be much faster for large data sets, and avoids various * writing data directly. This can be much faster for large data sets,
* stages of copies and data duplication inherent in the JSON packet type. The * and avoids various stages of copies and data duplication inherent in
* bulk packet looks like: * the JSON packet type. The bulk packet looks like:
* *
* bulk [actor] [type] [length]:[data] * bulk [actor] [type] [length]:[data]
* *
* The interpretation of the data portion depends on the kind of actor and the * The interpretation of the data portion depends on the kind of actor and
* packet's type. See the Remote Debugging Protocol Stream Transport spec for * the packet's type. See the Remote Debugging Protocol Stream Transport
* more details. * spec for more details.
* @param transport DebuggerTransport *
* The transport instance that will own the packet. * @param {DebuggerTransport} transport
* Transport instance that will own the packet.
*/ */
function BulkPacket(transport) { function BulkPacket(transport) {
Packet.call(this, transport); Packet.call(this, transport);
@ -228,16 +235,18 @@ function BulkPacket(transport) {
} }
/** /**
* Attempt to initialize a new BulkPacket based on the incoming packet header * Attempt to initialize a new BulkPacket based on the incoming packet
* we've received so far. * header we've received so far.
* @param header string *
* The packet header string to attempt parsing. * @param {string} header
* @param transport DebuggerTransport * Packet header string to attempt parsing.
* The transport instance that will own the packet. * @param {DebuggerTransport} transport
* @return BulkPacket * Transport instance that will own the packet.
* The parsed packet, or null if it's not a match. *
* @return {BulkPacket}
* Parsed packet, or null if it's not a match.
*/ */
BulkPacket.fromHeader = function (header, transport) { BulkPacket.fromHeader = function(header, transport) {
let match = this.HEADER_PATTERN.exec(header); let match = this.HEADER_PATTERN.exec(header);
if (!match) { if (!match) {
@ -248,7 +257,7 @@ BulkPacket.fromHeader = function (header, transport) {
packet.header = { packet.header = {
actor: match[1], actor: match[1],
type: match[2], type: match[2],
length: +match[3] length: +match[3],
}; };
return packet; return packet;
}; };
@ -257,7 +266,7 @@ BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
BulkPacket.prototype = Object.create(Packet.prototype); BulkPacket.prototype = Object.create(Packet.prototype);
BulkPacket.prototype.read = function (stream) { BulkPacket.prototype.read = function(stream) {
// Temporarily pause monitoring of the input stream // Temporarily pause monitoring of the input stream
this._transport.pauseIncoming(); this._transport.pauseIncoming();
@ -272,8 +281,8 @@ BulkPacket.prototype.read = function (stream) {
deferred.resolve(copying); deferred.resolve(copying);
return copying; return copying;
}, },
stream: stream, stream,
done: deferred done: deferred,
}); });
// Await the result of reading from the stream // Await the result of reading from the stream
@ -288,7 +297,7 @@ BulkPacket.prototype.read = function (stream) {
}; };
}; };
BulkPacket.prototype.write = function (stream) { BulkPacket.prototype.write = function(stream) {
if (this._outgoingHeader === undefined) { if (this._outgoingHeader === undefined) {
// Format the serialized packet header to a buffer // Format the serialized packet header to a buffer
this._outgoingHeader = "bulk " + this.actor + " " + this.type + " " + this._outgoingHeader = "bulk " + this.actor + " " + this.type + " " +
@ -314,8 +323,8 @@ BulkPacket.prototype.write = function (stream) {
deferred.resolve(copying); deferred.resolve(copying);
return copying; return copying;
}, },
stream: stream, stream,
done: deferred done: deferred,
}); });
// Await the result of writing to the stream // Await the result of writing to the stream
@ -331,21 +340,21 @@ BulkPacket.prototype.write = function (stream) {
}; };
Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", { Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
get: function () { get() {
return this._readyForWriting.promise; return this._readyForWriting.promise;
} },
}); });
Object.defineProperty(BulkPacket.prototype, "header", { Object.defineProperty(BulkPacket.prototype, "header", {
get: function () { get() {
return { return {
actor: this.actor, actor: this.actor,
type: this.type, type: this.type,
length: this.length length: this.length,
}; };
}, },
set: function (header) { set(header) {
this.actor = header.actor; this.actor = header.actor;
this.type = header.type; this.type = header.type;
this.length = header.length; this.length = header.length;
@ -353,12 +362,12 @@ Object.defineProperty(BulkPacket.prototype, "header", {
}); });
Object.defineProperty(BulkPacket.prototype, "done", { Object.defineProperty(BulkPacket.prototype, "done", {
get: function () { get() {
return this._done; return this._done;
}, },
}); });
BulkPacket.prototype.toString = function () { BulkPacket.prototype.toString = function() {
return "Bulk: " + JSON.stringify(this.header, null, 2); return "Bulk: " + JSON.stringify(this.header, null, 2);
}; };
@ -379,19 +388,19 @@ function RawPacket(transport, data) {
RawPacket.prototype = Object.create(Packet.prototype); RawPacket.prototype = Object.create(Packet.prototype);
RawPacket.prototype.read = function (stream) { RawPacket.prototype.read = function(stream) {
// This hasn't yet been needed for testing. // This hasn't yet been needed for testing.
throw Error("Not implmented."); throw Error("Not implmented.");
}; };
RawPacket.prototype.write = function (stream) { RawPacket.prototype.write = function(stream) {
let written = stream.write(this._data, this._data.length); let written = stream.write(this._data, this._data.length);
this._data = this._data.slice(written); this._data = this._data.slice(written);
this._done = !this._data.length; this._done = !this._data.length;
}; };
Object.defineProperty(RawPacket.prototype, "done", { Object.defineProperty(RawPacket.prototype, "done", {
get: function () { get() {
return this._done; return this._done;
} },
}); });

View File

@ -30,7 +30,7 @@ var ownPriorityGetterTrap = {
return obj[prop]; return obj[prop];
} }
return (...args) => obj.send(prop, args); return (...args) => obj.send(prop, args);
} },
}; };
this.proxy = {}; this.proxy = {};
@ -52,8 +52,9 @@ this.proxy = {};
* @param {function(string, Object, number)} sendAsyncFn * @param {function(string, Object, number)} sendAsyncFn
* Callback for sending async messages. * Callback for sending async messages.
*/ */
proxy.toListener = function (mmFn, sendAsyncFn, browserFn) { proxy.toListener = function(mmFn, sendAsyncFn, browserFn) {
let sender = new proxy.AsyncMessageChannel(mmFn, sendAsyncFn, browserFn); let sender = new proxy.AsyncMessageChannel(
mmFn, sendAsyncFn, browserFn);
return new Proxy(sender, ownPriorityGetterTrap); return new Proxy(sender, ownPriorityGetterTrap);
}; };
@ -100,8 +101,8 @@ proxy.AsyncMessageChannel = class {
* @param {string} name * @param {string} name
* Function to call in the listener, e.g. for the message listener * Function to call in the listener, e.g. for the message listener
* "Marionette:foo8", use "foo". * "Marionette:foo8", use "foo".
* @param {Array.<?>=} args * @param {Array.<?>=} args
* Argument list to pass the function. If args has a single entry * Argument list to pass the function. If args has a single entry
* that is an object, we assume it's an old style dispatch, and * that is an object, we assume it's an old style dispatch, and
* the object will passed literally. * the object will passed literally.
* *
@ -142,7 +143,7 @@ proxy.AsyncMessageChannel = class {
// The currently selected tab or window has been closed. No clean-up // The currently selected tab or window has been closed. No clean-up
// is necessary to do because all loaded listeners are gone. // is necessary to do because all loaded listeners are gone.
this.closeHandler = event => { this.closeHandler = event => {
logger.debug(`Received DOM event "${event.type}" for "${event.target}"`); logger.debug(`Received DOM event ${event.type} for ${event.target}`);
switch (event.type) { switch (event.type) {
case "TabClose": case "TabClose":
@ -183,15 +184,15 @@ proxy.AsyncMessageChannel = class {
addHandlers() { addHandlers() {
modal.addHandler(this.dialogueObserver_); modal.addHandler(this.dialogueObserver_);
// Register event handlers in case the command closes the current tab or window, // Register event handlers in case the command closes the current
// and the promise has to be escaped. // tab or window, and the promise has to be escaped.
if (this.browser) { if (this.browser) {
this.browser.window.addEventListener("unload", this.closeHandler, false); this.browser.window.addEventListener("unload", this.closeHandler);
if (this.browser.tab) { if (this.browser.tab) {
let node = this.browser.tab.addEventListener ? let node = this.browser.tab.addEventListener ?
this.browser.tab : this.browser.contentBrowser; this.browser.tab : this.browser.contentBrowser;
node.addEventListener("TabClose", this.closeHandler, false); node.addEventListener("TabClose", this.closeHandler);
} }
} }
} }
@ -203,13 +204,12 @@ proxy.AsyncMessageChannel = class {
modal.removeHandler(this.dialogueObserver_); modal.removeHandler(this.dialogueObserver_);
if (this.browser) { if (this.browser) {
this.browser.window.removeEventListener("unload", this.closeHandler, false); this.browser.window.removeEventListener("unload", this.closeHandler);
if (this.browser.tab) { if (this.browser.tab) {
let node = this.browser.tab.addEventListener ? let node = this.browser.tab.addEventListener ?
this.browser.tab : this.browser.contentBrowser; this.browser.tab : this.browser.contentBrowser;
node.removeEventListener("TabClose", this.closeHandler);
node.removeEventListener("TabClose", this.closeHandler, false);
} }
} }
} }
@ -263,7 +263,7 @@ proxy.AsyncMessageChannel = class {
payload = data; payload = data;
} }
const msg = {type: type, data: payload}; const msg = {type, data: payload};
// here sendAsync is actually the content frame's // here sendAsync is actually the content frame's
// sendAsyncMessage(path, message) global // sendAsyncMessage(path, message) global
@ -307,7 +307,7 @@ proxy.AsyncMessageChannel = class {
removeAllListeners_() { removeAllListeners_() {
let ok = true; let ok = true;
for (let [p, cb] of this.listeners_) { for (let [p] of this.listeners_) {
ok |= this.removeListener_(p); ok |= this.removeListener_(p);
} }
return ok; return ok;
@ -327,7 +327,7 @@ proxy.AsyncMessageChannel.ReplyType = {
* The content frame's message manager, which itself is usually an * The content frame's message manager, which itself is usually an
* implementor of. * implementor of.
*/ */
proxy.toChromeAsync = function (frameMessageManager) { proxy.toChromeAsync = function(frameMessageManager) {
let sender = new AsyncChromeSender(frameMessageManager); let sender = new AsyncChromeSender(frameMessageManager);
return new Proxy(sender, ownPriorityGetterTrap); return new Proxy(sender, ownPriorityGetterTrap);
}; };
@ -342,7 +342,7 @@ proxy.toChromeAsync = function (frameMessageManager) {
* let promise = sender.send("runEmulatorCmd", "my command"); * let promise = sender.send("runEmulatorCmd", "my command");
* let rv = yield promise; * let rv = yield promise;
*/ */
this.AsyncChromeSender = class { class AsyncChromeSender {
constructor(frameMessageManager) { constructor(frameMessageManager) {
this.mm = frameMessageManager; this.mm = frameMessageManager;
} }
@ -353,7 +353,7 @@ this.AsyncChromeSender = class {
* @param {string} name * @param {string} name
* Function to call in the chrome, e.g. for "Marionette:foo", use * Function to call in the chrome, e.g. for "Marionette:foo", use
* "foo". * "foo".
* @param {?} args * @param {?} args
* Argument list to pass the function. Must be JSON serialisable. * Argument list to pass the function. Must be JSON serialisable.
* *
* @return {Promise} * @return {Promise}
@ -389,7 +389,7 @@ this.AsyncChromeSender = class {
return proxy; return proxy;
} }
}; }
/** /**
* Creates a transparent interface from the content- to the chrome context. * Creates a transparent interface from the content- to the chrome context.
@ -406,7 +406,7 @@ this.AsyncChromeSender = class {
* @param {nsISyncMessageSender} sendSyncMessageFn * @param {nsISyncMessageSender} sendSyncMessageFn
* The frame message manager's sendSyncMessage function. * The frame message manager's sendSyncMessage function.
*/ */
proxy.toChrome = function (sendSyncMessageFn) { proxy.toChrome = function(sendSyncMessageFn) {
let sender = new proxy.SyncChromeSender(sendSyncMessageFn); let sender = new proxy.SyncChromeSender(sendSyncMessageFn);
return new Proxy(sender, ownPriorityGetterTrap); return new Proxy(sender, ownPriorityGetterTrap);
}; };
@ -432,7 +432,7 @@ proxy.SyncChromeSender = class {
} }
}; };
var marshal = function (args) { var marshal = function(args) {
if (args.length == 1 && typeof args[0] == "object") { if (args.length == 1 && typeof args[0] == "object") {
return args[0]; return args[0];
} }

View File

@ -13,7 +13,7 @@ Cu.import("resource://gre/modules/Task.jsm");
Cu.import("chrome://marionette/content/assert.js"); Cu.import("chrome://marionette/content/assert.js");
Cu.import("chrome://marionette/content/capture.js"); Cu.import("chrome://marionette/content/capture.js");
const {InvalidArgumentError} = const {InvalidArgumentError} =
Cu.import("chrome//marionette/content/error.js", {}); Cu.import("chrome://marionette/content/error.js", {});
this.EXPORTED_SYMBOLS = ["reftest"]; this.EXPORTED_SYMBOLS = ["reftest"];
@ -25,7 +25,7 @@ const logger = Log.repository.getLogger("Marionette");
const SCREENSHOT_MODE = { const SCREENSHOT_MODE = {
unexpected: 0, unexpected: 0,
fail: 1, fail: 1,
always: 2 always: 2,
}; };
const STATUS = { const STATUS = {
@ -46,7 +46,7 @@ reftest.Runner = class {
this.driver = driver; this.driver = driver;
this.canvasCache = new Map([[null, []]]); this.canvasCache = new Map([[null, []]]);
this.windowUtils = null; this.windowUtils = null;
this.lastUrl = null; this.lastURL = null;
this.remote = Preferences.get(PREF_E10S); this.remote = Preferences.get(PREF_E10S);
} }
@ -73,7 +73,7 @@ reftest.Runner = class {
.reduce((map, key) => map.set(key, urlCount[key]), new Map()); .reduce((map, key) => map.set(key, urlCount[key]), new Map());
yield this.ensureWindow(); yield this.ensureWindow();
}; }
*ensureWindow() { *ensureWindow() {
if (this.reftestWin && !this.reftestWin.closed) { if (this.reftestWin && !this.reftestWin.closed) {
@ -189,7 +189,7 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
let result = yield Promise.race([testRunner, timeoutPromise]); let result = yield Promise.race([testRunner, timeoutPromise]);
this.parentWindow.clearTimeout(timeoutHandle); this.parentWindow.clearTimeout(timeoutHandle);
if(result.status === STATUS.TIMEOUT) { if (result.status === STATUS.TIMEOUT) {
this.abort(); this.abort();
} }
@ -197,9 +197,13 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
} }
*runTest(testUrl, references, expected, timeout) { *runTest(testUrl, references, expected, timeout) {
let win = yield this.ensureWindow(); let win = yield this.ensureWindow();
function toBase64(screenshot) {
let dataURL = screenshot.canvas.toDataURL();
return dataURL.split(",")[1];
}
win.innerWidth = 600; win.innerWidth = 600;
win.innerHeight = 600; win.innerHeight = 600;
@ -208,7 +212,7 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
let screenshotData = []; let screenshotData = [];
let stack = []; let stack = [];
for (let i = references.length-1; i >= 0; i--) { for (let i = references.length - 1; i >= 0; i--) {
let item = references[i]; let item = references[i];
stack.push([testUrl, item[0], item[1], item[2]]); stack.push([testUrl, item[0], item[1], item[2]]);
} }
@ -219,13 +223,15 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
let [lhsUrl, rhsUrl, references, relation] = stack.pop(); let [lhsUrl, rhsUrl, references, relation] = stack.pop();
message += `Testing ${lhsUrl} ${relation} ${rhsUrl}\n`; message += `Testing ${lhsUrl} ${relation} ${rhsUrl}\n`;
let comparison = yield this.compareUrls(win, lhsUrl, rhsUrl, relation, timeout); let comparison = yield this.compareUrls(
win, lhsUrl, rhsUrl, relation, timeout);
function recordScreenshot() { function recordScreenshot() {
let toBase64 = screenshot => screenshot.canvas.toDataURL().split(",")[1]; let encodedLHS = toBase64(comparison.lhs);
screenshotData.push([{url: lhsUrl, screenshot: toBase64(comparison.lhs)}, let encodedRHS = toBase64(comparison.rhs);
relation, screenshotData.push([{url: lhsUrl, screenshot: encodedLHS},
{url:rhsUrl, screenshot: toBase64(comparison.rhs)}]); relation,
{url: rhsUrl, screenshot: encodedRHS}]);
} }
if (this.screenshotMode === SCREENSHOT_MODE.always) { if (this.screenshotMode === SCREENSHOT_MODE.always) {
@ -241,16 +247,18 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
} else { } else {
// Reached a leaf node so all of one reference chain passed // Reached a leaf node so all of one reference chain passed
status = STATUS.PASS; status = STATUS.PASS;
if (this.screenshotMode <= SCREENSHOT_MODE.fail && expected != status) { if (this.screenshotMode <= SCREENSHOT_MODE.fail &&
expected != status) {
recordScreenshot(); recordScreenshot();
} }
break; break;
} }
} else if (!stack.length) { } else if (!stack.length) {
// If we don't have any alternatives to try then this will be the last iteration, // If we don't have any alternatives to try then this will be
// so save the failing screenshots if required // the last iteration, so save the failing screenshots if required.
if (this.screenshotMode === SCREENSHOT_MODE.fail || let isFail = this.screenshotMode === SCREENSHOT_MODE.fail;
(this.screenshotMode === SCREENSHOT_MODE.unexpected && expected != status)) { let isUnexpected = this.screenshotMode === SCREENSHOT_MODE.unexpected;
if (isFail || (isUnexpected && expected != status)) {
recordScreenshot(); recordScreenshot();
} }
} }
@ -267,13 +275,14 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
let result = {status, message, extra: {}}; let result = {status, message, extra: {}};
if (screenshotData.length) { if (screenshotData.length) {
// For now the tbpl formatter only accepts one screenshot, so just return the // For now the tbpl formatter only accepts one screenshot, so just
// last one we took. // return the last one we took.
result.extra.reftest_screenshots = screenshotData[screenshotData.length - 1]; let lastScreenshot = screenshotData[screenshotData.length - 1];
result.extra.reftest_screenshots = lastScreenshot;
} }
return result; return result;
}; }
*compareUrls(win, lhsUrl, rhsUrl, relation, timeout) { *compareUrls(win, lhsUrl, rhsUrl, relation, timeout) {
logger.info(`Testing ${lhsUrl} ${relation} ${rhsUrl}`); logger.info(`Testing ${lhsUrl} ${relation} ${rhsUrl}`);
@ -285,21 +294,25 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
let maxDifferences = {}; let maxDifferences = {};
let differences = this.windowUtils.compareCanvases(lhs.canvas, rhs.canvas, maxDifferences); let differences = this.windowUtils.compareCanvases(
lhs.canvas, rhs.canvas, maxDifferences);
let passed; let passed;
switch (relation) { switch (relation) {
case "==": case "==":
passed = differences === 0; passed = differences === 0;
if (!passed) { if (!passed) {
logger.info(`Found ${differences} pixels different, maximum difference per channel ${maxDifferences.value}`); logger.info(`Found ${differences} pixels different, ` +
} `maximum difference per channel ${maxDifferences.value}`);
break; }
case "!=": break;
passed = differences !== 0;
break; case "!=":
default: passed = differences !== 0;
throw new InvalidArgumentError("Reftest operator should be '==' or '!='"); break;
default:
throw new InvalidArgumentError("Reftest operator should be '==' or '!='");
} }
return {lhs, rhs, passed}; return {lhs, rhs, passed};
@ -309,7 +322,8 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
let canvas = null; let canvas = null;
let remainingCount = this.urlCount.get(url) || 1; let remainingCount = this.urlCount.get(url) || 1;
let cache = remainingCount > 1; let cache = remainingCount > 1;
logger.debug(`screenshot ${url} remainingCount: ${remainingCount} cache: ${cache}`); logger.debug(`screenshot ${url} remainingCount: ` +
`${remainingCount} cache: ${cache}`);
let reuseCanvas = false; let reuseCanvas = false;
if (this.canvasCache.has(url)) { if (this.canvasCache.has(url)) {
logger.debug(`screenshot ${url} taken from cache`); logger.debug(`screenshot ${url} taken from cache`);
@ -332,26 +346,34 @@ min-width: 600px; min-height: 600px; max-width: 600px; max-height: 600px`;
ctxInterface.DRAWWINDOW_DRAW_VIEW; ctxInterface.DRAWWINDOW_DRAW_VIEW;
logger.debug(`Starting load of ${url}`); logger.debug(`Starting load of ${url}`);
if (this.lastUrl === url) { let navigateOpts = {
commandId: this.driver.listener.activeMessageId,
pageTimeout: timeout,
};
if (this.lastURL === url) {
logger.debug(`Refreshing page`); logger.debug(`Refreshing page`);
yield this.driver.listener.refresh({commandId: this.driver.listener.activeMessageId, yield this.driver.listener.refresh(navigateOpts);
pageTimeout: timeout});
} else { } else {
yield this.driver.listener.get({commandId: this.driver.listener.activeMessageId, navigateOpts.url = url;
url: url, navigateOpts.loadEventExpected = false;
pageTimeout: timeout, yield this.driver.listener.get(navigateOpts);
loadEventExpected: false}); this.lastURL = url;
this.lastUrl = url;
} }
this.driver.curBrowser.contentBrowser.focus(); this.driver.curBrowser.contentBrowser.focus();
yield this.driver.listener.reftestWait(url, this.remote); yield this.driver.listener.reftestWait(url, this.remote);
canvas = capture.canvas(win, 0, 0, win.innerWidth, win.innerHeight, {canvas, flags}); canvas = capture.canvas(
win,
0, // left
0, // top
win.innerWidth,
win.innerHeight,
{canvas, flags});
} }
if (cache) { if (cache) {
this.canvasCache.set(url, canvas); this.canvasCache.set(url, canvas);
}; }
this.urlCount.set(url, remainingCount - 1); this.urlCount.set(url, remainingCount - 1);
return {canvas, reuseCanvas}; return {canvas, reuseCanvas};
} }

View File

@ -20,13 +20,14 @@ Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("chrome://marionette/content/assert.js"); Cu.import("chrome://marionette/content/assert.js");
Cu.import("chrome://marionette/content/driver.js"); const {GeckoDriver} = Cu.import("chrome://marionette/content/driver.js", {});
const { const {
error, error,
UnknownCommandError, UnknownCommandError,
} = Cu.import("chrome://marionette/content/error.js", {}); } = Cu.import("chrome://marionette/content/error.js", {});
Cu.import("chrome://marionette/content/message.js"); Cu.import("chrome://marionette/content/message.js");
Cu.import("chrome://marionette/content/transport.js"); const {DebuggerTransport} =
Cu.import("chrome://marionette/content/transport.js", {});
XPCOMUtils.defineLazyServiceGetter( XPCOMUtils.defineLazyServiceGetter(
this, "env", "@mozilla.org/process/environment;1", "nsIEnvironment"); this, "env", "@mozilla.org/process/environment;1", "nsIEnvironment");
@ -161,8 +162,14 @@ const RECOMMENDED_PREFS = new Map([
// Do not show datareporting policy notifications which can // Do not show datareporting policy notifications which can
// interfere with tests // interfere with tests
["datareporting.healthreport.about.reportUrl", "http://%(server)s/dummy/abouthealthreport/"], [
["datareporting.healthreport.documentServerURI", "http://%(server)s/dummy/healthreport/"], "datareporting.healthreport.about.reportUrl",
"http://%(server)s/dummy/abouthealthreport/",
],
[
"datareporting.healthreport.documentServerURI",
"http://%(server)s/dummy/healthreport/",
],
["datareporting.healthreport.logging.consoleEnabled", false], ["datareporting.healthreport.logging.consoleEnabled", false],
["datareporting.healthreport.service.enabled", false], ["datareporting.healthreport.service.enabled", false],
["datareporting.healthreport.service.firstRun", false], ["datareporting.healthreport.service.firstRun", false],
@ -204,7 +211,10 @@ const RECOMMENDED_PREFS = new Map([
["extensions.update.notifyUser", false], ["extensions.update.notifyUser", false],
// Make sure opening about:addons will not hit the network // Make sure opening about:addons will not hit the network
["extensions.webservice.discoverURL", "http://%(server)s/dummy/discoveryURL"], [
"extensions.webservice.discoverURL",
"http://%(server)s/dummy/discoveryURL",
],
// Allow the application to have focus even it runs in the background // Allow the application to have focus even it runs in the background
["focusmanager.testmode", true], ["focusmanager.testmode", true],
@ -277,7 +287,7 @@ server.TCPListener = class {
* @param {number} port * @param {number} port
* Port for server to listen to. * Port for server to listen to.
*/ */
constructor (port) { constructor(port) {
this.port = port; this.port = port;
this.socket = null; this.socket = null;
this.conns = new Set(); this.conns = new Set();
@ -295,12 +305,12 @@ server.TCPListener = class {
* @return {GeckoDriver} * @return {GeckoDriver}
* A driver instance. * A driver instance.
*/ */
driverFactory () { driverFactory() {
Preferences.set(PREF_CONTENT_LISTENER, false); Preferences.set(PREF_CONTENT_LISTENER, false);
return new GeckoDriver(Services.appinfo.name, this); return new GeckoDriver(Services.appinfo.name, this);
} }
set acceptConnections (value) { set acceptConnections(value) {
if (!value) { if (!value) {
logger.info("New connections will no longer be accepted"); logger.info("New connections will no longer be accepted");
} else { } else {
@ -317,7 +327,7 @@ server.TCPListener = class {
* The marionette.port preference will be populated with the value * The marionette.port preference will be populated with the value
* of |this.port|. * of |this.port|.
*/ */
start () { start() {
if (this.alive) { if (this.alive) {
return; return;
} }
@ -347,7 +357,7 @@ server.TCPListener = class {
env.set(ENV_ENABLED, "1"); env.set(ENV_ENABLED, "1");
} }
stop () { stop() {
if (!this.alive) { if (!this.alive) {
return; return;
} }
@ -363,12 +373,12 @@ server.TCPListener = class {
} }
this.alteredPrefs.clear(); this.alteredPrefs.clear();
Services.obs.notifyObservers(this, NOTIFY_RUNNING, false); Services.obs.notifyObservers(this, NOTIFY_RUNNING);
this.alive = false; this.alive = false;
} }
onSocketAccepted (serverSocket, clientSocket) { onSocketAccepted(serverSocket, clientSocket) {
if (!this._acceptConnections) { if (!this._acceptConnections) {
logger.warn("New connections are currently not accepted"); logger.warn("New connections are currently not accepted");
return; return;
@ -383,12 +393,13 @@ server.TCPListener = class {
conn.onclose = this.onConnectionClosed.bind(this); conn.onclose = this.onConnectionClosed.bind(this);
this.conns.add(conn); this.conns.add(conn);
logger.debug(`Accepted connection ${conn.id} from ${clientSocket.host}:${clientSocket.port}`); logger.debug(`Accepted connection ${conn.id} ` +
`from ${clientSocket.host}:${clientSocket.port}`);
conn.sayHello(); conn.sayHello();
transport.ready(); transport.ready();
} }
onConnectionClosed (conn) { onConnectionClosed(conn) {
logger.debug(`Closed connection ${conn.id}`); logger.debug(`Closed connection ${conn.id}`);
this.conns.delete(conn); this.conns.delete(conn);
} }
@ -408,7 +419,7 @@ server.TCPListener = class {
* Factory function that produces a |GeckoDriver|. * Factory function that produces a |GeckoDriver|.
*/ */
server.TCPConnection = class { server.TCPConnection = class {
constructor (connID, transport, driverFactory) { constructor(connID, transport, driverFactory) {
this.id = connID; this.id = connID;
this.conn = transport; this.conn = transport;
@ -432,7 +443,7 @@ server.TCPConnection = class {
* Debugger transport callback that cleans up * Debugger transport callback that cleans up
* after a connection is closed. * after a connection is closed.
*/ */
onClosed (reason) { onClosed(reason) {
this.driver.deleteSession(); this.driver.deleteSession();
if (this.onclose) { if (this.onclose) {
this.onclose(this); this.onclose(this);
@ -451,7 +462,7 @@ server.TCPConnection = class {
* message type, message ID, method name or error, and parameters * message type, message ID, method name or error, and parameters
* or result. * or result.
*/ */
onPacket (data) { onPacket(data) {
// unable to determine how to respond // unable to determine how to respond
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
let e = new TypeError( let e = new TypeError(
@ -505,7 +516,7 @@ server.TCPConnection = class {
* @param {Command} cmd * @param {Command} cmd
* The requested command to execute. * The requested command to execute.
*/ */
execute (cmd) { execute(cmd) {
let resp = this.createResponse(cmd.id); let resp = this.createResponse(cmd.id);
let sendResponse = () => resp.sendConditionally(resp => !resp.sent); let sendResponse = () => resp.sendConditionally(resp => !resp.sent);
let sendError = resp.sendError.bind(resp); let sendError = resp.sendError.bind(resp);
@ -543,14 +554,14 @@ server.TCPConnection = class {
* @return {message.Response} * @return {message.Response}
* Response to the message with |msgID|. * Response to the message with |msgID|.
*/ */
createResponse (msgID) { createResponse(msgID) {
if (typeof msgID != "number") { if (typeof msgID != "number") {
msgID = -1; msgID = -1;
} }
return new Response(msgID, this.send.bind(this)); return new Response(msgID, this.send.bind(this));
} }
sendError (err, cmdID) { sendError(err, cmdID) {
let resp = new Response(cmdID, this.send.bind(this)); let resp = new Response(cmdID, this.send.bind(this));
resp.sendError(err); resp.sendError(err);
} }
@ -562,13 +573,13 @@ server.TCPConnection = class {
* This is the only message sent by Marionette that does not follow * This is the only message sent by Marionette that does not follow
* the regular message format. * the regular message format.
*/ */
sayHello () { sayHello() {
let whatHo = { let whatHo = {
applicationType: "gecko", applicationType: "gecko",
marionetteProtocol: PROTOCOL_VERSION, marionetteProtocol: PROTOCOL_VERSION,
}; };
this.sendRaw(whatHo); this.sendRaw(whatHo);
}; }
/** /**
* Delegates message to client based on the provided {@code cmdID}. * Delegates message to client based on the provided {@code cmdID}.
@ -583,7 +594,7 @@ server.TCPConnection = class {
* @param {Command,Response} msg * @param {Command,Response} msg
* The command or response to send. * The command or response to send.
*/ */
send (msg) { send(msg) {
msg.origin = MessageOrigin.Server; msg.origin = MessageOrigin.Server;
if (msg instanceof Command) { if (msg instanceof Command) {
this.commands_.set(msg.id, msg); this.commands_.set(msg.id, msg);
@ -601,10 +612,10 @@ server.TCPConnection = class {
* @param {Response} resp * @param {Response} resp
* The response to send back to the client. * The response to send back to the client.
*/ */
sendToClient (resp) { sendToClient(resp) {
this.driver.responseCompleted(); this.driver.responseCompleted();
this.sendMessage(resp); this.sendMessage(resp);
}; }
/** /**
* Marshal message to the Marionette message format and send it. * Marshal message to the Marionette message format and send it.
@ -612,7 +623,7 @@ server.TCPConnection = class {
* @param {Command,Response} msg * @param {Command,Response} msg
* The message to send. * The message to send.
*/ */
sendMessage (msg) { sendMessage(msg) {
this.log_(msg); this.log_(msg);
let payload = msg.toMsg(); let payload = msg.toMsg();
this.sendRaw(payload); this.sendRaw(payload);
@ -625,17 +636,17 @@ server.TCPConnection = class {
* @param {Object} payload * @param {Object} payload
* The payload to ship. * The payload to ship.
*/ */
sendRaw (payload) { sendRaw(payload) {
this.conn.send(payload); this.conn.send(payload);
} }
log_ (msg) { log_(msg) {
let a = (msg.origin == MessageOrigin.Client ? " -> " : " <- "); let a = (msg.origin == MessageOrigin.Client ? " -> " : " <- ");
let s = JSON.stringify(msg.toMsg()); let s = JSON.stringify(msg.toMsg());
logger.trace(this.id + a + s); logger.trace(this.id + a + s);
} }
toString () { toString() {
return `[object server.TCPConnection ${this.id}]`; return `[object server.TCPConnection ${this.id}]`;
} }
}; };

View File

@ -32,7 +32,7 @@ this.session = {};
/** Representation of WebDriver session timeouts. */ /** Representation of WebDriver session timeouts. */
session.Timeouts = class { session.Timeouts = class {
constructor () { constructor() {
// disabled // disabled
this.implicit = 0; this.implicit = 0;
// five mintues // five mintues
@ -41,9 +41,9 @@ session.Timeouts = class {
this.script = 30000; this.script = 30000;
} }
toString () { return "[object session.Timeouts]"; } toString() { return "[object session.Timeouts]"; }
toJSON () { toJSON() {
return { return {
implicit: this.implicit, implicit: this.implicit,
pageLoad: this.pageLoad, pageLoad: this.pageLoad,
@ -51,7 +51,7 @@ session.Timeouts = class {
}; };
} }
static fromJSON (json) { static fromJSON(json) {
assert.object(json); assert.object(json);
let t = new session.Timeouts(); let t = new session.Timeouts();
@ -136,7 +136,8 @@ session.Proxy = class {
case "pac": case "pac":
Preferences.set("network.proxy.type", 2); Preferences.set("network.proxy.type", 2);
Preferences.set("network.proxy.autoconfig_url", this.proxyAutoconfigUrl); Preferences.set(
"network.proxy.autoconfig_url", this.proxyAutoconfigUrl);
return true; return true;
case "autodetect": case "autodetect":
@ -156,13 +157,13 @@ session.Proxy = class {
} }
} }
toString () { return "[object session.Proxy]"; } toString() { return "[object session.Proxy]"; }
toJSON () { toJSON() {
return marshal({ return marshal({
proxyType: this.proxyType, proxyType: this.proxyType,
httpProxy: this.httpProxy, httpProxy: this.httpProxy,
httpProxyPort: this.httpProxyPort , httpProxyPort: this.httpProxyPort,
sslProxy: this.sslProxy, sslProxy: this.sslProxy,
sslProxyPort: this.sslProxyPort, sslProxyPort: this.sslProxyPort,
ftpProxy: this.ftpProxy, ftpProxy: this.ftpProxy,
@ -174,7 +175,7 @@ session.Proxy = class {
}); });
} }
static fromJSON (json) { static fromJSON(json) {
let p = new session.Proxy(); let p = new session.Proxy();
if (typeof json == "undefined" || json === null) { if (typeof json == "undefined" || json === null) {
return p; return p;
@ -218,7 +219,7 @@ session.Proxy = class {
/** WebDriver session capabilities representation. */ /** WebDriver session capabilities representation. */
session.Capabilities = class extends Map { session.Capabilities = class extends Map {
constructor () { constructor() {
super([ super([
// webdriver // webdriver
["browserName", appinfo.name], ["browserName", appinfo.name],
@ -241,7 +242,7 @@ session.Capabilities = class extends Map {
]); ]);
} }
set (key, value) { set(key, value) {
if (key === "timeouts" && !(value instanceof session.Timeouts)) { if (key === "timeouts" && !(value instanceof session.Timeouts)) {
throw new TypeError(); throw new TypeError();
} else if (key === "proxy" && !(value instanceof session.Proxy)) { } else if (key === "proxy" && !(value instanceof session.Proxy)) {
@ -272,7 +273,7 @@ session.Capabilities = class extends Map {
* @return {session.Capabilities} * @return {session.Capabilities}
* Internal representation of WebDriver capabilities. * Internal representation of WebDriver capabilities.
*/ */
static fromJSON (json, {merge = false} = {}) { static fromJSON(json, {merge = false} = {}) {
if (typeof json == "undefined" || json === null) { if (typeof json == "undefined" || json === null) {
json = {}; json = {};
} }
@ -285,12 +286,13 @@ session.Capabilities = class extends Map {
} }
// Processes capabilities as described by WebDriver. // Processes capabilities as described by WebDriver.
static merge_ (json) { static merge_(json) {
for (let entry of [json.desiredCapabilities, json.requiredCapabilities]) { for (let entry of [json.desiredCapabilities, json.requiredCapabilities]) {
if (typeof entry == "undefined" || entry === null) { if (typeof entry == "undefined" || entry === null) {
continue; continue;
} }
assert.object(entry, error.pprint`Expected ${entry} to be a capabilities object`); assert.object(entry,
error.pprint`Expected ${entry} to be a capabilities object`);
} }
let desired = json.desiredCapabilities || {}; let desired = json.desiredCapabilities || {};
@ -302,7 +304,7 @@ session.Capabilities = class extends Map {
} }
// Matches capabilities as described by WebDriver. // Matches capabilities as described by WebDriver.
static match_ (caps = {}) { static match_(caps = {}) {
let matched = new session.Capabilities(); let matched = new session.Capabilities();
const defined = v => typeof v != "undefined" && v !== null; const defined = v => typeof v != "undefined" && v !== null;
@ -311,11 +313,11 @@ session.Capabilities = class extends Map {
// Iff |actual| provides some value, or is a wildcard or an exact // Iff |actual| provides some value, or is a wildcard or an exact
// match of |expected|. This means it can be null or undefined, // match of |expected|. This means it can be null or undefined,
// or "*", or "firefox". // or "*", or "firefox".
function stringMatch (actual, expected) { function stringMatch(actual, expected) {
return !defined(actual) || (wildcard(actual) || actual === expected); return !defined(actual) || (wildcard(actual) || actual === expected);
} }
for (let [k,v] of Object.entries(caps)) { for (let [k, v] of Object.entries(caps)) {
switch (k) { switch (k) {
case "browserName": case "browserName":
let bname = matched.get("browserName"); let bname = matched.get("browserName");
@ -368,7 +370,8 @@ session.Capabilities = class extends Map {
if (Object.values(session.PageLoadStrategy).includes(v)) { if (Object.values(session.PageLoadStrategy).includes(v)) {
matched.set("pageLoadStrategy", v); matched.set("pageLoadStrategy", v);
} else { } else {
throw new InvalidArgumentError("Unknown page load strategy: " + v); throw new InvalidArgumentError(
"Unknown page load strategy: " + v);
} }
} }
@ -409,8 +412,8 @@ function marshal(obj) {
function* iter(mapOrObject) { function* iter(mapOrObject) {
if (mapOrObject instanceof Map) { if (mapOrObject instanceof Map) {
for (const [k,v] of mapOrObject) { for (const [k, v] of mapOrObject) {
yield [k,v]; yield [k, v];
} }
} else { } else {
for (const k of Object.keys(mapOrObject)) { for (const k of Object.keys(mapOrObject)) {
@ -419,7 +422,7 @@ function marshal(obj) {
} }
} }
for (let [k,v] of iter(obj)) { for (let [k, v] of iter(obj)) {
// Skip empty values when serialising to JSON. // Skip empty values when serialising to JSON.
if (typeof v == "undefined" || v === null) { if (typeof v == "undefined" || v === null) {
continue; continue;
@ -429,10 +432,9 @@ function marshal(obj) {
// JSON representation. // JSON representation.
if (typeof v.toJSON == "function") { if (typeof v.toJSON == "function") {
v = marshal(v.toJSON()); v = marshal(v.toJSON());
}
// Or do the same for object literals. // Or do the same for object literals.
else if (isObject(v)) { } else if (isObject(v)) {
v = marshal(v); v = marshal(v);
} }

View File

@ -4,7 +4,8 @@
"use strict"; "use strict";
const {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu} = Components; const {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu, results: Cr} =
Components;
Cu.import("resource://gre/modules/EventEmitter.jsm"); Cu.import("resource://gre/modules/EventEmitter.jsm");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
@ -19,37 +20,39 @@ this.EXPORTED_SYMBOLS = ["StreamUtils"];
const BUFFER_SIZE = 0x8000; const BUFFER_SIZE = 0x8000;
/** /**
* This helper function (and its companion object) are used by bulk senders and * This helper function (and its companion object) are used by bulk
* receivers to read and write data in and out of other streams. Functions that * senders and receivers to read and write data in and out of other streams.
* make use of this tool are passed to callers when it is time to read or write * Functions that make use of this tool are passed to callers when it is
* bulk data. It is highly recommended to use these copier functions instead of * time to read or write bulk data. It is highly recommended to use these
* the stream directly because the copier enforces the agreed upon length. * copier functions instead of the stream directly because the copier
* Since bulk mode reuses an existing stream, the sender and receiver must write * enforces the agreed upon length. Since bulk mode reuses an existing
* and read exactly the agreed upon amount of data, or else the entire transport * stream, the sender and receiver must write and read exactly the agreed
* will be left in a invalid state. Additionally, other methods of stream * upon amount of data, or else the entire transport will be left in a
* copying (such as NetUtil.asyncCopy) close the streams involved, which would * invalid state. Additionally, other methods of stream copying (such as
* terminate the debugging transport, and so it is avoided here. * NetUtil.asyncCopy) close the streams involved, which would terminate
* the debugging transport, and so it is avoided here.
* *
* Overall, this *works*, but clearly the optimal solution would be able to just * Overall, this *works*, but clearly the optimal solution would be
* use the streams directly. If it were possible to fully implement * able to just use the streams directly. If it were possible to fully
* nsIInputStream / nsIOutputStream in JS, wrapper streams could be created to * implement nsIInputStream/nsIOutputStream in JS, wrapper streams could
* enforce the length and avoid closing, and consumers could use familiar stream * be created to enforce the length and avoid closing, and consumers could
* utilities like NetUtil.asyncCopy. * use familiar stream utilities like NetUtil.asyncCopy.
* *
* The function takes two async streams and copies a precise number of bytes * The function takes two async streams and copies a precise number
* from one to the other. Copying begins immediately, but may complete at some * of bytes from one to the other. Copying begins immediately, but may
* future time depending on data size. Use the returned promise to know when * complete at some future time depending on data size. Use the returned
* it's complete. * promise to know when it's complete.
* *
* @param input nsIAsyncInputStream * @param {nsIAsyncInputStream} input
* The stream to copy from. * Stream to copy from.
* @param output nsIAsyncOutputStream * @param {nsIAsyncOutputStream} output
* The stream to copy to. * Stream to copy to.
* @param length Integer * @param {number} length
* The amount of data that needs to be copied. * Amount of data that needs to be copied.
* @return Promise *
* The promise is resolved when copying completes or rejected if any * @return {Promise}
* (unexpected) errors occur. * Promise is resolved when copying completes or rejected if any
* (unexpected) errors occur.
*/ */
function copyStream(input, output, length) { function copyStream(input, output, length) {
let copier = new StreamCopier(input, output, length); let copier = new StreamCopier(input, output, length);
@ -60,7 +63,8 @@ function StreamCopier(input, output, length) {
EventEmitter.decorate(this); EventEmitter.decorate(this);
this._id = StreamCopier._nextId++; this._id = StreamCopier._nextId++;
this.input = input; this.input = input;
// Save off the base output stream, since we know it's async as we've required // Save off the base output stream, since we know it's async as we've
// required
this.baseAsyncOutput = output; this.baseAsyncOutput = output;
if (IOUtil.outputStreamIsBuffered(output)) { if (IOUtil.outputStreamIsBuffered(output)) {
this.output = output; this.output = output;
@ -75,7 +79,7 @@ function StreamCopier(input, output, length) {
promise: new Promise((resolve, reject) => { promise: new Promise((resolve, reject) => {
this._deferred.resolve = resolve; this._deferred.resolve = resolve;
this._deferred.reject = reject; this._deferred.reject = reject;
}) }),
}; };
this._copy = this._copy.bind(this); this._copy = this._copy.bind(this);
@ -83,21 +87,22 @@ function StreamCopier(input, output, length) {
this._destroy = this._destroy.bind(this); this._destroy = this._destroy.bind(this);
// Copy promise's then method up to this object. // Copy promise's then method up to this object.
// Allows the copier to offer a promise interface for the simple succeed or //
// fail scenarios, but also emit events (due to the EventEmitter) for other // Allows the copier to offer a promise interface for the simple succeed
// states, like progress. // or fail scenarios, but also emit events (due to the EventEmitter)
// for other states, like progress.
this.then = this._deferred.promise.then.bind(this._deferred.promise); this.then = this._deferred.promise.then.bind(this._deferred.promise);
this.then(this._destroy, this._destroy); this.then(this._destroy, this._destroy);
// Stream ready callback starts as |_copy|, but may switch to |_flush| at end // Stream ready callback starts as |_copy|, but may switch to |_flush|
// if flushing would block the output stream. // at end if flushing would block the output stream.
this._streamReadyCallback = this._copy; this._streamReadyCallback = this._copy;
} }
StreamCopier._nextId = 0; StreamCopier._nextId = 0;
StreamCopier.prototype = { StreamCopier.prototype = {
copy: function () { copy() {
// Dispatch to the next tick so that it's possible to attach a progress // Dispatch to the next tick so that it's possible to attach a progress
// event listener, even for extremely fast copies (like when testing). // event listener, even for extremely fast copies (like when testing).
Services.tm.currentThread.dispatch(() => { Services.tm.currentThread.dispatch(() => {
@ -110,7 +115,7 @@ StreamCopier.prototype = {
return this; return this;
}, },
_copy: function () { _copy() {
let bytesAvailable = this.input.available(); let bytesAvailable = this.input.available();
let amountToCopy = Math.min(bytesAvailable, this._amountLeft); let amountToCopy = Math.min(bytesAvailable, this._amountLeft);
this._debug("Trying to copy: " + amountToCopy); this._debug("Trying to copy: " + amountToCopy);
@ -143,14 +148,14 @@ StreamCopier.prototype = {
this.input.asyncWait(this, 0, 0, Services.tm.currentThread); this.input.asyncWait(this, 0, 0, Services.tm.currentThread);
}, },
_emitProgress: function () { _emitProgress() {
this.emit("progress", { this.emit("progress", {
bytesSent: this._length - this._amountLeft, bytesSent: this._length - this._amountLeft,
totalBytes: this._length totalBytes: this._length,
}); });
}, },
_flush: function () { _flush() {
try { try {
this.output.flush(); this.output.flush();
} catch (e) { } catch (e) {
@ -167,7 +172,7 @@ StreamCopier.prototype = {
this._deferred.resolve(); this._deferred.resolve();
}, },
_destroy: function () { _destroy() {
this._destroy = null; this._destroy = null;
this._copy = null; this._copy = null;
this._flush = null; this._flush = null;
@ -176,37 +181,40 @@ StreamCopier.prototype = {
}, },
// nsIInputStreamCallback // nsIInputStreamCallback
onInputStreamReady: function () { onInputStreamReady() {
this._streamReadyCallback(); this._streamReadyCallback();
}, },
// nsIOutputStreamCallback // nsIOutputStreamCallback
onOutputStreamReady: function () { onOutputStreamReady() {
this._streamReadyCallback(); this._streamReadyCallback();
}, },
_debug: function (msg) { _debug(msg) {
} },
}; };
/** /**
* Read from a stream, one byte at a time, up to the next |delimiter| * Read from a stream, one byte at a time, up to the next |delimiter|
* character, but stopping if we've read |count| without finding it. Reading * character, but stopping if we've read |count| without finding it.
* also terminates early if there are less than |count| bytes available on the * Reading also terminates early if there are less than |count| bytes
* stream. In that case, we only read as many bytes as the stream currently has * available on the stream. In that case, we only read as many bytes as
* to offer. * the stream currently has to offer.
* TODO: This implementation could be removed if bug 984651 is fixed, which *
* provides a native version of the same idea. * TODO: This implementation could be removed if bug 984651 is fixed,
* @param stream nsIInputStream * which provides a native version of the same idea.
* The input stream to read from. *
* @param delimiter string * @param {nsIInputStream} stream
* The character we're trying to find. * Input stream to read from.
* @param count integer * @param {string} delimiter
* The max number of characters to read while searching. * Character we're trying to find.
* @return string * @param {number} count
* The data collected. If the delimiter was found, this string will * Max number of characters to read while searching.
* end with it. *
* @return {string}
* Collected data. If the delimiter was found, this string will
* end with it.
*/ */
function delimitedRead(stream, delimiter, count) { function delimitedRead(stream, delimiter, count) {
let scriptableStream; let scriptableStream;
@ -235,8 +243,7 @@ function delimitedRead(stream, delimiter, count) {
return data; return data;
} }
const StreamUtils = { this.StreamUtils = {
copyStream, copyStream,
delimitedRead delimitedRead,
}; };

View File

@ -128,7 +128,11 @@ add_test(function test_toJSON() {
equal(e1s.message, e1.message); equal(e1s.message, e1.message);
equal(e1s.stacktrace, e1.stack); equal(e1s.stacktrace, e1.stack);
let e2 = new JavaScriptError("first", "second", "third", "fourth"); let e2 = new JavaScriptError("first", {
fnName: "second",
file: "third",
line: "fourth",
});
let e2s = e2.toJSON(); let e2s = e2.toJSON();
equal(e2.status, e2s.error); equal(e2.status, e2s.error);
equal(e2.message, e2s.message); equal(e2.message, e2s.message);
@ -311,9 +315,9 @@ add_test(function test_JavaScriptError() {
equal("undefined", new JavaScriptError(undefined).message); equal("undefined", new JavaScriptError(undefined).message);
// TODO(ato): Bug 1240550 // TODO(ato): Bug 1240550
//equal("funcname @file", new JavaScriptError("message", "funcname", "file").stack); //equal("funcname @file", new JavaScriptError("message", {fnName: "funcname", file: "file"}).stack);
equal("funcname @file, line line", equal("funcname @file, line line",
new JavaScriptError("message", "funcname", "file", "line").stack); new JavaScriptError("message", {fnName: "funcname", file: "file", line: "line"}).stack);
// TODO(ato): More exhaustive tests for JS stack computation // TODO(ato): More exhaustive tests for JS stack computation

View File

@ -6,31 +6,36 @@
/* global Pipe, ScriptableInputStream, uneval */ /* global Pipe, ScriptableInputStream, uneval */
const {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; const {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu, results: Cr} =
Components;
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/EventEmitter.jsm"); Cu.import("resource://gre/modules/EventEmitter.jsm");
Cu.import("chrome://marionette/content/stream-utils.js"); const {StreamUtils} =
const { Packet, JSONPacket, BulkPacket } = Cu.import("chrome://marionette/content/stream-utils.js", {});
Cu.import("chrome://marionette/content/packets.js"); const {Packet, JSONPacket, BulkPacket} =
const defer = function () { Cu.import("chrome://marionette/content/packets.js", {});
const defer = function() {
let deferred = { let deferred = {
promise: new Promise((resolve, reject) => { promise: new Promise((resolve, reject) => {
deferred.resolve = resolve; deferred.resolve = resolve;
deferred.reject = reject; deferred.reject = reject;
}) }),
}; };
return deferred; return deferred;
}; };
const executeSoon = function (func) {
const executeSoon = function(func) {
Services.tm.dispatchToMainThread(func); Services.tm.dispatchToMainThread(func);
}; };
const flags = { wantVerbose: false, wantLogging: false };
const flags = {wantVerbose: false, wantLogging: false};
const dumpv = const dumpv =
flags.wantVerbose ? flags.wantVerbose ?
function (msg) {dump(msg + "\n");} : function(msg) { dump(msg + "\n"); } :
function () {}; function() {};
const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init"); const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init");
@ -42,23 +47,24 @@ this.EXPORTED_SYMBOLS = ["DebuggerTransport"];
const PACKET_HEADER_MAX = 200; const PACKET_HEADER_MAX = 200;
/** /**
* An adapter that handles data transfers between the debugger client and * An adapter that handles data transfers between the debugger client
* server. It can work with both nsIPipe and nsIServerSocket transports so * and server. It can work with both nsIPipe and nsIServerSocket
* long as the properly created input and output streams are specified. * transports so long as the properly created input and output streams
* (However, for intra-process connections, LocalDebuggerTransport, below, * are specified. (However, for intra-process connections,
* is more efficient than using an nsIPipe pair with DebuggerTransport.) * LocalDebuggerTransport, below, is more efficient than using an nsIPipe
* pair with DebuggerTransport.)
* *
* @param input nsIAsyncInputStream * @param {nsIAsyncInputStream} input
* The input stream. * The input stream.
* @param output nsIAsyncOutputStream * @param {nsIAsyncOutputStream} output
* The output stream. * The output stream.
* *
* Given a DebuggerTransport instance dt: * Given a DebuggerTransport instance dt:
* 1) Set dt.hooks to a packet handler object (described below). * 1) Set dt.hooks to a packet handler object (described below).
* 2) Call dt.ready() to begin watching for input packets. * 2) Call dt.ready() to begin watching for input packets.
* 3) Call dt.send() / dt.startBulkSend() to send packets. * 3) Call dt.send() / dt.startBulkSend() to send packets.
* 4) Call dt.close() to close the connection, and disengage from the event * 4) Call dt.close() to close the connection, and disengage from
* loop. * the event loop.
* *
* A packet handler is an object with the following methods: * A packet handler is an object with the following methods:
* *
@ -71,32 +77,34 @@ const PACKET_HEADER_MAX = 200;
* * actor: Name of actor that will receive the packet * * actor: Name of actor that will receive the packet
* * type: Name of actor's method that should be called on receipt * * type: Name of actor's method that should be called on receipt
* * length: Size of the data to be read * * length: Size of the data to be read
* * stream: This input stream should only be used directly if you can ensure * * stream: This input stream should only be used directly if you
* that you will read exactly |length| bytes and will not close the * can ensure that you will read exactly |length| bytes and
* stream when reading is complete * will not close the stream when reading is complete
* * done: If you use the stream directly (instead of |copyTo| below), you * * done: If you use the stream directly (instead of |copyTo|
* must signal completion by resolving / rejecting this deferred. * below), you must signal completion by resolving/rejecting
* If it's rejected, the transport will be closed. If an Error is * this deferred. If it's rejected, the transport will
* supplied as a rejection value, it will be logged via |dump|. * be closed. If an Error is supplied as a rejection value,
* If you do use |copyTo|, resolving is taken care of for you when * it will be logged via |dump|. If you do use |copyTo|,
* copying completes. * resolving is taken care of for you when copying completes.
* * copyTo: A helper function for getting your data out of the stream that * * copyTo: A helper function for getting your data out of the
* meets the stream handling requirements above, and has the * stream that meets the stream handling requirements above,
* following signature: * and has the following signature:
* @param output nsIAsyncOutputStream
* The stream to copy to.
* @return Promise
* The promise is resolved when copying completes or rejected if any
* (unexpected) errors occur.
* This object also emits "progress" events for each chunk that is
* copied. See stream-utils.js.
* *
* - onClosed(reason) - called when the connection is closed. |reason| is * @param nsIAsyncOutputStream {output}
* an optional nsresult or object, typically passed when the transport is * The stream to copy to.
* closed due to some error in a underlying stream.
* *
* See ./packets.js and the Remote Debugging Protocol specification for more * @return {Promise}
* details on the format of these packets. * The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur. This object
* also emits "progress" events for each chunk that is
* copied. See stream-utils.js.
*
* - onClosed(reason) - called when the connection is closed. |reason|
* is an optional nsresult or object, typically passed when the
* transport is closed due to some error in a underlying stream.
*
* See ./packets.js and the Remote Debugging Protocol specification for
* more details on the format of these packets.
*/ */
function DebuggerTransport(input, output) { function DebuggerTransport(input, output) {
EventEmitter.decorate(this); EventEmitter.decorate(this);
@ -105,8 +113,8 @@ function DebuggerTransport(input, output) {
this._scriptableInput = new ScriptableInputStream(input); this._scriptableInput = new ScriptableInputStream(input);
this._output = output; this._output = output;
// The current incoming (possibly partial) header, which will determine which // The current incoming (possibly partial) header, which will determine
// type of Packet |_incoming| below will become. // which type of Packet |_incoming| below will become.
this._incomingHeader = ""; this._incomingHeader = "";
// The current incoming Packet object // The current incoming Packet object
this._incoming = null; this._incoming = null;
@ -128,10 +136,10 @@ DebuggerTransport.prototype = {
* *
* This method returns immediately, without waiting for the entire * This method returns immediately, without waiting for the entire
* packet to be transmitted, registering event handlers as needed to * packet to be transmitted, registering event handlers as needed to
* transmit the entire packet. Packets are transmitted in the order * transmit the entire packet. Packets are transmitted in the order they
* they are passed to this method. * are passed to this method.
*/ */
send: function (object) { send(object) {
this.emit("send", object); this.emit("send", object);
let packet = new JSONPacket(this); let packet = new JSONPacket(this);
@ -143,45 +151,52 @@ DebuggerTransport.prototype = {
/** /**
* Transmit streaming data via a bulk packet. * Transmit streaming data via a bulk packet.
* *
* This method initiates the bulk send process by queuing up the header data. * This method initiates the bulk send process by queuing up the header
* The caller receives eventual access to a stream for writing. * data. The caller receives eventual access to a stream for writing.
* *
* N.B.: Do *not* attempt to close the stream handed to you, as it will * N.B.: Do *not* attempt to close the stream handed to you, as it
* continue to be used by this transport afterwards. Most users should * will continue to be used by this transport afterwards. Most users
* instead use the provided |copyFrom| function instead. * should instead use the provided |copyFrom| function instead.
* *
* @param header Object * @param {Object} header
* This is modeled after the format of JSON packets above, but does not * This is modeled after the format of JSON packets above, but does
* actually contain the data, but is instead just a routing header: * not actually contain the data, but is instead just a routing
* * actor: Name of actor that will receive the packet * header:
* * type: Name of actor's method that should be called on receipt *
* * length: Size of the data to be sent * - actor: Name of actor that will receive the packet
* @return Promise * - type: Name of actor's method that should be called on receipt
* The promise will be resolved when you are allowed to write to the * - length: Size of the data to be sent
* stream with an object containing: *
* * stream: This output stream should only be used directly if * @return {Promise}
* you can ensure that you will write exactly |length| * The promise will be resolved when you are allowed to write to
* bytes and will not close the stream when writing is * the stream with an object containing:
* complete *
* * done: If you use the stream directly (instead of |copyFrom| * - stream: This output stream should only be used directly
* below), you must signal completion by resolving / * if you can ensure that you will write exactly
* rejecting this deferred. If it's rejected, the * |length| bytes and will not close the stream when
* transport will be closed. If an Error is supplied as * writing is complete.
* a rejection value, it will be logged via |dump|. If * - done: If you use the stream directly (instead of
* you do use |copyFrom|, resolving is taken care of for * |copyFrom| below), you must signal completion by
* you when copying completes. * resolving/rejecting this deferred. If it's
* * copyFrom: A helper function for getting your data onto the * rejected, the transport will be closed. If an
* stream that meets the stream handling requirements * Error is supplied as a rejection value, it will
* above, and has the following signature: * be logged via |dump|. If you do use |copyFrom|,
* @param input nsIAsyncInputStream * resolving is taken care of for you when copying
* The stream to copy from. * completes.
* @return Promise * - copyFrom: A helper function for getting your data onto the
* The promise is resolved when copying completes or * stream that meets the stream handling requirements
* rejected if any (unexpected) errors occur. * above, and has the following signature:
* This object also emits "progress" events for each chunk *
* that is copied. See stream-utils.js. * @param {nsIAsyncInputStream} input
* The stream to copy from.
*
* @return {Promise}
* The promise is resolved when copying completes
* or rejected if any (unexpected) errors occur.
* This object also emits "progress" events for
* each chunkthat is copied. See stream-utils.js.
*/ */
startBulkSend: function (header) { startBulkSend(header) {
this.emit("startbulksend", header); this.emit("startbulksend", header);
let packet = new BulkPacket(this); let packet = new BulkPacket(this);
@ -193,11 +208,13 @@ DebuggerTransport.prototype = {
/** /**
* Close the transport. * Close the transport.
* @param reason nsresult / object (optional) *
* The status code or error message that corresponds to the reason for * @param {(nsresult|object)=} reason
* closing the transport (likely because a stream closed or failed). * The status code or error message that corresponds to the reason
* for closing the transport (likely because a stream closed
* or failed).
*/ */
close: function (reason) { close(reason) {
this.emit("close", reason); this.emit("close", reason);
this.active = false; this.active = false;
@ -225,10 +242,11 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Flush data to the outgoing stream. Waits until the output stream notifies * Flush data to the outgoing stream. Waits until the output
* us that it is ready to be written to (via onOutputStreamReady). * stream notifies us that it is ready to be written to (via
* onOutputStreamReady).
*/ */
_flushOutgoing: function () { _flushOutgoing() {
if (!this._outgoingEnabled || this._outgoing.length === 0) { if (!this._outgoingEnabled || this._outgoing.length === 0) {
return; return;
} }
@ -245,29 +263,29 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Pause this transport's attempts to write to the output stream. This is * Pause this transport's attempts to write to the output stream.
* used when we've temporarily handed off our output stream for writing bulk * This is used when we've temporarily handed off our output stream for
* data. * writing bulk data.
*/ */
pauseOutgoing: function () { pauseOutgoing() {
this._outgoingEnabled = false; this._outgoingEnabled = false;
}, },
/** /**
* Resume this transport's attempts to write to the output stream. * Resume this transport's attempts to write to the output stream.
*/ */
resumeOutgoing: function () { resumeOutgoing() {
this._outgoingEnabled = true; this._outgoingEnabled = true;
this._flushOutgoing(); this._flushOutgoing();
}, },
// nsIOutputStreamCallback // nsIOutputStreamCallback
/** /**
* This is called when the output stream is ready for more data to be written. * This is called when the output stream is ready for more data to
* The current outgoing packet will attempt to write some amount of data, but * be written. The current outgoing packet will attempt to write some
* may not complete. * amount of data, but may not complete.
*/ */
onOutputStreamReady: function (stream) { onOutputStreamReady(stream) {
if (!this._outgoingEnabled || this._outgoing.length === 0) { if (!this._outgoingEnabled || this._outgoing.length === 0) {
return; return;
} }
@ -288,7 +306,7 @@ DebuggerTransport.prototype = {
/** /**
* Remove the current outgoing packet from the queue upon completion. * Remove the current outgoing packet from the queue upon completion.
*/ */
_finishCurrentOutgoing: function () { _finishCurrentOutgoing() {
if (this._currentOutgoing) { if (this._currentOutgoing) {
this._currentOutgoing.destroy(); this._currentOutgoing.destroy();
this._outgoing.shift(); this._outgoing.shift();
@ -298,7 +316,7 @@ DebuggerTransport.prototype = {
/** /**
* Clear the entire outgoing queue. * Clear the entire outgoing queue.
*/ */
_destroyAllOutgoing: function () { _destroyAllOutgoing() {
for (let packet of this._outgoing) { for (let packet of this._outgoing) {
packet.destroy(); packet.destroy();
} }
@ -306,11 +324,11 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Initialize the input stream for reading. Once this method has been called, * Initialize the input stream for reading. Once this method has been
* we watch for packets on the input stream, and pass them to the appropriate * called, we watch for packets on the input stream, and pass them to
* handlers via this.hooks. * the appropriate handlers via this.hooks.
*/ */
ready: function () { ready() {
this.active = true; this.active = true;
this._waitForIncoming(); this._waitForIncoming();
}, },
@ -319,7 +337,7 @@ DebuggerTransport.prototype = {
* Asks the input stream to notify us (via onInputStreamReady) when it is * Asks the input stream to notify us (via onInputStreamReady) when it is
* ready for reading. * ready for reading.
*/ */
_waitForIncoming: function () { _waitForIncoming() {
if (this._incomingEnabled) { if (this._incomingEnabled) {
let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
this._input.asyncWait(this, 0, 0, threadManager.currentThread); this._input.asyncWait(this, 0, 0, threadManager.currentThread);
@ -327,18 +345,18 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Pause this transport's attempts to read from the input stream. This is * Pause this transport's attempts to read from the input stream.
* used when we've temporarily handed off our input stream for reading bulk * This is used when we've temporarily handed off our input stream for
* data. * reading bulk data.
*/ */
pauseIncoming: function () { pauseIncoming() {
this._incomingEnabled = false; this._incomingEnabled = false;
}, },
/** /**
* Resume this transport's attempts to read from the input stream. * Resume this transport's attempts to read from the input stream.
*/ */
resumeIncoming: function () { resumeIncoming() {
this._incomingEnabled = true; this._incomingEnabled = true;
this._flushIncoming(); this._flushIncoming();
this._waitForIncoming(); this._waitForIncoming();
@ -348,7 +366,7 @@ DebuggerTransport.prototype = {
/** /**
* Called when the stream is either readable or closed. * Called when the stream is either readable or closed.
*/ */
onInputStreamReady: function (stream) { onInputStreamReady(stream) {
try { try {
while (stream.available() && this._incomingEnabled && while (stream.available() && this._incomingEnabled &&
this._processIncoming(stream, stream.available())) { this._processIncoming(stream, stream.available())) {
@ -365,16 +383,17 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Process the incoming data. Will create a new currently incoming Packet if * Process the incoming data. Will create a new currently incoming
* needed. Tells the incoming Packet to read as much data as it can, but * Packet if needed. Tells the incoming Packet to read as much data
* reading may not complete. The Packet signals that its data is ready for * as it can, but reading may not complete. The Packet signals that
* delivery by calling one of this transport's _on*Ready methods (see * its data is ready for delivery by calling one of this transport's
* ./packets.js and the _on*Ready methods below). * _on*Ready methods (see ./packets.js and the _on*Ready methods below).
* @return boolean *
* Whether incoming stream processing should continue for any * @return {boolean}
* remaining data. * Whether incoming stream processing should continue for any
* remaining data.
*/ */
_processIncoming: function (stream, count) { _processIncoming(stream, count) {
dumpv("Data available: " + count); dumpv("Data available: " + count);
if (!count) { if (!count) {
@ -406,8 +425,7 @@ DebuggerTransport.prototype = {
this._incoming.read(stream, this._scriptableInput); this._incoming.read(stream, this._scriptableInput);
} }
} catch (e) { } catch (e) {
let msg = "Error reading incoming packet: (" + e + " - " + e.stack + ")"; dump(`Error reading incoming packet: (${e} - ${e.stack})\n`);
dump(msg + "\n");
// Now in an invalid state, shut down the transport. // Now in an invalid state, shut down the transport.
this.close(); this.close();
@ -426,13 +444,14 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Read as far as we can into the incoming data, attempting to build up a * Read as far as we can into the incoming data, attempting to build
* complete packet header (which terminates with ":"). We'll only read up to * up a complete packet header (which terminates with ":"). We'll only
* PACKET_HEADER_MAX characters. * read up to PACKET_HEADER_MAX characters.
* @return boolean *
* True if we now have a complete header. * @return {boolean}
* True if we now have a complete header.
*/ */
_readHeader: function () { _readHeader() {
let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length; let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length;
this._incomingHeader += this._incomingHeader +=
StreamUtils.delimitedRead(this._scriptableInput, ":", amountToRead); StreamUtils.delimitedRead(this._scriptableInput, ":", amountToRead);
@ -458,7 +477,7 @@ DebuggerTransport.prototype = {
/** /**
* If the incoming packet is done, log it as needed and clear the buffer. * If the incoming packet is done, log it as needed and clear the buffer.
*/ */
_flushIncoming: function () { _flushIncoming() {
if (!this._incoming.done) { if (!this._incoming.done) {
return; return;
} }
@ -469,10 +488,10 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Handler triggered by an incoming JSONPacket completing it's |read| method. * Handler triggered by an incoming JSONPacket completing it's |read|
* Delivers the packet to this.hooks.onPacket. * method. Delivers the packet to this.hooks.onPacket.
*/ */
_onJSONObjectReady: function (object) { _onJSONObjectReady(object) {
executeSoon(() => { executeSoon(() => {
// Ensure the transport is still alive by the time this runs. // Ensure the transport is still alive by the time this runs.
if (this.active) { if (this.active) {
@ -483,12 +502,12 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Handler triggered by an incoming BulkPacket entering the |read| phase for * Handler triggered by an incoming BulkPacket entering the |read|
* the stream portion of the packet. Delivers info about the incoming * phase for the stream portion of the packet. Delivers info about the
* streaming data to this.hooks.onBulkPacket. See the main comment on the * incoming streaming data to this.hooks.onBulkPacket. See the main
* transport at the top of this file for more details. * comment on the transport at the top of this file for more details.
*/ */
_onBulkReadReady: function (...args) { _onBulkReadReady(...args) {
executeSoon(() => { executeSoon(() => {
// Ensure the transport is still alive by the time this runs. // Ensure the transport is still alive by the time this runs.
if (this.active) { if (this.active) {
@ -499,29 +518,30 @@ DebuggerTransport.prototype = {
}, },
/** /**
* Remove all handlers and references related to the current incoming packet, * Remove all handlers and references related to the current incoming
* either because it is now complete or because the transport is closing. * packet, either because it is now complete or because the transport
* is closing.
*/ */
_destroyIncoming: function () { _destroyIncoming() {
if (this._incoming) { if (this._incoming) {
this._incoming.destroy(); this._incoming.destroy();
} }
this._incomingHeader = ""; this._incomingHeader = "";
this._incoming = null; this._incoming = null;
} },
}; };
/** /**
* An adapter that handles data transfers between the debugger client and * An adapter that handles data transfers between the debugger client
* server when they both run in the same process. It presents the same API as * and server when they both run in the same process. It presents the
* DebuggerTransport, but instead of transmitting serialized messages across a * same API as DebuggerTransport, but instead of transmitting serialized
* connection it merely calls the packet dispatcher of the other side. * messages across a connection it merely calls the packet dispatcher of
* the other side.
* *
* @param other LocalDebuggerTransport * @param {LocalDebuggerTransport} other
* The other endpoint for this debugger connection. * The other endpoint for this debugger connection.
* *
* @see DebuggerTransport * @see {DebuggerTransport}
*/ */
function LocalDebuggerTransport(other) { function LocalDebuggerTransport(other) {
EventEmitter.decorate(this); EventEmitter.decorate(this);
@ -529,9 +549,10 @@ function LocalDebuggerTransport(other) {
this.other = other; this.other = other;
this.hooks = null; this.hooks = null;
// A packet number, shared between this and this.other. This isn't used by the // A packet number, shared between this and this.other. This isn't
// protocol at all, but it makes the packet traces a lot easier to follow. // used by the protocol at all, but it makes the packet traces a lot
this._serial = this.other ? this.other._serial : { count: 0 }; // easier to follow.
this._serial = this.other ? this.other._serial : {count: 0};
this.close = this.close.bind(this); this.close = this.close.bind(this);
} }
@ -540,7 +561,7 @@ LocalDebuggerTransport.prototype = {
* Transmit a message by directly calling the onPacket handler of the other * Transmit a message by directly calling the onPacket handler of the other
* endpoint. * endpoint.
*/ */
send: function (packet) { send(packet) {
this.emit("send", packet); this.emit("send", packet);
let serial = this._serial.count++; let serial = this._serial.count++;
@ -558,7 +579,8 @@ LocalDebuggerTransport.prototype = {
executeSoon(() => { executeSoon(() => {
// Avoid the cost of JSON.stringify() when logging is disabled. // Avoid the cost of JSON.stringify() when logging is disabled.
if (flags.wantLogging) { if (flags.wantLogging) {
dumpv("Received packet " + serial + ": " + JSON.stringify(packet, null, 2)); dumpv(`Received packet ${serial}: ` +
JSON.stringify(packet, null, 2));
} }
if (other.hooks) { if (other.hooks) {
other.emit("packet", packet); other.emit("packet", packet);
@ -569,15 +591,15 @@ LocalDebuggerTransport.prototype = {
}, },
/** /**
* Send a streaming bulk packet directly to the onBulkPacket handler of the * Send a streaming bulk packet directly to the onBulkPacket handler
* other endpoint. * of the other endpoint.
* *
* This case is much simpler than the full DebuggerTransport, since there is * This case is much simpler than the full DebuggerTransport, since
* no primary stream we have to worry about managing while we hand it off to * there is no primary stream we have to worry about managing while
* others temporarily. Instead, we can just make a single use pipe and be * we hand it off to others temporarily. Instead, we can just make a
* done with it. * single use pipe and be done with it.
*/ */
startBulkSend: function ({actor, type, length}) { startBulkSend({actor, type, length}) {
this.emit("startbulksend", {actor, type, length}); this.emit("startbulksend", {actor, type, length});
let serial = this._serial.count++; let serial = this._serial.count++;
@ -599,9 +621,9 @@ LocalDebuggerTransport.prototype = {
// Receiver // Receiver
let deferred = defer(); let deferred = defer();
let packet = { let packet = {
actor: actor, actor,
type: type, type,
length: length, length,
copyTo: (output) => { copyTo: (output) => {
let copying = let copying =
StreamUtils.copyStream(pipe.inputStream, output, length); StreamUtils.copyStream(pipe.inputStream, output, length);
@ -609,7 +631,7 @@ LocalDebuggerTransport.prototype = {
return copying; return copying;
}, },
stream: pipe.inputStream, stream: pipe.inputStream,
done: deferred done: deferred,
}; };
this.other.emit("bulkpacket", packet); this.other.emit("bulkpacket", packet);
@ -622,8 +644,8 @@ LocalDebuggerTransport.prototype = {
// Sender // Sender
let sendDeferred = defer(); let sendDeferred = defer();
// The remote transport is not capable of resolving immediately here, so we // The remote transport is not capable of resolving immediately here,
// shouldn't be able to either. // so we shouldn't be able to either.
executeSoon(() => { executeSoon(() => {
let copyDeferred = defer(); let copyDeferred = defer();
@ -635,7 +657,7 @@ LocalDebuggerTransport.prototype = {
return copying; return copying;
}, },
stream: pipe.outputStream, stream: pipe.outputStream,
done: copyDeferred done: copyDeferred,
}); });
// Await the result of writing to the stream // Await the result of writing to the stream
@ -648,7 +670,7 @@ LocalDebuggerTransport.prototype = {
/** /**
* Close the transport. * Close the transport.
*/ */
close: function () { close() {
this.emit("close"); this.emit("close");
if (this.other) { if (this.other) {
@ -671,24 +693,24 @@ LocalDebuggerTransport.prototype = {
/** /**
* An empty method for emulating the DebuggerTransport API. * An empty method for emulating the DebuggerTransport API.
*/ */
ready: function () {}, ready() {},
/** /**
* Helper function that makes an object fully immutable. * Helper function that makes an object fully immutable.
*/ */
_deepFreeze: function (object) { _deepFreeze(object) {
Object.freeze(object); Object.freeze(object);
for (let prop in object) { for (let prop in object) {
// Freeze the properties that are objects, not on the prototype, and not // Freeze the properties that are objects, not on the prototype,
// already frozen. Note that this might leave an unfrozen reference // and not already frozen. Note that this might leave an unfrozen
// somewhere in the object if there is an already frozen object containing // reference somewhere in the object if there is an already frozen
// an unfrozen object. // object containing an unfrozen object.
if (object.hasOwnProperty(prop) && typeof object === "object" && if (object.hasOwnProperty(prop) && typeof object === "object" &&
!Object.isFrozen(object)) { !Object.isFrozen(object)) {
this._deepFreeze(object[prop]); this._deepFreeze(object[prop]);
} }
} }
} },
}; };
/** /**
@ -733,28 +755,29 @@ ChildDebuggerTransport.prototype = {
if (e.result != Cr.NS_ERROR_NULL_POINTER) { if (e.result != Cr.NS_ERROR_NULL_POINTER) {
throw e; throw e;
} }
// In some cases, especially when using messageManagers in non-e10s mode, we reach // In some cases, especially when using messageManagers in non-e10s
// this point with a dead messageManager which only throws errors but does not // mode, we reach this point with a dead messageManager which only
// seem to indicate in any other way that it is dead. // throws errors but does not seem to indicate in any other way that
// it is dead.
} }
}, },
ready: function () { ready() {
this._addListener(); this._addListener();
}, },
close: function () { close() {
this._removeListener(); this._removeListener();
this.emit("close"); this.emit("close");
this.hooks.onClosed(); this.hooks.onClosed();
}, },
receiveMessage: function ({data}) { receiveMessage({data}) {
this.emit("packet", data); this.emit("packet", data);
this.hooks.onPacket(data); this.hooks.onPacket(data);
}, },
send: function (packet) { send(packet) {
this.emit("send", packet); this.emit("send", packet);
try { try {
this._mm.sendAsyncMessage(this._messageName, packet); this._mm.sendAsyncMessage(this._messageName, packet);
@ -762,13 +785,14 @@ ChildDebuggerTransport.prototype = {
if (e.result != Cr.NS_ERROR_NULL_POINTER) { if (e.result != Cr.NS_ERROR_NULL_POINTER) {
throw e; throw e;
} }
// In some cases, especially when using messageManagers in non-e10s mode, we reach // In some cases, especially when using messageManagers in non-e10s
// this point with a dead messageManager which only throws errors but does not // mode, we reach this point with a dead messageManager which only
// seem to indicate in any other way that it is dead. // throws errors but does not seem to indicate in any other way that
// it is dead.
} }
}, },
startBulkSend: function () { startBulkSend() {
throw new Error("Can't send bulk data to child processes."); throw new Error("Can't send bulk data to child processes.");
}, },
@ -791,7 +815,7 @@ ChildDebuggerTransport.prototype = {
if (!this.isWorker) { if (!this.isWorker) {
// Main thread // Main thread
(function () { (function() {
/** /**
* A transport that uses a WorkerDebugger to send packets from the main * A transport that uses a WorkerDebugger to send packets from the main
* thread to a worker thread. * thread to a worker thread.
@ -805,30 +829,30 @@ if (!this.isWorker) {
WorkerDebuggerTransport.prototype = { WorkerDebuggerTransport.prototype = {
constructor: WorkerDebuggerTransport, constructor: WorkerDebuggerTransport,
ready: function () { ready() {
this._dbg.addListener(this); this._dbg.addListener(this);
}, },
close: function () { close() {
this._dbg.removeListener(this); this._dbg.removeListener(this);
if (this.hooks) { if (this.hooks) {
this.hooks.onClosed(); this.hooks.onClosed();
} }
}, },
send: function (packet) { send(packet) {
this._dbg.postMessage(JSON.stringify({ this._dbg.postMessage(JSON.stringify({
type: "message", type: "message",
id: this._id, id: this._id,
message: packet message: packet,
})); }));
}, },
startBulkSend: function () { startBulkSend() {
throw new Error("Can't send bulk data from worker threads!"); throw new Error("Can't send bulk data from worker threads!");
}, },
_onMessage: function (message) { _onMessage(message) {
let packet = JSON.parse(message); let packet = JSON.parse(message);
if (packet.type !== "message" || packet.id !== this._id) { if (packet.type !== "message" || packet.id !== this._id) {
return; return;
@ -837,16 +861,16 @@ if (!this.isWorker) {
if (this.hooks) { if (this.hooks) {
this.hooks.onPacket(packet.message); this.hooks.onPacket(packet.message);
} }
} },
}; };
}).call(this); }).call(this);
} else { } else {
// Worker thread // Worker thread
(function () { (function() {
/** /**
* A transport that uses a WorkerDebuggerGlobalScope to send packets from a * A transport that uses a WorkerDebuggerGlobalScope to send packets
* worker thread to the main thread. * from a worker thread to the main thread.
*/ */
function WorkerDebuggerTransport(scope, id) { function WorkerDebuggerTransport(scope, id) {
this._scope = scope; this._scope = scope;
@ -857,30 +881,30 @@ if (!this.isWorker) {
WorkerDebuggerTransport.prototype = { WorkerDebuggerTransport.prototype = {
constructor: WorkerDebuggerTransport, constructor: WorkerDebuggerTransport,
ready: function () { ready() {
this._scope.addEventListener("message", this._onMessage); this._scope.addEventListener("message", this._onMessage);
}, },
close: function () { close() {
this._scope.removeEventListener("message", this._onMessage); this._scope.removeEventListener("message", this._onMessage);
if (this.hooks) { if (this.hooks) {
this.hooks.onClosed(); this.hooks.onClosed();
} }
}, },
send: function (packet) { send(packet) {
this._scope.postMessage(JSON.stringify({ this._scope.postMessage(JSON.stringify({
type: "message", type: "message",
id: this._id, id: this._id,
message: packet message: packet,
})); }));
}, },
startBulkSend: function () { startBulkSend() {
throw new Error("Can't send bulk data from worker threads!"); throw new Error("Can't send bulk data from worker threads!");
}, },
_onMessage: function (event) { _onMessage(event) {
let packet = JSON.parse(event.data); let packet = JSON.parse(event.data);
if (packet.type !== "message" || packet.id !== this._id) { if (packet.type !== "message" || packet.id !== this._id) {
return; return;
@ -889,7 +913,7 @@ if (!this.isWorker) {
if (this.hooks) { if (this.hooks) {
this.hooks.onPacket(packet.message); this.hooks.onPacket(packet.message);
} }
} },
}; };
}).call(this); }).call(this);

View File

@ -59,7 +59,7 @@ this.wait = {};
* @throws {?} * @throws {?}
* If |func| throws, its error is propagated. * If |func| throws, its error is propagated.
*/ */
wait.until = function (func, timeout = 2000, interval = 10) { wait.until = function(func, timeout = 2000, interval = 10) {
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -72,7 +72,8 @@ wait.until = function (func, timeout = 2000, interval = 10) {
throw rejected; throw rejected;
} }
// return if timeout is 0, allowing |func| to be evaluated at least once // return if timeout is 0, allowing |func| to be evaluated at
// least once
if (start == end || new Date().getTime() >= end) { if (start == end || new Date().getTime() >= end) {
resolve(rejected); resolve(rejected);
} }