Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
100
gaia/apps/clock/test/marionette/lib/actions.js
Executable file
100
gaia/apps/clock/test/marionette/lib/actions.js
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
/* global marionette */
|
||||
'use strict';
|
||||
|
||||
var $ = require('./mquery');
|
||||
var StopwatchActions = require('./stopwatch_actions');
|
||||
var TimerActions = require('./timer_actions');
|
||||
var AlarmActions = require('./alarm_actions');
|
||||
|
||||
function ClockAppActions(options) {
|
||||
this._client = marionette.client({
|
||||
profile: {
|
||||
prefs: {
|
||||
// we need to disable the keyboard to avoid intermittent failures on
|
||||
// Travis (transitions might take longer to run and block UI).
|
||||
'dom.mozInputMethod.enabled': false,
|
||||
// NOTE: We used to require 'focusmanager.testmode=true' for
|
||||
// B2G-Desktop, but Mulet and Device tests do not require it.
|
||||
}
|
||||
},
|
||||
desiredCapabilities: options && options.desiredCapabilities
|
||||
});
|
||||
|
||||
this.stopwatch = new StopwatchActions(this._client);
|
||||
this.timer = new TimerActions(this._client);
|
||||
this.alarm = new AlarmActions(this._client, this);
|
||||
}
|
||||
|
||||
module.exports = ClockAppActions;
|
||||
|
||||
ClockAppActions.prototype = {
|
||||
origin: 'app://clock.gaiamobile.org',
|
||||
|
||||
launch: function(tab) {
|
||||
$.client = this._client;
|
||||
this._client.apps.launch(this.origin);
|
||||
this._client.apps.switchToApp(this.origin);
|
||||
this._client.helper.waitForElement('#clock-view .visible');
|
||||
|
||||
if (tab && tab !== 'alarm') {
|
||||
this.openTab(tab);
|
||||
}
|
||||
|
||||
// Wait until everything has loaded.
|
||||
this._client.waitFor(function() {
|
||||
return !!this.currentPanelId;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
restart: function(tab) {
|
||||
this._client.apps.close(this.origin);
|
||||
this.launch(tab);
|
||||
},
|
||||
|
||||
get currentPanelId() {
|
||||
var activePanel = $('.panel.active');
|
||||
return activePanel && activePanel.data('panelId');
|
||||
},
|
||||
|
||||
tapAndTransition: function(selector) {
|
||||
var previousPanel = this.currentPanelId;
|
||||
|
||||
if (typeof selector === 'string') {
|
||||
selector = this._client.findElement(selector);
|
||||
}
|
||||
selector.tap();
|
||||
this._client.switchToShadowRoot();
|
||||
|
||||
var self = this;
|
||||
this._client.waitFor(function() {
|
||||
var searchTimeout = self._client.searchTimeout;
|
||||
self._client.setSearchTimeout(0);
|
||||
|
||||
var test = this.currentPanelId &&
|
||||
(previousPanel !== this.currentPanelId) &&
|
||||
!$('.slide-in-right, .slide-in-left')[0];
|
||||
self._client.setSearchTimeout(searchTimeout);
|
||||
return test;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
// Application Navigation
|
||||
|
||||
openTab: function(tabName) {
|
||||
// Tap the button to open the tab
|
||||
this.tapAndTransition({
|
||||
alarm: '#alarm-tab',
|
||||
timer: '#timer-tab',
|
||||
stopwatch: '#stopwatch-tab'
|
||||
}[tabName]);
|
||||
|
||||
$('[data-panel-id=' + tabName + ']')
|
||||
.waitToAppear();
|
||||
},
|
||||
|
||||
|
||||
isButtonUsable: function(btn) {
|
||||
return $(btn).attr('disabled') === 'false' && $(btn).displayed();
|
||||
}
|
||||
|
||||
};
|
||||
216
gaia/apps/clock/test/marionette/lib/alarm_actions.js
Executable file
216
gaia/apps/clock/test/marionette/lib/alarm_actions.js
Executable file
|
|
@ -0,0 +1,216 @@
|
|||
'use strict';
|
||||
/* global marionetteScriptFinished */
|
||||
|
||||
var utils = require('./utils');
|
||||
var $ = require('./mquery');
|
||||
|
||||
function AlarmActions(client, actions) {
|
||||
this._client = client;
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
module.exports = AlarmActions;
|
||||
|
||||
AlarmActions.prototype = {
|
||||
|
||||
openNewForm: function() {
|
||||
if ($('#banner-countdown').displayed()) {
|
||||
this.waitForBanner();
|
||||
}
|
||||
this.actions.tapAndTransition('#alarm-new');
|
||||
},
|
||||
|
||||
openEditForm: function(idx) {
|
||||
if ($('#banner-countdown').displayed()) {
|
||||
this.waitForBanner();
|
||||
}
|
||||
this.actions.tapAndTransition(
|
||||
'.alarm-cell:nth-child(' + (idx + 1) + ') .alarm-item');
|
||||
},
|
||||
|
||||
toggle: function(idx) {
|
||||
var alarmWasEnabled = this.list[idx].enabled;
|
||||
|
||||
$('.alarm-cell:nth-child(' + (idx + 1) + ') .alarmEnable span')
|
||||
.tap();
|
||||
this._client.waitFor(function() {
|
||||
return this.list[idx].enabled !== alarmWasEnabled;
|
||||
}.bind(this));
|
||||
|
||||
if (!alarmWasEnabled) {
|
||||
this.waitForBanner();
|
||||
}
|
||||
|
||||
var numAlarmsEnabled = this.list.filter(function(alarm) {
|
||||
return alarm.enabled;
|
||||
}).length;
|
||||
|
||||
// If the number of alarms transitioned from 0 <=> 1, ensure the
|
||||
// system status bar updates before continuing.
|
||||
if (numAlarmsEnabled === 1) {
|
||||
this._client.waitFor(function() {
|
||||
return this.statusIcon;
|
||||
}.bind(this));
|
||||
} else if (numAlarmsEnabled === 0) {
|
||||
this._client.waitFor(function() {
|
||||
return !this.statusIcon;
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
cancelForm: function() {
|
||||
var header = this._client.findElement('gaia-header');
|
||||
this._client.switchToShadowRoot(header);
|
||||
var button = this._client.findElement('.action-button');
|
||||
this.actions.tapAndTransition(button);
|
||||
},
|
||||
|
||||
delete: function() {
|
||||
this.actions.tapAndTransition('#alarm-delete');
|
||||
},
|
||||
|
||||
saveForm: function() {
|
||||
this.actions.tapAndTransition('#alarm-done');
|
||||
},
|
||||
|
||||
get nameField() {
|
||||
return $('#edit-alarm [name="alarm.label"]');
|
||||
},
|
||||
|
||||
get timeField() {
|
||||
return $('#time-select');
|
||||
},
|
||||
|
||||
get volumeInput() {
|
||||
return $('#alarm-volume-input');
|
||||
},
|
||||
|
||||
fire: function(idx, date, attentionHandler, type) {
|
||||
// Trigger a fake 'alarm' event:
|
||||
this._client.executeScript(function(idx, date, type) {
|
||||
// String to Date object.
|
||||
date = new Date(date);
|
||||
var id = document.querySelector(
|
||||
'.alarm-cell:nth-child(' + (idx + 1) + ') .alarm-item').dataset.id;
|
||||
var alarm = new CustomEvent('test-alarm', {
|
||||
detail: {
|
||||
id: parseInt(id, 10),
|
||||
type: type || 'normal',
|
||||
date: date
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(alarm);
|
||||
}, [idx, date, type]);
|
||||
|
||||
if (attentionHandler) {
|
||||
// Switch to the Attention Screen
|
||||
this._client.switchToFrame();
|
||||
this._client.switchToFrame($('iframe[name="_blank"]').el);
|
||||
// Wait for the onring page is ready.
|
||||
this._client.waitFor(function() {
|
||||
if (this._client.findElement('html')
|
||||
.getAttribute('class')
|
||||
.indexOf('ready') !== -1) {
|
||||
return true;
|
||||
}
|
||||
}.bind(this));
|
||||
attentionHandler();
|
||||
// Now switch back to the system, then to the clock.
|
||||
this._client.switchToFrame();
|
||||
this._client.apps.switchToApp(this.actions.origin);
|
||||
}
|
||||
},
|
||||
|
||||
remove: function(idx) {
|
||||
var count = this.list.length;
|
||||
this.openEditForm(idx);
|
||||
this.actions.tapAndTransition('#alarm-delete');
|
||||
this._client.waitFor(function() {
|
||||
return this.list.length === count - 1;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
create: function(name, minutesFromNow) {
|
||||
name = name || 'Alarm';
|
||||
if (!minutesFromNow) {
|
||||
minutesFromNow = 20;
|
||||
}
|
||||
|
||||
var originalAlarmCount = this.list.length;
|
||||
|
||||
var now = Date.now();
|
||||
this.openNewForm();
|
||||
|
||||
this.nameField.val(name);
|
||||
this.timeField.val(new Date(now + minutesFromNow));
|
||||
|
||||
var time = new Date();
|
||||
time.setHours.apply(time, this.timeField.val().split(':'));
|
||||
|
||||
this.saveForm();
|
||||
|
||||
var alarmData = null;
|
||||
this._client.waitFor(function() {
|
||||
// Verify that the alarm created is in the list of alarms.
|
||||
this.list.forEach(function(alarm) {
|
||||
if (alarm.name === name && alarm.enabled &&
|
||||
utils.stringContainsTime(alarm.timeString, time)) {
|
||||
alarmData = alarm;
|
||||
}
|
||||
});
|
||||
return !!alarmData;
|
||||
}.bind(this));
|
||||
|
||||
this._client.waitFor(function() {
|
||||
return this.list.length === originalAlarmCount + 1;
|
||||
}.bind(this));
|
||||
|
||||
$('[data-panel-id="alarm"]').waitToAppear();
|
||||
this.waitForBanner();
|
||||
return {
|
||||
name: alarmData.name,
|
||||
time: time,
|
||||
timeString: alarmData.timeString,
|
||||
enabled: alarmData.enabled
|
||||
};
|
||||
},
|
||||
|
||||
get list() {
|
||||
// For now, do this all in one executeScript() call, because the
|
||||
// rendering logic for alarms wipes out all elements at once. It
|
||||
// could cause us to read inconsistent state from the DOM.
|
||||
return this._client.executeScript(function() {
|
||||
var domItems = document.querySelectorAll('.alarm-cell');
|
||||
var alarms = [];
|
||||
for (var i = 0; i < domItems.length; i++) {
|
||||
var item = domItems[i];
|
||||
alarms.push({
|
||||
name: item.querySelector('.label').textContent,
|
||||
timeString: item.querySelector('.time').textContent,
|
||||
enabled: item.querySelector('.input-enable').checked
|
||||
});
|
||||
}
|
||||
return alarms;
|
||||
});
|
||||
},
|
||||
|
||||
// Status Checks
|
||||
|
||||
get statusIcon() {
|
||||
this._client.setScriptTimeout(2000);
|
||||
return this._client.executeAsyncScript(function() {
|
||||
var req = navigator.mozSettings.createLock().get('alarm.enabled');
|
||||
req.onsuccess = function() {
|
||||
marionetteScriptFinished(req.result['alarm.enabled']);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
waitForBanner: function() {
|
||||
$('#banner-countdown')
|
||||
.tap()
|
||||
.waitToDisappear();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
265
gaia/apps/clock/test/marionette/lib/mquery.js
Executable file
265
gaia/apps/clock/test/marionette/lib/mquery.js
Executable file
|
|
@ -0,0 +1,265 @@
|
|||
'use strict';
|
||||
var Marionette = require('marionette-client');
|
||||
|
||||
/**
|
||||
* mQuery allows you to query and modify the DOM through Marionette,
|
||||
* in a style resembling jQuery. While jQuery is often heckled for
|
||||
* being poor for designing large-scale apps, its quick DOM traversal
|
||||
* and manipulation is hard to beat. For integration tests, which tend
|
||||
* to require convenient, clear, mostly-read-only access to the DOM
|
||||
* with fewer architectural concerns, a jQuery-like API fits well.
|
||||
*
|
||||
* Typical use is something like this:
|
||||
*
|
||||
* var $ = require('mquery');
|
||||
*
|
||||
* $.client = marionette.client();
|
||||
*
|
||||
* console.log($('.my-element').text());
|
||||
* console.log($('#textbox').val());
|
||||
*
|
||||
* You can also set values in input fields like so:
|
||||
*
|
||||
* $('#textbox').val('foo');
|
||||
*
|
||||
* This also works with system <select> boxes and other inputs too.
|
||||
*
|
||||
* Architecturally, mQuery does something interesting: An item
|
||||
* returned from mQuery is _both_ an array of elements _and_ a single
|
||||
* Element at the same time. For instance, you can pass the result of
|
||||
* $('.foo') directly to Marionette calls that require a
|
||||
* Marionette.Element; similarly, since mQuery behaves like an array
|
||||
* as well, you can also use this anywhere you previously used
|
||||
* Marionette.findElements().
|
||||
*
|
||||
* While typical usage assigns $.client to your Marionette.Client
|
||||
* instance, you can instead pass { client: myClient } as an option to
|
||||
* mQuery if you'd prefer not to globally scope out your mQuery
|
||||
* instances.
|
||||
*
|
||||
* Read the code to see what functions are implemented; many of the
|
||||
* most commonly-used jQuery features are included. Add more if you
|
||||
* find them useful. While this initially contains some
|
||||
* half-implemented methods (e.g. ".css()" can only read values, not
|
||||
* set them), the eventual goal is to match jQuery's API where it
|
||||
* makes sense.
|
||||
*/
|
||||
var mQuery = function(selectorOrElement, opts) {
|
||||
var client = (opts && opts.client) || mQuery.client;
|
||||
var parent = (opts && opts.parent) || null;
|
||||
|
||||
if (!client) {
|
||||
throw new Error(
|
||||
'You must set mQuery.client to your Marionette client.');
|
||||
}
|
||||
|
||||
var elements;
|
||||
var selector;
|
||||
if (typeof selectorOrElement === 'string') {
|
||||
elements = (parent || mQuery.client).findElements(selectorOrElement);
|
||||
selector = selectorOrElement;
|
||||
} else if (Array.isArray(selectorOrElement)) {
|
||||
elements = selectorOrElement;
|
||||
} else if (selectorOrElement instanceof ElementSet) {
|
||||
return selectorOrElement;
|
||||
} else if (selectorOrElement instanceof Marionette.Element) {
|
||||
elements = [selectorOrElement];
|
||||
} else {
|
||||
throw new Error('mQuery doesn\'t know how to handle ' + selectorOrElement);
|
||||
}
|
||||
|
||||
return new ElementSet(elements, {
|
||||
client: client,
|
||||
selector: selector
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = mQuery;
|
||||
|
||||
mQuery.client = null;
|
||||
|
||||
/**
|
||||
* ElementSet acts as a lightweight wrapper over Marionette.Element,
|
||||
* providing chainable, functional interface somewhat like jQuery.
|
||||
*/
|
||||
function ElementSet(elements, opts) {
|
||||
this.client = opts.client;
|
||||
this.selector = opts.selector;
|
||||
this.el = elements[0] || null;
|
||||
this.id = this.el && this.el.id; // Act like Marionette if we can.
|
||||
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
this.push(elements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Behave like an Array of Marionette.Element instances.
|
||||
ElementSet.prototype = Object.create(Array.prototype);
|
||||
|
||||
// Behave like a single instance of Marionette.Element too.
|
||||
Object.keys(Marionette.Element.prototype).forEach(function(funcName) {
|
||||
var elementFn = Marionette.Element.prototype[funcName];
|
||||
ElementSet.prototype[funcName] = function() {
|
||||
return elementFn.apply(this.el, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
// Behave like jQuery. (These are only getters, not setters though.)
|
||||
ElementSet.prototype.attr = ElementSet.prototype.getAttribute;
|
||||
ElementSet.prototype.css = ElementSet.prototype.cssProperty;
|
||||
|
||||
ElementSet.prototype.toString = function() {
|
||||
return '<ElementSet: ' + this.length + '>';
|
||||
};
|
||||
|
||||
// For the purposes of executeScript, behave like Marionette.Element.
|
||||
ElementSet.prototype.toJSON = function() {
|
||||
return { ELEMENT: this.id };
|
||||
};
|
||||
|
||||
// Chainable helper methods.
|
||||
|
||||
ElementSet.prototype.waitToAppear = function() {
|
||||
this.client.helper.waitForElement(this.el);
|
||||
return this;
|
||||
};
|
||||
|
||||
ElementSet.prototype.waitToDisappear = function() {
|
||||
this.client.helper.waitForElementToDisappear(this.el);
|
||||
return this;
|
||||
};
|
||||
|
||||
function makeChainable(proto, funcName) {
|
||||
var origFn = proto[funcName];
|
||||
proto[funcName] = function() {
|
||||
origFn.apply(this, arguments);
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
makeChainable(ElementSet.prototype, 'clear');
|
||||
makeChainable(ElementSet.prototype, 'sendKeys');
|
||||
|
||||
ElementSet.prototype.tap = function() {
|
||||
this.waitToAppear();
|
||||
this.el.scriptWith(function(el) {
|
||||
el.scrollIntoView(false);
|
||||
});
|
||||
this.el.tap();
|
||||
return this;
|
||||
};
|
||||
|
||||
ElementSet.prototype.find = function(selector) {
|
||||
if (!this.el) {
|
||||
throw new Error(
|
||||
'Cannot call mQuery.find() when mQuery did not match any elements.');
|
||||
}
|
||||
return mQuery(selector, {
|
||||
client: this.client,
|
||||
parent: this.el
|
||||
});
|
||||
};
|
||||
|
||||
ElementSet.prototype.eq = function(idx) {
|
||||
return mQuery(this[idx], {
|
||||
client: this.client
|
||||
});
|
||||
};
|
||||
|
||||
ElementSet.prototype.height = function() {
|
||||
if (!this.el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.el.size().height;
|
||||
};
|
||||
|
||||
ElementSet.prototype.width = function() {
|
||||
if (!this.el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.el.size().width;
|
||||
};
|
||||
|
||||
ElementSet.prototype.parent = function() {
|
||||
if (!this.el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mQuery(this.client.executeScript(function(el) {
|
||||
return el.parentElement;
|
||||
}, [this.el]), {
|
||||
client: this.client
|
||||
});
|
||||
};
|
||||
|
||||
ElementSet.prototype.data = function(attr, value) {
|
||||
if (!this.el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
return this.client.executeScript(function(el, attr) {
|
||||
return el.dataset[attr];
|
||||
}, [this.el, attr]);
|
||||
} else if (arguments.length === 2) {
|
||||
return this.client.executeScript(function(el, attr, value) {
|
||||
el.dataset[attr] = value;
|
||||
}, [this.el, attr, value]);
|
||||
} else {
|
||||
return this.client.executeScript(function(el) {
|
||||
return el.dataset;
|
||||
}, [this.el]);
|
||||
}
|
||||
};
|
||||
|
||||
ElementSet.prototype.val = function(value) {
|
||||
if (!this.el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var el = this.el;
|
||||
if (!arguments.length) {
|
||||
return el.getAttribute('value');
|
||||
} else {
|
||||
if (el.tagName().toLowerCase() === 'select') {
|
||||
// Save the origin so that we know how to switch back from the
|
||||
// system frame to the application frame.
|
||||
var origin = this.client.executeScript(function() {
|
||||
return window.location.origin;
|
||||
});
|
||||
|
||||
this.client.executeScript(function(el) {
|
||||
window.focus(); // Without this line, sometimes el.focus() does nothing.
|
||||
el.focus();
|
||||
}, [el]);
|
||||
|
||||
this.client.switchToFrame(); // Switch to the system frame.
|
||||
|
||||
var options = this.client.findElements(
|
||||
'.value-selector-select-option-popup li');
|
||||
var values = (!Array.isArray(value) ? [value] : value);
|
||||
options.forEach(function(option) {
|
||||
var itemText = option.findElement('label span').text();
|
||||
var shouldBeSelected = (values.indexOf(itemText) !== -1);
|
||||
var isSelected = (option.getAttribute('aria-selected') === 'true');
|
||||
if (shouldBeSelected !== isSelected) {
|
||||
option.scriptWith(function(el) {
|
||||
el.scrollIntoView(false);
|
||||
});
|
||||
option.tap();
|
||||
}
|
||||
});
|
||||
this.client.helper.waitForElement('.value-option-confirm').tap();
|
||||
this.client.apps.switchToApp(origin);
|
||||
} else {
|
||||
this.client.forms.fill(el, value);
|
||||
}
|
||||
el.scriptWith(function(el) {
|
||||
el.dispatchEvent(new CustomEvent('change'));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
40
gaia/apps/clock/test/marionette/lib/picker.js
Executable file
40
gaia/apps/clock/test/marionette/lib/picker.js
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
'use strict';
|
||||
/* global KeyEvent */
|
||||
|
||||
var $ = require('./mquery');
|
||||
|
||||
exports.getSpinnerValue = function(el) {
|
||||
el = $(el);
|
||||
var offsetTop = /translateY\(-?(\d+)/.exec(el.attr('style'))[1];
|
||||
var pickerItems = el.find('.picker-unit');
|
||||
var itemHeight = pickerItems[0].size().height;
|
||||
var index = Math.floor(parseInt(offsetTop, 10) / itemHeight);
|
||||
return parseInt(pickerItems[index].text(), 10);
|
||||
};
|
||||
|
||||
exports.setSpinnerValue = function(el, value) {
|
||||
el = $(el);
|
||||
var currentValue = exports.getSpinnerValue(el);
|
||||
var targetValue = parseInt(value, 10);
|
||||
var delta = targetValue - currentValue;
|
||||
|
||||
el.parent().scriptWith(function(el, delta) {
|
||||
var count = Math.abs(delta);
|
||||
for (var i = 0; i < count; i++) {
|
||||
var evt = new CustomEvent('keypress');
|
||||
evt.keyCode = delta > 0 ? KeyEvent.DOM_VK_DOWN : KeyEvent.DOM_VK_UP;
|
||||
el.dispatchEvent(evt);
|
||||
}
|
||||
}, [delta]);
|
||||
|
||||
exports.waitForSpinStop(el);
|
||||
};
|
||||
|
||||
exports.waitForSpinStop = function(el) {
|
||||
var currentY, lastY;
|
||||
$.client.waitFor(function() {
|
||||
lastY = currentY;
|
||||
currentY = el.location().y;
|
||||
return currentY === lastY;
|
||||
}, { interval: 200 });
|
||||
};
|
||||
71
gaia/apps/clock/test/marionette/lib/stopwatch_actions.js
Executable file
71
gaia/apps/clock/test/marionette/lib/stopwatch_actions.js
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
'use strict';
|
||||
|
||||
var utils = require('./utils');
|
||||
var $ = require('./mquery');
|
||||
|
||||
function StopwatchActions(client) {
|
||||
this._client = client;
|
||||
}
|
||||
|
||||
module.exports = StopwatchActions;
|
||||
|
||||
StopwatchActions.prototype = {
|
||||
get duration() {
|
||||
return utils.extractDuration($('.stopwatch-time').text());
|
||||
},
|
||||
|
||||
get laps() {
|
||||
return this._client.executeScript(function() {
|
||||
var domItems = document.querySelectorAll('.lap-cell');
|
||||
var laps = [];
|
||||
for (var i = 0; i < domItems.length; i++) {
|
||||
var item = domItems[i];
|
||||
laps.push({
|
||||
name: item.querySelector('.lap-name').textContent,
|
||||
durationString: item.querySelector('.lap-duration').textContent
|
||||
});
|
||||
}
|
||||
return laps;
|
||||
}).map(function(lap) {
|
||||
lap.duration = utils.extractDuration(lap.durationString);
|
||||
return lap;
|
||||
});
|
||||
},
|
||||
|
||||
start: function() {
|
||||
$('.stopwatch-start').tap();
|
||||
this.advanceTime();
|
||||
return this;
|
||||
},
|
||||
|
||||
lap: function() {
|
||||
$('.stopwatch-lap').tap();
|
||||
this.advanceTime();
|
||||
return this;
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
$('.stopwatch-pause').tap();
|
||||
return this;
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
$('.stopwatch-reset').tap();
|
||||
return this;
|
||||
},
|
||||
|
||||
setMaxLaps: function(num) {
|
||||
$('[data-panel-id="stopwatch"]').data('maxLaps', num);
|
||||
return this;
|
||||
},
|
||||
|
||||
advanceTime: function(milliseconds) {
|
||||
milliseconds = milliseconds || 1;
|
||||
var expectedDuration = this.duration + milliseconds;
|
||||
this._client.waitFor(function() {
|
||||
return this.duration >= expectedDuration;
|
||||
}.bind(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
};
|
||||
57
gaia/apps/clock/test/marionette/lib/timer_actions.js
Executable file
57
gaia/apps/clock/test/marionette/lib/timer_actions.js
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
'use strict';
|
||||
|
||||
var utils = require('./utils');
|
||||
var $ = require('./mquery');
|
||||
var pickerUtils = require('./picker');
|
||||
|
||||
function TimerActions(client) {
|
||||
this._client = client;
|
||||
}
|
||||
|
||||
module.exports = TimerActions;
|
||||
|
||||
TimerActions.prototype = {
|
||||
|
||||
get hours() { return pickerUtils.getSpinnerValue('.picker-hours'); },
|
||||
get minutes() { return pickerUtils.getSpinnerValue('.picker-minutes'); },
|
||||
set hours(value) { pickerUtils.setSpinnerValue('.picker-hours', value); },
|
||||
set minutes(value) { pickerUtils.setSpinnerValue('.picker-minutes', value); },
|
||||
|
||||
get countdown() {
|
||||
return utils.extractDuration($('#timer-time').text());
|
||||
},
|
||||
|
||||
get sound() {
|
||||
return $('#timer-sound').val();
|
||||
},
|
||||
|
||||
start: function() {
|
||||
$('#timer-create').tap();
|
||||
// Wait until the timer has moved from its default state of 0:00
|
||||
this._client.waitFor(function() {
|
||||
return this.countdown > 0;
|
||||
}.bind(this));
|
||||
return this;
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
$('#timer-pause').tap();
|
||||
return this;
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
$('#timer-start').tap();
|
||||
return this;
|
||||
},
|
||||
|
||||
cancel: function() {
|
||||
$('#timer-cancel').tap();
|
||||
$('#timer-create').waitToAppear();
|
||||
return this;
|
||||
},
|
||||
|
||||
advanceTime: function(milliseconds) {
|
||||
return this._client.helper.wait(milliseconds);
|
||||
}
|
||||
|
||||
};
|
||||
85
gaia/apps/clock/test/marionette/lib/utils.js
Executable file
85
gaia/apps/clock/test/marionette/lib/utils.js
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
/**
|
||||
* Extract a duration from any point in the given string. See the
|
||||
* inline assertions below for expected usage.
|
||||
*
|
||||
* @return Duration found in milliseconds, or null if none found.
|
||||
*/
|
||||
exports.extractDuration = function(str) {
|
||||
var match = /(?:(\d+):)?(\d+):(\d+)(?:[:.](\d)?(\d)?(\d)?)?/i.exec(str);
|
||||
if (!match) { return null; }
|
||||
var factors = [60 * 60 * 1000, 60 * 1000, 1000, 100, 10, 1];
|
||||
return match.slice(1).reduce(function(sum, value, i) {
|
||||
return sum + ((value || 0) * factors[i]);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
assert.equal(exports.extractDuration('It took 0:00:00.000'), 0);
|
||||
assert.equal(exports.extractDuration('It took 00:00.1'), 100);
|
||||
assert.equal(exports.extractDuration('It took 1:00:00.010'), 3600010);
|
||||
assert.equal(exports.extractDuration('It took 1:05:10.050'), 3910050);
|
||||
assert.equal(exports.extractDuration('It took 30:22:22.55'), 109342550);
|
||||
|
||||
/**
|
||||
* Extract a time from any point in the given string. See the inline
|
||||
* assertions below for expected usage.
|
||||
*
|
||||
* @return An object with { hours: N, minutes: N } in 24-hour format.
|
||||
*/
|
||||
function extractTime(str) {
|
||||
var match = /\b([0-9]+):([0-9]+)\s*(am|pm|a\.m\.|p\.m\.)?/i.exec(str);
|
||||
var hr = parseInt(match[1], 10);
|
||||
var min = parseInt(match[2], 10);
|
||||
var hasAmPm = !!match[3];
|
||||
if (hasAmPm && /p/i.test(match[3])) {
|
||||
if (hr < 12) {
|
||||
hr += 12;
|
||||
}
|
||||
} else {
|
||||
if (hr === 12) {
|
||||
hr = 0;
|
||||
}
|
||||
}
|
||||
return { hours: hr, minutes: min, hasAmPm: hasAmPm };
|
||||
}
|
||||
|
||||
exports.assertStringContainsTime = function(str, date) {
|
||||
var time = extractTime(str);
|
||||
if (time.hasAmPm) {
|
||||
assert.equal(time.hours, date.getHours());
|
||||
} else {
|
||||
assert.equal(time.hours % 12, date.getHours() % 12);
|
||||
}
|
||||
assert.equal(time.minutes, date.getMinutes());
|
||||
};
|
||||
|
||||
exports.stringContainsTime = function(str, date) {
|
||||
try {
|
||||
exports.assertStringContainsTime(str, date);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
exports.stringToDate = function(str) {
|
||||
var time = extractTime(str);
|
||||
var date = new Date();
|
||||
date.setHours(time.hours);
|
||||
date.setMinutes(time.minutes);
|
||||
return date;
|
||||
};
|
||||
|
||||
assert.deepEqual(extractTime('It is 12:30 PM.'),
|
||||
{ hours: 12, minutes: 30, hasAmPm: true });
|
||||
assert.deepEqual(extractTime('It is 12:30 AM.'),
|
||||
{ hours: 0, minutes: 30, hasAmPm: true });
|
||||
assert.deepEqual(extractTime('It is 15:22.'),
|
||||
{ hours: 15, minutes: 22, hasAmPm: false });
|
||||
assert.deepEqual(extractTime('It is 0:00.'),
|
||||
{ hours: 0, minutes: 0, hasAmPm: false });
|
||||
assert.deepEqual(extractTime('It is 6:00 p.m..'),
|
||||
{ hours: 18, minutes: 0, hasAmPm: true });
|
||||
Loading…
Reference in a new issue