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,19 @@
{
"name": "dashboard",
"version": "0.0.0",
"homepage": "https://github.com/mozilla-b2g/gaia/tree/master/tv_apps/dashboard",
"description": "Smart Screen Dashboard App",
"main": "index.html",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"evt": "fxos-components/evt#~0.4.0"
}
}

View file

@ -0,0 +1,19 @@
{
"name": "evt",
"main": "index.js",
"version": "0.4.0",
"homepage": "https://github.com/wilsonpage/evt",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"package.json",
"test",
"tests",
"README.md"
]
}

View file

@ -0,0 +1,154 @@
/**
* Evt
*
* A super lightweight
* event emitter library.
*
* @version 0.3.3
* @author Wilson Page <wilson.page@me.com>
*/
;(function() {
/**
* Locals
*/
var proto = Events.prototype;
var slice = [].slice;
/**
* Creates a new event emitter
* instance, or if passed an
* object, mixes the event logic
* into it.
*
* @param {Object} obj
* @return {Object}
*/
function Events(obj) {
if (!(this instanceof Events)) return new Events(obj);
if (obj) return mixin(obj, proto);
}
/**
* Registers a callback
* with an event name.
*
* @param {String} name
* @param {Function} cb
* @return {Event}
*/
proto.on = function(name, cb) {
this._cbs = this._cbs || {};
(this._cbs[name] || (this._cbs[name] = [])).push(cb);
return this;
};
/**
* Attach a callback that once
* called, detaches itself.
*
* TODO: Implement `.off()` to work
* with `once()` callbacks.
*
* @param {String} name
* @param {Function} cb
* @public
*/
proto.once = function(name, cb) {
this.on(name, one);
function one() {
cb.apply(this, arguments);
this.off(name, one);
}
};
/**
* Removes a single callback,
* or all callbacks associated
* with the passed event name.
*
* @param {String} name
* @param {Function} cb
* @return {Event}
*/
proto.off = function(name, cb) {
this._cbs = this._cbs || {};
if (!name) { this._cbs = {}; return; }
if (!cb) { return delete this._cbs[name]; }
var cbs = this._cbs[name] || [];
var i;
while (cbs && ~(i = cbs.indexOf(cb))) { cbs.splice(i, 1); }
return this;
};
/**
* Fires an event, triggering
* all callbacks registered on this
* event name.
*
* @param {String} name
* @return {Event}
*/
proto.fire = proto.emit = function(options) {
var cbs = this._cbs = this._cbs || {};
var name = options.name || options;
var batch = (cbs[name] || []).concat(cbs['*'] || []);
var ctx = options.ctx || this;
if (batch.length) {
this._fireArgs = arguments;
var args = slice.call(arguments, 1);
while (batch.length) {
batch.shift().apply(ctx, args);
}
}
return this;
};
proto.firer = function(name) {
var self = this;
return function() {
var args = slice.call(arguments);
args.unshift(name);
self.fire.apply(self, args);
};
};
/**
* Util
*/
/**
* Mixes in the properties
* of the second object into
* the first.
*
* @param {Object} a
* @param {Object} b
* @return {Object}
*/
function mixin(a, b) {
for (var key in b) a[key] = b[key];
return a;
}
/**
* Expose 'Event' (UMD)
*/
if (typeof exports === 'object') {
module.exports = Events;
} else if (typeof define === 'function' && define.amd) {
define(function(){ return Events; });
} else {
window.evt = Events;
}
})();

View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Dashboard</title>
<link rel="stylesheet" type="text/css" href="style/dashboard.css">
<!-- Shared L10n libraries -->
<script src="shared/js/l10n.js"></script>
<script src="shared/js/l10n_date.js"></script>
<!-- L10n properties -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/dashboard.{locale}.properties"/>
<link rel="localization" href="shared/locales/date/date.{locale}.properties"/>
<!-- Helper libraries -->
<script defer src="bower_components/evt/index.js"></script>
<!-- Shared TV library for keyboard-based navigating -->
<script defer src="shared/js/smart-screen/key_navigation_adapter.js"></script>
<!-- Specific code -->
<script defer src="js/app_widget.js"></script>
<script defer src="js/digital_clock.js"></script>
<script defer src="js/dashboard.js"></script>
<script defer src="js/bootstrap.js"></script>
</head>
<body>
<div id="background" class="background"></div>
<div id="background-mask" class="background-mask"></div>
<div role="region" id="main-section" tabIndex="-1">
<section id="center-panel">
<div id="digital-clock">
<p id="time-now" class='time'>
<span id="hours-tens-digit" class="hours-tens-digit"></span>
<span id="hours-units-digit" class="hours-units-digit"></span>
<span class="colons">:</span>
<span id="minutes-tens-digit" class="minutes-tens-digit"></span>
<span id="minutes-units-digit" class="minutes-units-digit"></span>
<span class="colons">:</span>
<span id="seconds-tens-digit" class="seconds-tens-digit"></span>
<span id="seconds-units-digit" class="seconds-units-digit"></span>
</p>
<p id="date-now" class='date'>
<span id="today-is" data-l10n-id="today-is"></span><br>
<span id="greeting"></span>
</p>
</div>
</section>
<section id="top-panel"></section>
<section id="right-panel"></section>
<section id="bottom-panel"></section>
<section id="left-panel"></section>
</div>
</body>
</html>

View file

@ -0,0 +1,44 @@
'use strict';
(function(exports) {
function AppWidget(config) {
this.manifestURL = config.manifestURL;
this.widget = config.widget;
this.launchPath = config.url;
this.position = config.position;
this.isExpanded = false;
var browser = document.createElement('iframe');
// XXX: Change the following property and dashboard App permission to
// widget-specific one after widget path bug is fixed. See bug 1167063.
browser.setAttribute('mozbrowser', 'true');
browser.setAttribute('remote', 'true');
browser.setAttribute('mozapp', config.manifestURL);
browser.src = config.url;
this.iframe = browser;
this.container = document.getElementById(this.position + '-panel');
this.container.appendChild(this.iframe);
}
AppWidget.prototype = {
_setHash: function aw_setHash(hash) {
// Note: The '#' should always present to prevent reload
this.iframe.src = this.iframe.src.split('#')[0] + '#' + hash;
},
toggleExpand: function aw_setExpand(value) {
if(typeof value !== 'undefined') {
this.isExpanded = ! this.isExpanded;
} else {
this.isExpanded = value;
}
this._setHash(this.isExpanded ? 'expand' : '');
},
focus: function aw_focus() {
this.iframe.focus();
}
};
exports.AppWidget = AppWidget;
}(window));

4
gaia/tv_apps/dashboard/js/bootstrap.js vendored Executable file
View file

@ -0,0 +1,4 @@
/* globals Dashboard */
'use strict';
window.dashboard = new Dashboard();
window.dashboard.init();

View file

@ -0,0 +1,99 @@
'use strict';
/* global KeyNavigationAdapter, AppWidget, DigitalClock */
(function(exports) {
function Dashboard () {}
Dashboard.prototype = {
init: function d_init() {
document.body.dataset.activeDirection = '';
this.elements = {};
this.elements.mainSection = document.getElementById('main-section');
this.keyNavigationAdapter = new KeyNavigationAdapter();
this.keyNavigationAdapter.on('move', this.onMove.bind(this));
this.keyNavigationAdapter.on('before-move', this.onMove.bind(this));
this.keyNavigationAdapter.init(window, {useMozBrowserKeyEvents: true});
document.addEventListener(
'visibilitychange', this.onVisibilityChange.bind(this));
this.widgets = {};
this.widgets.down = new AppWidget({
manifestURL: 'app://weather-widget.gaiamobile.org/manifest.webapp',
widget: 'weather',
url: 'app://weather-widget.gaiamobile.org/widget.html',
position: 'bottom'
});
this.digitalClock = new DigitalClock();
this.digitalClock.init();
this.digitalClock.start();
},
onVisibilityChange: function d_onVisibilityChange() {
if (document.visibilityState !== 'visible') {
this._clearActiveDirection();
document.body.classList.remove('active');
} else {
this.digitalClock.generateGreeting();
document.body.classList.add('active');
}
},
onMove: function d_onMove(key) {
var activeDirection = document.body.dataset.activeDirection;
switch (activeDirection) {
case 'up':
if (key === 'down') {
this._clearActiveDirection();
}
break;
case 'right':
if (key === 'left') {
this._clearActiveDirection();
}
break;
case 'down':
if (key === 'up') {
this._clearActiveDirection();
}
break;
case 'left':
if (key === 'right') {
this._clearActiveDirection();
}
break;
default:
document.body.dataset.activeDirection = key;
if (this.widgets[key]) {
this.widgets[key].toggleExpand(true);
this._holdFocus();
// Note: Gecko refuses to change focus on a key event callback. we
// need to setTimeout to prevent fail.
setTimeout(() => this.widgets[key].focus());
}
break;
}
},
_holdFocus: function() {
document.activeElement.blur();
this.elements.mainSection.focus();
},
_clearActiveDirection: function() {
var direction = document.body.dataset.activeDirection;
this.widgets[direction] && this.widgets[direction].toggleExpand(false);
document.body.dataset.activeDirection = '';
this._holdFocus();
}
};
exports.Dashboard = Dashboard;
}(window));

View file

@ -0,0 +1,138 @@
'use strict';
(function (exports) {
function DigitalClock () {
this.date = null;
this.timeoutId = null;
this.elements = {
'hours-tens-digit': document.getElementById('hours-tens-digit'),
'hours-units-digit': document.getElementById('hours-units-digit'),
'minutes-tens-digit': document.getElementById('minutes-tens-digit'),
'minutes-units-digit': document.getElementById('minutes-units-digit'),
'seconds-tens-digit': document.getElementById('seconds-tens-digit'),
'seconds-units-digit': document.getElementById('seconds-units-digit'),
'today': document.getElementById('today-is'),
'greeting': document.getElementById('greeting')
};
}
DigitalClock.prototype = {
init: function () {
this._initTime(new Date());
},
start: function () {
this._tick(this._update.bind(this));
},
stop: function () {
if (this.timeoutId) {
window.clearTimeout(this.timeoutId);
this.timeoutId = null;
}
},
/**
* Start ticking the clock.
* @param {Function} refresh A callback to run on every tick.
*/
_tick: function (refresh) {
var now = new Date();
refresh(now);
self.timeoutId = setTimeout(this._tick.bind(this, refresh),
1000 - now.getMilliseconds());
},
/**
* Init the time of the clock.
*/
_initTime: function (date) {
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();
this.elements['hours-tens-digit'].textContent = Math.floor(hours / 10);
this.elements['hours-units-digit'].textContent = hours % 10;
this.elements['minutes-tens-digit'].textContent =
Math.floor(minutes / 10);
this.elements['minutes-units-digit'].textContent = minutes % 10;
this.elements['seconds-tens-digit'].textContent =
Math.floor(seconds / 10);
this.elements['seconds-units-digit'].textContent = seconds % 10;
this._changeDate(date);
this.date = date;
},
generateGreeting: function () {
var hours = this.date ? this.date.getHours() : (new Date()).getHours;
this.elements.greeting.dataset.l10nId = 'greeting-at-' + hours;
},
/**
* Update the display date and time.
* @param {Date} date The Date to be displayed.
*/
_update: function (date) {
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();
if (this.date.getHours() !== hours) {
this._changeTime('hours', this.date.getHours(), hours);
}
if (this.date.getMinutes() !== minutes) {
this._changeTime('minutes', this.date.getMinutes(), minutes);
}
this._changeTime('seconds', this.date.getSeconds(), seconds);
if (this.date.getDate() !== date.getDate()) {
this._changeDate(date);
}
this.date = date;
},
_changeTime: function(unit, oldValue, newValue) {
if (Math.floor(oldValue / 10) !== Math.floor(newValue / 10)) {
this._changeDigit(unit + '-tens-digit', Math.floor(newValue / 10));
}
this._changeDigit(unit + '-units-digit', newValue % 10);
},
/**
* Change the specified digit and handle the changing animations.
* @param {String} position The position of the element.
* @param {Number} digit The number to be changed to.
*/
_changeDigit: function (position, digit) {
// TODO: handle animations
this.elements[position].textContent = digit;
},
/**
* Change the display date using mozL10n.
* @param {[Date} date The date to be displayed.
*/
_changeDate: function (date) {
var dateTimeFormat = new navigator.mozL10n.DateTimeFormat();
navigator.mozL10n.formatValue('dateFormat').then(function (format) {
var formatted = dateTimeFormat.localeFormat(date, format);
this.elements.today.dataset.l10nArgs =
JSON.stringify({ 'date': formatted });
}.bind(this));
}
};
exports.DigitalClock = DigitalClock;
})(window);

View file

@ -0,0 +1,18 @@
{
"name": "tv-dashboard",
"tags": {
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["tv_apps/dashboard/js/**/*.js"]
},
"plugins": [
"plugins/markdown",
"plugins/summarize"
],
"opts": {
"private": true,
"recurse": true,
"destination": "docs/tv-dashboard"
}
}

View file

@ -0,0 +1,26 @@
dateFormat = %A, %b %e
today-is=Today is {{date}}
greeting-at-0=Good Evening
greeting-at-1=Good Evening
greeting-at-2=Good Evening
greeting-at-3=Good Evening
greeting-at-4=Good Morning
greeting-at-5=Good Morning
greeting-at-6=Good Morning
greeting-at-7=Good Morning
greeting-at-8=Good Morning
greeting-at-9=Good Morning
greeting-at-10=Good Morning
greeting-at-11=Good Morning
greeting-at-12=Good Afternoon
greeting-at-13=Good Afternoon
greeting-at-14=Good Afternoon
greeting-at-15=Good Afternoon
greeting-at-16=Good Afternoon
greeting-at-17=Good Afternoon
greeting-at-18=Good Afternoon
greeting-at-19=Good Afternoon
greeting-at-20=Good Evening
greeting-at-21=Good Evening
greeting-at-22=Good Evening
greeting-at-23=Good Evening

View file

@ -0,0 +1,27 @@
{
"name": "Dashboard",
"description": "Smart Screen Dashboard ",
"launch_path": "/index.html",
"type": "certified",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"en-US": {
"name": "Dashboard",
"description": "Smart Screen Dashboard"
}
},
"default_locale": "en-US",
"icons": {
"336": "/style/icons/dashboard_336.png",
"672": "/style/icons/dashboard_672.png"
},
"orientation": "landscape",
"permissions": {
"browser": {},
"embed-apps": {},
"webapps-manage": {}
}
}

View file

@ -0,0 +1,306 @@
html {
font-size: 10px;
/* We have Fira Sans as default font in real device. But it isn't default in
* browser. That's the main reason this line is here.*/
font-family: "Fira Sans";
}
html, body {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
body {
transform-style: preserve-3d;
perspective: 25rem;
}
a {
outline: 0;
}
a:hover, a:active, a:focus {
outline: 0;
}
#main-section {
position: absolute;
width: 100%;
height: 100%;
transition: transform 1s cubic-bezier(0.5, 0.0, 0.0, 1.0);
}
/* Activate widgets */
body[data-active-direction="up"] #main-section {
transform: translateY(25rem);
}
body[data-active-direction="right"] #main-section {
transform: translateX(-32.8rem);
}
body[data-active-direction="down"] #main-section {
transform: translateY(-28.2rem);
}
body[data-active-direction="left"] #main-section {
transform: translateX(39rem);
}
body[data-active-direction="up"] #center-panel,
body[data-active-direction="right"] #center-panel,
body[data-active-direction="down"] #center-panel,
body[data-active-direction="left"] #center-panel {
opacity: 0.5;
}
body[data-active-direction="up"] #right-panel,
body[data-active-direction="up"] #bottom-panel,
body[data-active-direction="up"] #left-panel,
body[data-active-direction="right"] #top-panel,
body[data-active-direction="right"] #bottom-panel,
body[data-active-direction="right"] #left-panel,
body[data-active-direction="down"] #top-panel,
body[data-active-direction="down"] #right-panel,
body[data-active-direction="down"] #left-panel,
body[data-active-direction="left"] #top-panel,
body[data-active-direction="left"] #right-panel,
body[data-active-direction="left"] #bottom-panel {
opacity: 0;
}
.background {
position: fixed;
top: -14.1rem;
left: -16.4rem;
width: calc(100% + 36.2rem);
height: calc(100% + 26.6rem);
background-image: url(images/Default_wallpaper_02.jpg);
background-position: center;
background-size: cover;
transition: transform 1s cubic-bezier(0.5, 0.0, 0.0, 1.0);
}
body[data-active-direction="up"] .background {
transform: translateY(-12.5rem);
}
body[data-active-direction="right"] .background {
transform: translateX(16.4rem);
}
body[data-active-direction="down"] .background {
transform: translateY(14.1rem);
}
body[data-active-direction="left"] .background {
transform: translateX(-19.6rem);
}
.background-mask {
position: fixed;
width: 100%;
height: 100%;
background: linear-gradient(to right, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 74%, rgba(0,0,0,0.3) 100%),
linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0) 46%, rgba(0,0,0,0) 54%, rgba(0,0,0,0.3) 100%);
}
#center-panel,
#top-panel,
#right-panel,
#bottom-panel,
#left-panel {
position: absolute;
transition: opacity 1s cubic-bezier(0.5, 0.0, 0.0, 1.0);
}
/* center-panel */
#center-panel {
width: 100%;
height: 100%;
}
/* digital clock */
#digital-clock {
position: absolute;
top: 50%;
left: 50%;
margin: -20.9rem 0 0 -57.5rem;
width: 115rem;
height: 41.8rem;
color: white;
font-weight: 100;
text-align: center;
}
#digital-clock .time {
display: flex;
justify-content: center;
margin: 0;
font-size: 34rem;
line-height: 34rem;
white-space: nowrap;
}
#digital-clock .time span {
width: 17rem;
}
#digital-clock .time span.colons {
position: relative;
top: -3rem;
width: 6rem;
}
#digital-clock .date {
position: relative;
margin-top: 4.4rem;
font-size: 3.4rem;
line-height: 3.4rem;
overflow: hidden;
}
#today-is {
position: relative;
/*top: -4rem;*/
}
#greeting {
position: relative;
top: 4rem;
}
/* top-panel */
#top-panel {
width: 100%;
height: 34.8rem;
top: -25rem;
}
/* right-panel */
#right-panel {
width: 48rem;
height: 100%;
right: -32.8rem;
}
/* bottom-panel */
#bottom-panel {
width: 100%;
height: 44.2rem;
bottom: -28.2rem;
}
/* left-panel */
#left-panel {
width: 58.2rem;
height: 100%;
left: -39rem;
}
iframe {
border: none;
width: 100%;
height: 100%;
}
/* Intro animation */
/*
* Animation for background.
*/
.active .background {
animation: shrink 1s cubic-bezier(0.0, 0.0, 0.0, 1.0) 1.2s;
}
@keyframes shrink {
from { transform: translateZ(10rem); }
to { transform: translateZ(0); }
}
/*
* Animations for greetings and date.
*/
/* XXX: 'both' will keep the animation at running state, not sure if it'll affect performance. ; */
/* Come back to this if there's a performance issue. */
.active #today-is {
animation: slidedown 0.3s cubic-bezier(0.0, 0.0, 0.0, 1.0) 4.5s both;
}
.active #greeting {
animation: slideupdown 2.6s cubic-bezier(0.0, 0.0, 0.0, 1.0) 2.2s both;
}
@keyframes slidedown {
from {
top: -4rem;
}
to {
top: 0;
}
}
@keyframes slideupdown {
0% {
top: 4rem;
}
11.54% {
top: -3.4rem;
}
88.46% {
top: -3.4rem;
}
100% { top: 4rem;}
}
/*
* The following rules set transform origin to central point of the screen.
* For the formula calc(XXrem +/- 50vh/vw), they locate to respective screen
* edges by the former term XXrem, then componsate 50vh/50vw for centeral point.
*/
.active #bottom-panel {
/* 16rem is the height of its preview area. */
transform-origin: 50vw calc(16rem - 50vh);
animation: zoomin 1s cubic-bezier(0.0, 0.0, 0.0, 1.0) 1.2s both;
}
.active #top-panel {
/* 25rem is the height of its hidden area. */
transform-origin: 50vw calc(25rem + 50vh);
animation: zoomin 1s cubic-bezier(0.0, 0.0, 0.0, 1.0) 1.2s both;
}
.active #left-panel {
/* 39rem is the width of its hidden area. */
transform-origin: calc(39rem + 50vw) 50vh;
animation: zoomin 1s cubic-bezier(0.0, 0.0, 0.0, 1.0) 1.2s both;
}
.active #right-panel {
/* 15.2rem is the width of its preview area. */
transform-origin: calc(15.2rem - 50vw) 50vh;
animation: zoomin 1s cubic-bezier(0.0, 0.0, 0.0, 1.0) 1.2s both;
}
@keyframes zoomin {
from {
transform: scale(1.7);
}
to {
transform: scale(1);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View file

@ -0,0 +1,14 @@
{
"extends": "../../../../.jshintrc",
"node": true,
"predef": [
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished"
]
}

View file

@ -0,0 +1,123 @@
'use strict';
var APP_URL = 'app://dashboard.gaiamobile.org';
var Keys = {
'up': '\ue013',
'right': '\ue014',
'down': '\ue015',
'left': '\ue012'
};
var assert = require('chai').assert;
// Skip because the dashboard is not needed app now.
// Should re-enable the test when we need it.
marionette.skip('Test Arrow Key Events', function() {
var opts = {
hostOptions: {
screen: {
width: 1920,
height: 1080
}
}
};
var client = marionette.client({
profile: opts,
// XXX: Set this to true once Accessibility is implemented in TV
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
// Launch test app
client.apps.launch(APP_URL);
client.apps.switchToApp(APP_URL);
});
test('Display and hide top widget after pressing up/down',
{ devices: ['tv'] }, function () {
var element = client.helper.waitForElement('body');
element.sendKeys(Keys.up);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'up');
element.sendKeys(Keys.down);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), '');
});
test('Display and hide right widget after pressing right/left',
{ devices: ['tv'] }, function () {
var element = client.helper.waitForElement('body');
element.sendKeys(Keys.right);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'right');
element.sendKeys(Keys.left);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), '');
});
test('Display and hide bottom widget after pressing down/up',
{ devices: ['tv'] }, function () {
var element = client.helper.waitForElement('body');
element.sendKeys(Keys.down);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'down');
element.sendKeys(Keys.up);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), '');
});
test('Display and hide left widget after pressing left/right',
{ devices: ['tv'] }, function () {
var element = client.helper.waitForElement('body');
element.sendKeys(Keys.left);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'left');
element.sendKeys(Keys.right);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), '');
});
test('Doesn\'t hide top widgets after pressing up when top widget is visible',
{ devices: ['tv'] }, function () {
var element = client.helper.waitForElement('body');
// test top widget
element.sendKeys(Keys.up);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'up');
element.sendKeys(Keys.up);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'up');
});
test('Doesn\'t hide top widgets after pressing right or left ' +
'when top widget is visible', { devices: ['tv'] }, function () {
var element = client.helper.waitForElement('body');
// test top widget
element.sendKeys(Keys.up);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'up');
element.sendKeys(Keys.right);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'up');
element.sendKeys(Keys.left);
assert.equal(element.scriptWith(function(elem) {
return elem.dataset.activeDirection;
}), 'up');
});
});

View file

@ -0,0 +1,109 @@
'use strict';
var APP_URL = 'app://dashboard.gaiamobile.org';
var assert = require('chai').assert;
// Skip because the dashboard is not needed app now.
// Should re-enable the test when we need it.
marionette.skip('Test Digital Clock', function() {
var opts = {
hostOptions: {
screen: {
width: 1920,
height: 1080
}
}
};
var client = marionette.client({ profile: opts });
setup(function() {
// Launch test app
client.apps.launch(APP_URL);
client.apps.switchToApp(APP_URL);
});
test('Should display the current time',
{ devices: ['tv'] }, function () {
var element = client.helper.waitForElement('body');
var hoursTens = element.findElement('#hours-tens-digit');
assert.ok(hoursTens.scriptWith(function(elem) {
var now = new Date();
return parseInt(elem.textContent, 10) ===
Math.floor(now.getHours() / 10);
}));
var hoursUnits = element.findElement('#hours-units-digit');
assert.ok(hoursUnits.scriptWith(function(elem) {
var now = new Date();
return parseInt(elem.textContent, 10) === now.getHours() % 10;
}));
var minutesTens = element.findElement('#minutes-tens-digit');
assert.ok(minutesTens.scriptWith(function(elem) {
var now = new Date();
return parseInt(elem.textContent, 10) ===
Math.floor(now.getMinutes() / 10);
}));
var minutesUnits = element.findElement('#minutes-units-digit');
assert.ok(minutesUnits.scriptWith(function(elem) {
var now = new Date();
return parseInt(elem.textContent, 10) === now.getMinutes() % 10;
}));
var secondsTens = element.findElement('#seconds-tens-digit');
assert.ok(secondsTens.scriptWith(function(elem) {
var now = new Date();
return parseInt(elem.textContent, 10) ===
Math.floor(now.getSeconds() / 10);
}));
var secondsUnits = element.findElement('#seconds-units-digit');
assert.ok(secondsUnits.scriptWith(function(elem) {
var now = new Date();
return parseInt(elem.textContent, 10) === now.getSeconds() % 10;
}));
});
test('Should display the month, date and weekday info',
{ devices: ['tv'] }, function () {
var element = client.helper.waitForElement('body');
var today = element.findElement('#today-is');
assert.ok(today.scriptWith(function(elem) {
var weekday = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
];
var month = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
var now = new Date();
var dateString = 'Today is ' + weekday[now.getDay()] + ', ' +
month[now.getMonth()] + ' ' + now.getDate();
return elem.textContent === dateString;
}));
});
});

View file

@ -0,0 +1,18 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"assert",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"require",
"requireApp",
"sinon",
"mocha",
"loadBodyHTML"
]
}

View file

@ -0,0 +1,70 @@
/* globals AppWidget */
require('/js/app_widget.js');
suite('AppWidget', function() {
'use strict';
var subject;
var frameData = {
manifestURL: 'url1',
widget: 'widgetName1',
url: 'about:blank',
position: 'up'
};
var upPanel;
setup(function() {
upPanel = document.createElement('div');
upPanel.id = 'up-panel';
document.body.appendChild(upPanel);
subject = new AppWidget(frameData);
});
teardown(function() {
document.body.removeChild(upPanel);
});
suite('creation > ', function() {
test('should init frame inside up-panel', function() {
assert.equal(upPanel.firstChild, subject.iframe);
});
test('should init frame with appropriate mozapp attributes', function() {
var iframe = subject.iframe;
assert.equal(iframe.getAttribute('mozbrowser'), 'true');
assert.equal(iframe.getAttribute('remote'), 'true');
assert.equal(iframe.getAttribute('mozapp'), frameData.manifestURL);
assert.equal(iframe.src, frameData.url);
});
});
suite('toggleExpand() >', function() {
setup(function() {
subject.toggleExpand(true);
});
test('should add hash "expand" when expanding', function() {
var hash = subject.iframe.src.split('#')[1];
assert.equal(hash, 'expand');
});
test('should remove hash "expand" when shrinking', function() {
subject.toggleExpand(false);
// We have to confirm toggleExpand leaves a '#' in url to prevent reload.
// (thus split() shoul return an array of two strings).
var hash = subject.iframe.src.split('#')[1];
assert.equal(hash, '');
});
});
suite('focus() > ', function () {
test('should focus its iframe', function() {
var spy = this.sinon.spy(subject.iframe, 'focus');
subject.focus();
assert.isTrue(spy.calledOnce);
});
});
});

View file

@ -0,0 +1,147 @@
'use strict';
/* global Dashboard, MocksHelper */
require('/bower_components/evt/index.js');
require('/shared/js/smart-screen/key_navigation_adapter.js');
require('mock_app_widget.js');
require('mock_digital_clock.js');
require('/js/dashboard.js');
var MocksHelperForUnitTest = new MocksHelper([
'AppWidget',
'DigitalClock'
]).init();
suite('Dashboard', function() {
var dashboard;
var mainSection;
var clock;
MocksHelperForUnitTest.attachTestHelpers();
setup(function() {
clock = this.sinon.useFakeTimers();
mainSection = document.createElement('div');
mainSection.id = 'main-section';
document.body.appendChild(mainSection);
dashboard = new Dashboard();
dashboard.init();
});
teardown(function() {
document.body.removeChild(mainSection);
});
test('should initialize keyNavigationAdapter and digitalClock', function() {
assert.isDefined(dashboard.keyNavigationAdapter);
assert.isDefined(dashboard.digitalClock);
});
test('should change body dataset.activeDirection ' +
'to up when dashboard.onMove is called with up', function() {
dashboard.onMove('up');
assert.equal(document.body.dataset.activeDirection, 'up');
dashboard.onMove('down');
assert.equal(document.body.dataset.activeDirection, '');
});
test('should change body dataset.activeDirection ' +
'to right when dashboard.onMove is called with right', function() {
dashboard.onMove('right');
assert.equal(document.body.dataset.activeDirection, 'right');
dashboard.onMove('left');
assert.equal(document.body.dataset.activeDirection, '');
});
test('should change body dataset.activeDirection ' +
'to down when dashboard.onMove is called with down', function() {
dashboard.onMove('down');
assert.equal(document.body.dataset.activeDirection, 'down');
dashboard.onMove('up');
assert.equal(document.body.dataset.activeDirection, '');
});
test('should change body dataset.activeDirection ' +
'to left when dashboard.onMove is called with left', function() {
dashboard.onMove('left');
assert.equal(document.body.dataset.activeDirection, 'left');
dashboard.onMove('right');
assert.equal(document.body.dataset.activeDirection, '');
});
test('should not change body dataset.activeDirection ' +
'when activeDirection is up and dashboard.onMove is ' +
'called with up',function () {
dashboard.onMove('up');
assert.equal(document.body.dataset.activeDirection, 'up');
dashboard.onMove('up');
assert.equal(document.body.dataset.activeDirection, 'up');
});
test('should not change body dataset.activeDirection ' +
'when activeDirection is up and dashboard.onMove is ' +
'called with right or left',function () {
dashboard.onMove('up');
assert.equal(document.body.dataset.activeDirection, 'up');
dashboard.onMove('right');
assert.equal(document.body.dataset.activeDirection, 'up');
dashboard.onMove('left');
assert.equal(document.body.dataset.activeDirection, 'up');
});
test('should expand bottom widget when moving down,' +
' then shrink when moving back', function() {
dashboard.onMove('down');
assert.isTrue(
dashboard.widgets.down.toggleExpand.withArgs(true).calledOnce);
dashboard.onMove('up');
assert.isTrue(
dashboard.widgets.down.toggleExpand.withArgs(false).calledOnce);
});
test('should focus on bottom widget when moving down', function () {
var mainSectionFocusSpy = this.sinon.spy(mainSection, 'focus');
dashboard.onMove('down');
assert.isTrue(mainSectionFocusSpy.calledOnce);
clock.tick();
assert.isTrue(dashboard.widgets.down.focus.calledOnce);
});
suite('visibilityChange >', function() {
function setFakeVisibility(value) {
Object.defineProperty(document, 'visibilityState', {
get: function(){
return value;
},
configurable: true
});
}
teardown(function() {
delete document.visibilityState;
});
test('should move to center if visibility becomes hidden', function() {
dashboard.onMove('down');
setFakeVisibility('hidden');
var mainSectionFocusSpy = this.sinon.spy(mainSection, 'focus');
dashboard.onVisibilityChange();
assert.equal(document.body.dataset.activeDirection, '');
assert.isTrue(mainSectionFocusSpy.calledOnce);
});
test('should toggle body class "expand" with visibility', function() {
setFakeVisibility('hidden');
dashboard.onVisibilityChange();
assert.isFalse(document.body.classList.contains('active'));
setFakeVisibility('visible');
dashboard.onVisibilityChange();
assert.isTrue(document.body.classList.contains('active'));
});
});
});

View file

@ -0,0 +1,111 @@
'use strict';
/* global DigitalClock, MockL10n */
// XXX: The mocked formatValue method in mock_l10n.js returns Promise.resolve
// instead of a Promise, making it difficult to use the mock_promise.js
// to test. So here the test code is commented out
// until the mock_l10n.js is changed.
//
// require('/shared/test/unit/mocks/mock_promise.js');
require('/shared/test/unit/mocks/mock_l10n.js');
require('/js/digital_clock.js');
suite('DigitalClock', function() {
var clock;
var digitalClock;
var startDate;
var realL10n;
function createElement(tagName, id) {
var element = document.createElement(tagName);
element.id = id;
return element;
}
var elements = {
hoursTensDigit: createElement('span', 'hours-tens-digit'),
hoursUnitsDigit: createElement('span', 'hours-units-digit'),
minutesTensDigit: createElement('span', 'minutes-tens-digit'),
minutesUnitsDigit: createElement('span', 'minutes-units-digit'),
secondsTensDigit: createElement('span', 'seconds-tens-digit'),
secondsUnitsDigit: createElement('span', 'seconds-units-digit'),
today: createElement('span', 'today-is'),
greeting: createElement('span', 'greeting')
};
suiteSetup(function() {
realL10n = window.navigator.mozL10n;
window.navigator.mozL10n = MockL10n;
for (var prop in elements) {
document.body.appendChild(elements[prop]);
}
});
suiteTeardown(function() {
var elements = document.getElementsByTagName('span');
for (var i=0; i < elements.length; i++) {
document.body.removeChild(elements[i]);
}
window.navigator.mozL10n = realL10n;
});
setup(function() {
// this.sinon.stub(window, 'Promise', MockPromise);
// this.sinon.spy(window.Promise, 'resolve');
startDate = new Date(2015, 4, 31);
clock = this.sinon.useFakeTimers(startDate.getTime());
digitalClock = new DigitalClock();
digitalClock.init();
});
teardown(function() {
// window.Promise.restore();
digitalClock.stop();
clock.restore();
});
test('Should display current date and time once the clock is initialized',
function() {
clock.tick();
assert.equal(digitalClock.date.getTime(), startDate.getTime());
assert.equal(elements.hoursTensDigit.textContent, '0');
assert.equal(elements.hoursUnitsDigit.textContent, '0');
assert.equal(elements.minutesTensDigit.textContent, '0');
assert.equal(elements.minutesUnitsDigit.textContent, '0');
assert.equal(elements.secondsTensDigit.textContent, '0');
assert.equal(elements.secondsUnitsDigit.textContent, '0');
// var p0 = window.Promise.resolve.firstCall.args[0];
// Using JSON.stringify here to match the result from the mock l10n.
// assert.equal(elements.today.dataset.l10nArgs,
// JSON.stringify({'date': JSON.stringify(new Date()) + 'dateFormat'}));
});
test('Should clear timers after the clock is stopped', function() {
digitalClock.stop();
assert.isNull(digitalClock.timeoutId);
});
test('Should update date and time after the clock starts', function() {
var now;
digitalClock.start();
// Set the clock to 1 day, 1 hour, 1 minute and 1 second later
// after initial date.
clock.tick(86400000);
now = new Date();
// var p0 = window.Promise.resolve.firstCall.args[0];
// assert.equal(elements.today.dataset.l10nArgs,
// JSON.stringify({'date': JSON.stringify(now) + 'dateFormat'}));
// Set the clock to 1 hour, 1 minute and 1 second later
clock.tick(3600000 + 60000 + 1000);
now = new Date();
assert.equal(digitalClock.date.getTime(), now.getTime());
assert.equal(elements.hoursUnitsDigit.textContent, '1');
assert.equal(elements.minutesUnitsDigit.textContent, '1');
assert.equal(elements.secondsUnitsDigit.textContent, '1');
});
});

View file

@ -0,0 +1,10 @@
/* globals sinon */
'use strict';
var MockAppWidget = function(config) {
this.toggleExpand = sinon.stub();
this.focus = sinon.stub();
};
MockAppWidget.prototype = {
};

View file

@ -0,0 +1,10 @@
'use strict';
var MockDigitalClock = function() {
this.init = sinon.stub();
this.start = sinon.stub();
this.stop = sinon.stub();
this.generateGreeting = sinon.stub();
};
MockDigitalClock.prototype = {};

View file

@ -0,0 +1,3 @@
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');