Publish Vulpes Preview 15 sources for desktop, Android and Tundra

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

View file

@ -0,0 +1,358 @@
'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));
}

View file

@ -0,0 +1,13 @@
{
"name": "marionette-helper",
"version": "0.3.3",
"author": {
"name": "The Gaia Team",
"email": "dev-gaia@lists.mozilla.org"
},
"dependencies": {
"marionette-apps": "file:../marionette-apps"
}
}

View file

@ -0,0 +1,64 @@
'use strict';
var assert = require('assert');
suite('MarionetteHelper.closest', function() {
// Require needed files.
var FakeApp = require('./lib/fake_app');
marionette.plugin('apps', require('marionette-apps'));
marionette.plugin('helper', require('../index'));
var helper;
var fakeApp;
var FAKE_APP_ORIGIN = 'fakeapp.gaiamobile.org';
var apps = {};
apps[FAKE_APP_ORIGIN] = __dirname + '/fakeapp';
var testTime;
var client = marionette.client({
profile: {
settings: {
'ftu.manifestURL': null,
'lockscreen.enabled': false
},
apps: apps
}
});
setup(function() {
helper = client.helper;
fakeApp = new FakeApp(client, 'app://' + FAKE_APP_ORIGIN);
fakeApp.launch();
testTime = new Date();
});
test('should return closest element that matches the selector', function() {
var link = client.findElement('a.parent');
var child = client.findElement('.child');
assert.equal(helper.closest(child, '.parent').id, link.id);
});
test('should loop until it finds "body"', function() {
var body = client.findElement('body');
var child = client.findElement('.child');
assert.equal(helper.closest(child, 'body').id, body.id);
});
test('should return "undefined" if it can\'t find it', function() {
var child = client.findElement('.child');
assert.equal(helper.closest(child, '#bogus'), undefined);
});
test('should allow selector as first argument', function() {
var link = client.findElement('a.parent');
assert.equal(helper.closest('.child', '.parent').id, link.id);
});
test('should return element itself if it matches selector', function() {
var link = client.findElement('a.parent');
assert.equal(helper.closest(link, '.parent').id, link.id);
});
});

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fake App</title>
<style>
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<select id="select">
<option value="option1">option1</option>
<option value="option2">option2</option>
<option value="option3">option3</option>
</select>
<input id="input">
<input id="input-date" type="date">
<input id="input-time" type="time">
<input id="input-datetime" type="datetime">
<input id="input-datetime-local" type="datetime-local">
<div class="parent">
<a href="#foo" class="parent">
<span class="child">Lorem</span> Ipsum
</a>
</div>
</body>
</html>

View file

@ -0,0 +1,10 @@
{
"name": "Fake app",
"description": "A fake app for testing.",
"type": "certified",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
}
}

View file

@ -0,0 +1,249 @@
'use strict';
var assert = require('assert');
marionette('MarionetteHelper', function() {
var subject;
marionette.plugin('helper', require('../index'));
marionette.plugin('apps', require('marionette-apps'));
var client = marionette.client({
profile: {
settings: {
'ftu.manifestURL': null,
'lockscreen.enabled': false
},
}
});
setup(function() {
subject = client.helper;
client.setSearchTimeout(2000);
client.executeScript(function() {
// start with a clean slate
document.body.innerHTML = '';
});
});
suite('#wait', function() {
test('basic operation', function() {
var before = new Date().getTime();
subject.wait(1000);
var after = new Date().getTime();
assert.ok(after - before >= 1000);
});
test('durations longer than current `scriptTimeout`', function() {
client.setScriptTimeout(23);
subject.wait(300);
assert.equal(client.scriptTimeout, 23);
});
});
test('#waitFor', function(done) {
var i = 0;
subject.waitFor(function() {
i++;
return i > 4;
}, function() {
assert.strictEqual(i, 5);
done();
}, 50, 200);
});
test('#waitForElement, element added to DOM', function() {
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
setTimeout(function() {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
document.body.appendChild(el);
}, 500);
}, [myRandomID]);
subject.waitForElement('#' + myRandomID);
});
test('#waitForElement, element displayed with CSS', function() {
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
el.style.display = 'none';
document.body.appendChild(el);
setTimeout(function() {
el.style.display = 'block';
}, 500);
}, [myRandomID]);
subject.waitForElement('#' + myRandomID);
});
test('#waitForElement with custom timeout', function(done) {
this.timeout(10000); // make this fail fast.
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
setTimeout(function() {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
document.body.appendChild(el);
}, 500);
}, [myRandomID]);
// Ensure the waitFor times out.
client.onScriptTimeout = function() {
assert(true, 'this should timeout');
done();
};
try {
subject.waitForElementToDisappear('#' + myRandomID, { timeout: 10 });
assert(false, 'should timeout before disappearing');
} catch (e) {}
});
test('#waitForChild, element added to DOM', function() {
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
var parent = document.createElement('div');
parent.id = 'parent';
document.body.appendChild(parent);
setTimeout(function() {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
parent.appendChild(el);
}, 500);
}, [myRandomID]);
var parent = client.findElement('#parent');
subject.waitForChild(parent, '#' + myRandomID);
});
test('#waitForChild, element displayed with CSS', function() {
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
var parent = document.createElement('div');
parent.id = 'parent';
document.body.appendChild(parent);
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
el.style.display = 'none';
parent.appendChild(el);
setTimeout(function() {
el.style.display = 'block';
}, 500);
}, [myRandomID]);
var parent = client.findElement('#parent');
subject.waitForChild(parent, '#' + myRandomID);
});
test('#waitForElementToDisappear DOM with an Element', function() {
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
document.body.appendChild(el);
setTimeout(function() {
document.body.removeChild(el);
}, 500);
}, [myRandomID]);
var el = client.findElement('#' + myRandomID);
subject.waitForElementToDisappear(el);
});
test('#waitForElementToDisappear DOM with a string', function() {
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
document.body.appendChild(el);
setTimeout(function() {
document.body.removeChild(el);
}, 500);
}, [myRandomID]);
subject.waitForElementToDisappear('#' + myRandomID);
});
test('#waitForElementToDisappear CSS with an Element', function() {
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
document.body.appendChild(el);
setTimeout(function() {
el.style.display = 'none';
}, 500);
}, [myRandomID]);
var el = client.findElement('#' + myRandomID);
subject.waitForElementToDisappear(el);
});
test('#waitForElementToDisappear DOM with a string', function() {
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
document.body.appendChild(el);
setTimeout(function() {
el.style.display = 'none';
}, 500);
}, [myRandomID]);
subject.waitForElementToDisappear('#' + myRandomID);
});
test('#waitForElementToDisappear with custom timeout', function(done) {
this.timeout(10000); // make this fail fast.
var myRandomID = 'YAAAAAAYRandomWow' + Date.now();
client.executeScript(function(myRandomID) {
var el = document.createElement('div');
el.id = myRandomID;
el.innerHTML = 'Hello Test!';
document.body.appendChild(el);
setTimeout(function() {
el.style.display = 'none';
}, 500);
}, [myRandomID]);
// Ensure the waitFor times out.
client.onScriptTimeout = function() {
assert(true, 'this should timeout');
done();
};
try {
subject.waitForElementToDisappear('#' + myRandomID, { timeout: 10 });
assert(false, 'should timeout before disappearing');
} catch (e) {}
});
suite.skip('#waitForAlert', function() {
setup(function() {
console.log('Will alert!');
client.executeScript(function() {
alert('lololololololol');
});
});
test('should wait for substring sync', function() {
subject.waitForAlert('lol');
});
test('should wait for substring async', function(done) {
subject.waitForAlert('lol', done);
});
test('should wait for RegExp sync', function() {
subject.waitForAlert(/(lo)*l/);
});
test('should wait for RegExp async', function(done) {
subject.waitForAlert(/(lo)*l/, done);
});
});
});

View file

@ -0,0 +1,87 @@
'use strict';
var assert = require('assert');
suite('MarionetteHelper.fillInputFieldAndtapSelectOption', function() {
// Require needed files.
var FakeApp = require('./lib/fake_app');
marionette.plugin('helper', require('../index'));
marionette.plugin('apps', require('marionette-apps'));
var helper;
var fakeApp;
var FAKE_APP_ORIGIN = 'fakeapp.gaiamobile.org';
var apps = {};
apps[FAKE_APP_ORIGIN] = __dirname + '/fakeapp';
var testTime;
var client = marionette.client({
profile: {
settings: {
'ftu.manifestURL': null,
'lockscreen.enabled': false
},
apps: apps
}
});
// We add lead zero on single digit. ex: 1 -> 01, 9 -> 09.
function addLeadZero(num) {
return num >= 10 ? num : ('0' + num);
}
setup(function() {
helper = client.helper;
fakeApp = new FakeApp(client, 'app://' + FAKE_APP_ORIGIN);
fakeApp.launch();
testTime = new Date();
});
test('should set on option', function() {
var optionValue = 'option2';
helper.tapSelectOption('#select', optionValue);
assert.ok(fakeApp.isSpecificSelectOptionSelected(optionValue));
});
test('should set value on input', function() {
var inputValue = 'inputtest';
helper.fillInputField('#input', inputValue);
assert.equal(fakeApp.inputElementValue, inputValue);
});
test('should set date on input', function() {
var inputValue =
testTime.getFullYear() + '-' + addLeadZero(testTime.getMonth()) + '-' +
addLeadZero(testTime.getDate());
helper.fillInputField('#input-date', testTime);
assert.equal(fakeApp.inputDateElementValue, inputValue);
});
test('should set time on input', function() {
var inputValue = addLeadZero(testTime.getHours()) + ':' +
addLeadZero(testTime.getMinutes());
helper.fillInputField('#input-time', testTime);
assert.equal(fakeApp.inputTimeElementValue, inputValue);
});
test('should set datetime on input', function() {
var inputValue = testTime.toISOString();
helper.fillInputField('#input-datetime', testTime);
assert.equal(fakeApp.inputDatetimeElementValue, inputValue);
});
test('should set datetime-local on input', function() {
var inputValue = testTime.getFullYear() + '-' +
addLeadZero(testTime.getMonth()) + '-' +
addLeadZero(testTime.getDate()) + 'T' +
addLeadZero(testTime.getHours()) + ':' +
addLeadZero(testTime.getMinutes()) + ':' +
addLeadZero(testTime.getSeconds()) + '.' +
testTime.getMilliseconds();
helper.fillInputField('#input-datetime-local', testTime);
assert.equal(fakeApp.inputDatetimeLocalElementValue, inputValue);
});
});

View file

@ -0,0 +1,61 @@
/* global module */
'use strict';
function FakeApp(client, origin) {
this.client = client;
this.origin = origin;
}
FakeApp.Selector = Object.freeze({
selectElement: '#select',
selectedOptionElement: '#select option:checked',
inputElement: '#input',
inputDateElement: '#input-date',
inputTimeElement: '#input-time',
inputDatetimeElement: '#input-datetime',
inputDatetimeLocalElement: '#input-datetime-local'
});
FakeApp.prototype = {
client: null,
get inputElementValue() {
return this.client.findElement(
FakeApp.Selector.inputElement).getAttribute('value');
},
get inputDateElementValue() {
return this.client.findElement(
FakeApp.Selector.inputDateElement).getAttribute('value');
},
get inputTimeElementValue() {
return this.client.findElement(
FakeApp.Selector.inputTimeElement).getAttribute('value');
},
get inputDatetimeElementValue() {
return this.client.findElement(
FakeApp.Selector.inputDatetimeElement).getAttribute('value');
},
get inputDatetimeLocalElementValue() {
return this.client.findElement(
FakeApp.Selector.inputDatetimeLocalElement).getAttribute('value');
},
get selectElement() {
return this.client.findElement(FakeApp.Selector.selectElement);
},
get selectedOption() {
return this.client.findElement(FakeApp.Selector.selectedOptionElement);
},
isSpecificSelectOptionSelected: function(value) {
return value == this.selectedOption.text();
},
launch: function() {
this.client.apps.launch(this.origin);
this.client.apps.switchToApp(this.origin);
},
close: function() {
this.client.apps.close(this.origin);
}
};
module.exports = FakeApp;