VulpesOS/gaia/tests/jsmarionette/plugins/marionette-helper/index.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

358 lines
11 KiB
JavaScript
Executable file

'use strict';
var Marionette = require('marionette-client');
/**
* @param {Marionette.Client} client Marionette client to use.
* @constructor
*/
function MarionetteHelper(client) {
this.client = client;
}
module.exports = MarionetteHelper;
/**
* DOM id for window.alert() and window.confirm() message container.
* @type {string}
*/
MarionetteHelper.ALERT_ID = '#modal-dialog-confirm-message';
/**
* Make a new helper.
* @param {Marionette.Client} client Marionette client to use.
* @param {Object} options Optional map of attributes for Apps.
* @return {Apps} instance.
*/
MarionetteHelper.setup = function(client, options) {
return new MarionetteHelper(client);
};
/**
* @const {number}
*/
MarionetteHelper.DEFAULT_TEST_INTERVAL = 100;
/**
* @const {number}
*/
MarionetteHelper.DEFAULT_TEST_TIMEOUT = 5000;
MarionetteHelper.prototype = {
/**
* @type {Marionette.Client}
*/
client: null,
/**
* Wait for some amount of time by blocking via a marionette call.
* Only ever use this for debugging!
* @param {number} millis number of seconds to sleep.
*/
wait: function(millis) {
// Add a small value to the scriptTimeout used for this
// `executeAsyncScript` invocation in order to account for the lack of
// millisecond precision in `setTimeout`.
var schedulerTolerance = 1000;
// Ensure that the asynchronous script will not raise a timeout error, even
// when the requested duration exceeds the client's current `scriptTimeout`
// value.
var scope = this.client.scope({
scriptTimeout: millis + schedulerTolerance
});
scope.executeAsyncScript(function(millis) {
setTimeout(marionetteScriptFinished, millis);
}, [millis]);
},
/**
* ~*~*~*~*~ o_O Deprecated o_O ~*~*~*~*~
* Use client#waitFor instead!
*
* @param {Function} test some function that returns a boolean.
* @param {Function} opt_callback optional function to invoke when test
* passes or times out.
* @param {Object} opt_context Optional context object for test block.
* @param {number} opt_interval Optional test frequency in millis.
* @param {number} opt_timeout Optional test timeout in millis.
*/
waitFor: function(test, opt_callback, opt_context, opt_interval,
opt_timeout) {
this.client.waitFor(test, {
interval: opt_interval,
timeout: opt_timeout
}, opt_callback);
},
/**
* Wait until a window.alert() or window.confirm() message comes up.
*
* @param {string|RegExp} alert to look for. If passed a string, we will
* check whether the string is a substring of the alert. If passed a
* RegExp, we will check whether it matches the alert.
* @param {Function} opt_callback optinal function to invoke when alert
* becomes visible.
* @param {number} opt_timeout Optional test timeout in millis.
*/
waitForAlert: function(alert, opt_callback, opt_timeout) {
if (typeof opt_callback === 'number') {
opt_timeout = opt_callback;
opt_callback = null;
}
if (typeof alert === 'string') {
// Convert the alert to a RegExp.
alert = new RegExp('.*' + alert + '.*');
}
// TODO(gaye): Perhaps we should save the iframe we're in?
this.client.switchToFrame();
this.client.waitFor(function() {
// TODO(gaye): Update this to do a less brittle check once we have
// marionette server support.
var msg = this.client
.findElement(MarionetteHelper.ALERT_ID)
.text();
return alert.test(msg);
}.bind(this), {
timeout: opt_timeout
}, opt_callback);
},
/**
* Wait for an element to be added to the DOM and displayed
* @param {Marionette.Element|string} el element or some css selector.
* @param {Object} [options] options object to be passed to client.waitFor.
* @return {Marionette.Element} Element we find with css selector.
*/
waitForElement: function(el, options) {
var client = this.client;
if (!isElement(el)) {
el = client.findElement(el);
}
client.waitFor(function() {
try {
return el.displayed.call(el);
} catch (err) {
if (err && err.type === 'ElementNotAccessibleError') {
// the element is not yet accessible
return false;
}
// the client threw an unexpected error, rethrow it
throw err;
}
}, options);
return el;
},
/**
* Wait for a child element of some parent to be added to the DOM
* and displayed.
*
* @param {Marionette.Element|string} parent element or css selector.
* @param {Marionette.Element|string} el element or css selector.
* @return {Marionette.Element} Element we find with css selector.
*/
waitForChild: function(parent, el) {
parent = this.waitForElement(parent);
if (!isElement(el)) {
el = parent.findElement(el);
}
return this.waitForElement(el);
},
/**
* Wait for an element either hidden or removed from the dom
* @param {Marionette.Element|string} el element or some css selector.
* @param {Object} [options] options object to be passed to client.waitFor.
*/
waitForElementToDisappear: function(el, options) {
if (!isElement(el)) {
try {
el = this.client.findElement(el);
} catch (err) {
if (err && err.type === 'NoSuchElement') {
// if the element already can't be found, we are done
return;
}
// something in the element search went horribly wrong
// so rethrow the error instead of just returning false
throw err;
}
}
this.client.waitFor(function() {
try {
return !el.displayed();
} catch (err) {
if (err && err.type === 'ElementNotAccessibleError') {
// element is still accessible to Accessibility API
return false;
}
if (err && err.type === 'StaleElementReference') {
// the element was removed from the dom, we are done
return true;
}
// the client threw an unexpected error, rethrow it
throw err;
}
}, options);
},
/**
* Tap input element and fill the value.
* To do: Support the time, date, datetime, and datetime-local input
* correctly.
* Please refer to http://bugzil.la/976453.
*
* @param {Marionette.Element|String} el marionette element object or
* css selector.
* @param {String|Object} input text or datetime object for the input.
*/
fillInputField: function(el, input) {
if (!isElement(el)) {
el = this.client.findElement(el);
}
var inputText;
if (input instanceof Date) {
// XXX: bug 978685
// We cannot get type through element.getAttribute('type') if the type is
// datetime or datetime-local, so we use below workaround.
var eleType = this.client.executeScript(function(el) {
return el.getAttribute('type');
}, [el]);
switch (eleType) {
case 'date':
inputText = input.getFullYear() + '-' +
addLeadZero(input.getMonth()) +
'-' + addLeadZero(input.getDate());
break;
case 'time':
inputText = addLeadZero(input.getHours()) + ':' +
addLeadZero(input.getMinutes());
break;
case 'datetime':
inputText = input.toISOString();
break;
case 'datetime-local':
inputText = input.getFullYear() + '-' +
addLeadZero(input.getMonth()) +
'-' + addLeadZero(input.getDate()) + 'T' +
addLeadZero(input.getHours()) + ':' +
addLeadZero(input.getMinutes()) + ':' +
addLeadZero(input.getSeconds()) + '.' +
input.getMilliseconds();
break;
default:
throw new Error('The ' + eleType + ' type element is not supported' +
' when the input param is a Date object.');
}
} else {
inputText = input;
}
// Below script is to trigger input event on the input correctly and fill
// the data.
this.client.executeScript(function(el, value) {
el.value = value;
var evt = new Event('input', {
'view': window,
'bubbles': true,
'cancelable': true
});
el.dispatchEvent(evt);
}, [el, inputText]);
// We add lead zero on single digit. ex: 1 -> 01, 9 -> 09.
function addLeadZero(num) {
return num >= 10 ? num : ('0' + num);
}
},
/**
* Select specific option from target select element.
*
* @param {Marionette.Element|String} el element or some css selector.
* @param {String} optionText text for the option.
*/
tapSelectOption: function(el, optionText) {
var selectedOption = null;
if (!isElement(el)) {
el = this.client.findElement(el);
}
el.findElements('option').some(function(optionEl) {
if (optionEl.text() === optionText) {
selectedOption = optionEl;
return true;
}
return false;
});
selectedOption.scriptWith(function(selectedOptionEl) {
selectedOptionEl.selected = true;
});
el.scriptWith(function(selectEl) {
var evt = new Event('change', {
'view': window,
'bubbles': true,
'cancelable': true
});
selectEl.dispatchEvent(evt);
});
},
/**
* Get the first element that matches the selector by testing the element
* itself and traversing up through its ancestors in the DOM tree.
*
* @param {Marionette.Element|String} el element or some css selector where
* the search should start.
* @param {String} selector A string containing a selector expression to
* match elements against.
* @return {Marionette.Element|undefined} Element found by selector
*/
closest: function(el, selector) {
if (!isElement(el)) {
el = this.client.findElement(el);
}
var result = this.client.executeScript(function(el, selector) {
// HTMLDocument doesn't have mozMatchesSelector and can't be matched
while (el && el !== document.documentElement) {
// XXX: firefox 30 still needs prefix
if (el.mozMatchesSelector(selector)) {
return el;
}
el = el.parentNode;
}
}, [el, selector]);
// executeScript returns "null" by default
return result || undefined;
}
};
/**
* @param {Object} maybeElement something that could or could not be an el.
* @return {boolean} Whether or not we have an element.
* @private
*/
function isElement(maybeElement) {
return maybeElement && (maybeElement instanceof Marionette.Element ||
(!!maybeElement.id && !!maybeElement.client));
}