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,54 @@
'use strict';
/* global utils, BookmarkEditor, BookmarkRemover */
var ActivityHandler = {
'save-bookmark': function ah_save(activity) {
BookmarkEditor.init({
data: activity.source.data,
onsaved: function onsaved(saved) {
window.addEventListener('status-hidden', function hidden() {
window.removeEventListener('status-hidden', hidden);
activity.postResult(saved ? 'saved' : 'updated');
});
var msg = saved ?
'added-to-home-screen-message' : 'updated-pinned-site';
navigator.mozL10n.formatValue(msg)
.then(msg => utils.status.show(msg));
},
oncancelled: function oncancelled() {
activity.postError('cancelled');
}
});
},
'remove-bookmark': function ah_remove(activity) {
BookmarkRemover.init({
id: activity.source.data.url,
onremoved: function onremoved() {
activity.postResult('removed');
},
oncancelled: function oncancelled(e) {
activity.postError(e);
}
});
}
};
navigator.mozSetMessageHandler('activity', function onActivity(activity) {
var name = activity.source.name;
switch (name) {
case 'save-bookmark':
case 'remove-bookmark':
if (activity.source.data.type === 'url') {
ActivityHandler[name](activity);
} else {
activity.postError('type not supported');
}
break;
default:
activity.postError('name not supported');
}
});

View file

@ -0,0 +1,175 @@
'use strict';
/* global UrlHelper, BookmarksDatabase, Icon */
/* exported BookmarkEditor */
var BookmarkEditor = {
BOOKMARK_ICON_SIZE: 60,
APP_ICON_SIZE: 60,
init: function bookmarkEditor_show(options) {
this.data = options.data;
this.onsaved = options.onsaved;
this.oncancelled = options.oncancelled;
var mode = 'add';
BookmarksDatabase.get(this.data.url).then((function got(bookmark) {
if (bookmark) {
this.data = bookmark;
mode = 'put';
}
this._init(mode);
}).bind(this), this._init.bind(this, mode));
},
_init: function bookmarkEditor_init(mode) {
this.mode = document.body.dataset.mode = mode;
this.bookmarkTitle = document.getElementById('bookmark-title');
this.bookmarkURL = document.getElementById('bookmark-url');
this.bookmarkIcon = document.getElementById('bookmark-icon');
this.header = document.getElementById('header');
this.saveButton = document.getElementById('done-button');
this.appInstallationSection = document.getElementById('app-installation');
this.appIcon = document.getElementById('app-icon');
this.appIconPlaceholder = document.getElementById('app-icon-placeholder');
this.appNameText = document.getElementById('app-name');
this.installAppButton = document.getElementById('install-app-button');
this.header.addEventListener('action', this.close.bind(this));
this.saveListener = this.save.bind(this);
this.saveButton.addEventListener('click', this.saveListener);
this.bookmarkTitle.value = this.data.name || '';
this.bookmarkURL.textContent = this.data.url;
this._renderIcon();
if (this.data.manifestURL) {
this.manifestURL = this.data.manifestURL;
this._fetchManifest(this.manifestURL);
}
this._checkDoneButton();
this.form = document.getElementById('bookmark-form');
this.form.addEventListener('input', this._checkDoneButton.bind(this));
this.form.addEventListener('submit', this._submit.bind(this));
var touchstart = 'ontouchstart' in window ? 'touchstart' : 'mousedown';
this.clearButton = document.getElementById('bookmark-title-clear');
this.clearButton.addEventListener(touchstart, this._clearTitle.bind(this));
if (mode === 'put') {
this._onEditMode();
this.saveButton.setAttribute('data-l10n-id', 'done-action');
} else {
this.saveButton.setAttribute('data-l10n-id', 'add-action');
}
// We're appending new elements to DOM so to make sure headers are
// properly resized and centered, we emmit a lazyload event.
// This will be removed when the gaia-header web component lands.
window.dispatchEvent(new CustomEvent('lazyload', {
detail: document.body
}));
},
_renderIcon: function renderIcon() {
var icon = new Icon(this.bookmarkIcon, this.data.icon);
icon.render({'size': this.BOOKMARK_ICON_SIZE});
},
_renderAppIcon: function renderAppIcon(manifest, size) {
// Parse icon URL from app manifest
var iconURL = window.IconsHelper.getBestIconFromWebManifest(manifest, size);
if (!iconURL) {
return;
}
// Switch out placeholder for real icon when it loads
this.appIconListener = this._handleAppIconLoad.bind(this);
this.appIcon.addEventListener('load', this.appIconListener);
this.appIcon.setAttribute('src', iconURL);
},
_handleAppIconLoad: function _handleAppIconLoad() {
this.appIconPlaceholder.classList.add('hidden');
this.appIcon.classList.remove('hidden');
this.appIcon.removeEventListener('load', this.appIconListener);
},
_fetchManifest: function bookmarkEditor_fetchManifest(manifestURL) {
var manifestPromise = window.WebManifestHelper.getManifest(manifestURL);
manifestPromise.then((function(manifestData) {
if (manifestData) {
this.installAppButtonListener = this._installApp.bind(this);
this.installAppButton.addEventListener('click',
this.installAppButtonListener);
this.appNameText.textContent = manifestData.short_name ||
manifestData.name;
this.appInstallationSection.classList.remove('hidden');
this._renderAppIcon(manifestData, manifestURL, this.APP_ICON_SIZE);
}
}).bind(this)).catch(function(error) {
console.error('Unable to get web manifest: ' + error);
});
return manifestPromise;
},
_onEditMode: function bookmarkEditor_onEditMode() {
// Done button will be disabled on edit mode once it is displayed
this.saveButton.disabled = true;
},
close: function bookmarkEditor_close() {
this.oncancelled();
},
/**
* Handles the submit case for the form when the user presses the enter key.
* @param {Event} event The form submit event.
*/
_submit: function(event) {
event.preventDefault();
if (!this.saveButton.disabled) {
this.save();
}
},
_clearTitle: function bookmarkEditor_clearTitle(event) {
event.preventDefault();
this.bookmarkTitle.value = '';
this._checkDoneButton();
},
_checkDoneButton: function bookmarkEditor_checkDoneButton() {
// If one of the fields is blank, the “Done” button should be dimmed and
// inactive
var title = this.bookmarkTitle.value.trim();
this.saveButton.disabled = title === '';
},
_installApp: function bookmarkEditor_installApp() {
window.navigator.mozApps.install(this.manifestURL);
},
save: function bookmarkEditor_save(evt) {
this.saveButton.removeEventListener('click', this.saveListener);
if (this.installAppButtonListener) {
this.installAppButton.removeEventListener('click',
this.installAppButtonListener);
}
// Only allow urls to be bookmarked.
// This is defensive check - callers should filter out non-URLs.
var url = this.data.url.trim();
if (UrlHelper.isNotURL(url)) {
this.oncancelled();
return;
}
this.data.name = this.bookmarkTitle.value;
this.data.url = url;
BookmarksDatabase[this.mode](this.data).then(this.onsaved.bind(this),
this.close.bind(this));
}
};

View file

@ -0,0 +1,39 @@
'use strict';
/* global BookmarksDatabase */
/* exported BookmarkRemover */
var BookmarkRemover = {
init: function bookmarkRemover_init(data) {
var id = data.id;
BookmarksDatabase.get(id).then(function got(bookmark) {
if (!bookmark) {
data.oncancelled('bookmark does not exist');
return;
}
// Title and message
var ttl = document.getElementById('title');
var msg = document.getElementById('message');
ttl.setAttribute('data-l10n-id', 'remove-title');
ttl.setAttribute('data-l10n-args',
JSON.stringify({ name: bookmark.name }));
msg.setAttribute('data-l10n-id', 'remove-message');
msg.setAttribute('data-l10n-args',
JSON.stringify({ name: bookmark.name }));
// Action handlers
document.getElementById('cancel-action').addEventListener('click',
function onCancel() {
data.oncancelled('cancelled');
}
);
document.getElementById('remove-action').addEventListener('click',
function onConfirm(evt) {
evt.target.removeEventListener('click', onConfirm);
BookmarksDatabase.remove(id).then(data.onremoved, data.oncancelled);
}
);
}, data.oncancelled);
}
};

View file

@ -0,0 +1,32 @@
section[role="status"] {
animation-name: hide;
animation-duration: .3s;
animation-fill-mode: forwards;
}
section[role="status"].onviewport {
animation-name: show;
}
section[role="status"].hidden {
display: none;
}
@keyframes show {
from {
transform: translateY(100%)
}
to {
transform: translateY(0)
}
}
@keyframes hide {
from {
transform: translateY(0)
}
to {
transform: translateY(100%)
}
}

View file

@ -0,0 +1,177 @@
'use strict';
window.utils = window.utils || {};
window.utils.status = (function() {
// How many milliseconds is displayed the status component by default
var DISPLAYED_TIME = 1500;
// References to the DOMElement(s) that renders the status UI component
var section, content;
// The numerical ID of the timeout in order to hide UI component
var timeoutID;
/*
* Clears the callback in charge of hiding the component after timeout
*/
function clearHideTimeout() {
if (timeoutID === null) {
return;
}
window.clearTimeout(timeoutID);
timeoutID = null;
}
/*
* Shows the status component
*
* @param{Object} Message. It could be a string or a DOMFragment that
* represents the normal and strong strings
*
* @param{int} It defines the time that the status is displayed in ms. This
* parameter is optional
*
*/
function show(message, duration) {
clearHideTimeout();
content.innerHTML = '';
if (typeof message === 'string') {
content.textContent = message;
} else {
try {
// Here we should have a DOMFragment
content.appendChild(message);
} catch (ex) {
console.error('DOMException: ' + ex.message);
}
}
section.classList.remove('hidden');
section.classList.add('onviewport');
timeoutID = window.setTimeout(hide, duration || DISPLAYED_TIME);
}
/*
* This function is invoked when some animation is ended
*/
function animationEnd(evt) {
var eventName = 'status-showed';
if (evt.animationName === 'hide') {
clearHideTimeout();
section.classList.add('hidden');
eventName = 'status-hidden';
}
window.dispatchEvent(new CustomEvent(eventName));
}
/*
* Hides the status component
*/
function hide() {
section.classList.remove('onviewport');
}
/*
* Releases memory
*/
function destroy() {
section.removeEventListener('animationend', animationEnd);
document.body.removeChild(section);
clearHideTimeout();
section = content = null;
}
function getPath() {
return '/js/components/';
}
/*
* Initializes the library. Basically it creates the markup:
*
* <section role="status">
* <p>xxx</p>
* </section>
*/
function initialize() {
if (section) {
return;
}
section = document.createElement('section');
var link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = getPath() + 'status-behavior.css';
document.head.appendChild(link);
section.setAttribute('role', 'status');
section.classList.add('hidden');
content = document.createElement('p');
section.appendChild(content);
section.addEventListener('animationend', animationEnd);
// to prevent flickering
setTimeout(function append() {
document.body.appendChild(section);
});
}
// Initializing the library
if (document.readyState === 'complete') {
initialize();
} else {
document.addEventListener('DOMContentLoaded', function loaded() {
document.removeEventListener('DOMContentLoaded', loaded);
initialize();
});
}
return {
/*
* The library is auto-initialized but it is for unit testing purposes
*/
init: initialize,
/*
* Shows the status component
*
* @param{Object} Message. It could be a string or a DOMFragment that
* represents the normal and strong strings
*
* @param{int} It defines the time that the status is displayed in ms
*
*/
show: show,
/*
* Hides the status component
*/
hide: hide,
/*
* Releases memory
*/
destroy: destroy,
/*
* Sets up the duration in milliseconds that a status is displayed
*
* @param{int} The time in milliseconds
*
*/
setDuration: function setDuration(time) {
DISPLAYED_TIME = time || DISPLAYED_TIME;
}
};
})();

18
gaia/apps/bookmark/jsdoc.json Executable file
View file

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

View file

@ -0,0 +1,19 @@
# Add and update pinned websites UIs
add-to-home-screen-title=Add to Home Screen
edit-pinned-site-title=Edit Pinned Site
added-to-home-screen-message=Added to home screen
updated-pinned-site=Updated pinned site
done-action=Done
add-action=Add
cancel-action=Cancel
add-application=Add Application
add=Add
application-available=An application is available for this web site.
# Remove pinned websites UI
remove-title=Remove {{name}}
remove-message=Remove {{name}} from system?
remove-action=Remove

View file

@ -0,0 +1,59 @@
{
"name": "Bookmark",
"description": "Bookmark Manager",
"type": "certified",
"role": "system",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"permissions": {
"themeable": {},
"systemXHR": {},
"moz-extremely-unstable-and-will-change-webcomponents": {}
},
"activities": {
"save-bookmark": {
"filters": {
"type": "url",
"url": {
"required": true,
"pattern": "(https?|rtsp|data):.{1,16384}"
}
},
"disposition": "inline",
"href": "/save.html",
"returnValue": true
},
"remove-bookmark": {
"filters": {
"type": "url",
"url": {
"required": true,
"pattern": "(https?|rtsp|data):.{1,16384}"
}
},
"disposition": "inline",
"href": "/remove.html",
"returnValue": true
}
},
"locales": {
"en-US": {
"name": "Bookmark",
"description": "Bookmark Manager"
},
"fr": {
"name": "Marque-pages",
"description": "Gestionnaire de marque-pages"
}
},
"default_locale": "en-US",
"datastores-owned": {
"bookmarks_store": {
"access": "readwrite",
"certified-only": false,
"description": "Stores bookmarks"
}
}
}

10
gaia/apps/bookmark/package.json Executable file
View file

@ -0,0 +1,10 @@
{
"webide": {
"prepackage": {
"command": "make",
"env": ["APP=bookmark"],
"cwd": "../.."
},
"packageDir": "../../build_stage/bookmark/"
}
}

32
gaia/apps/bookmark/remove.html Executable file
View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style/common.css">
<link rel="stylesheet" type="text/css" href="shared/style/confirm.css">
<script type="text/javascript" src="shared/js/l10n.js"></script>
<script type="text/javascript" defer src="shared/js/bookmarks_database.js"></script>
<script type="text/javascript" defer src="js/bookmark_remover.js"></script>
<script type="text/javascript" defer src="js/activity_handler.js"></script>
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/bookmark.{locale}.properties">
</head>
<body>
<form role="dialog" data-type="confirm">
<section>
<h1 id="title"></h1>
<p id="message"></p>
</section>
<menu>
<button id="cancel-action" type="button" data-l10n-id="cancel-action"></button>
<button id="remove-action" type="button" data-l10n-id="remove-action" class="danger"></button>
</menu>
</form>
</body>
</html>

64
gaia/apps/bookmark/save.html Executable file
View file

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style/common.css">
<link rel="stylesheet" type="text/css" href="style/save.css">
<!-- Shared code -->
<script type="text/javascript" src="shared/js/l10n.js"></script>
<script type="text/javascript" defer src="shared/js/url_helper.js"></script>
<script type="text/javascript" defer src="shared/js/bookmarks_database.js"></script>
<script type="text/javascript" defer src="shared/js/web_manifest_helper.js"></script>
<script type="text/javascript" defer src="shared/js/icons_helper.js"></script>
<script type="text/javascript" defer src="/shared/elements/gaia-header/dist/gaia-header.js"></script>
<link rel="stylesheet" type="text/css" href="shared/style/buttons.css">
<link rel="stylesheet" type="text/css" href="shared/style/headers.css">
<link rel="stylesheet" type="text/css" href="shared/style/input_areas.css">
<link rel="stylesheet" type="text/css" href="shared/style/status.css">
<link rel="stylesheet" type="text/css" href="/shared/elements/gaia-icons/gaia-icons.css">
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css">
<!-- Link to component just to copy the default_icon here -->
<!-- <script defer src="shared/elements/gaia_grid/script.js"></script> -->
<script defer src="shared/elements/gaia_grid/js/grid_icon_renderer.js"></script>
<script defer src="shared/js/homescreens/icon.js"></script>
<!-- Specific code -->
<script type="text/javascript" defer src="js/bookmark_editor.js"></script>
<script type="text/javascript" defer src="js/components/status.js"></script>
<script type="text/javascript" defer src="js/activity_handler.js"></script>
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/bookmark.{locale}.properties">
</head>
<body class="theme-settings">
<section id="bookmark-entry-sheet" role="region" class="skin-organic">
<gaia-header id="header" action="close">
<h1 id="add-bookmark-header" data-l10n-id="add-to-home-screen-title"></h1>
<h1 id="edit-bookmark-header" data-l10n-id="edit-pinned-site-title"></h1>
<button id="done-button" data-l10n-id="add-action"></button>
</gaia-header>
<form id="bookmark-form">
<div id="bookmark-icon"></div>
<p id="bookmark-url" dir="ltr"></p>
<p>
<input type="text" id="bookmark-title" dir="auto"></input>
<button id="bookmark-title-clear" type="reset">Clear</button>
</p>
</form>
<div id="app-installation" class="hidden">
<h2><span data-l10n-id="add-application">Add Application</span></h2>
<img id="app-icon-placeholder" src="../shared/elements/gaia_grid/images/default_icon.png">
<img id="app-icon" class="hidden">
<p id="app-name"></p>
<button id="install-app-button" data-l10n-id="add">Add</button>
<p data-l10n-id="application-available">An application is available for this web site.</p>
</div>
</section>
</body>
</html>

View file

@ -0,0 +1,8 @@
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: none;
font-size: 10px;
}

118
gaia/apps/bookmark/style/save.css Executable file
View file

@ -0,0 +1,118 @@
html, body {
background-color: #f4f4f4;
}
#bookmark-entry-sheet {
width: 100%;
height: 100%;
}
#bookmark-form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 1.5rem;
}
#bookmark-form p {
width: 100%;
}
#add-bookmark-header, #edit-bookmark-header {
display: none;
}
body[data-mode="add"] #add-bookmark-header {
display: block;
}
body[data-mode="put"] #edit-bookmark-header {
display: block;
}
#bookmark-icon {
background-repeat: no-repeat;
background-position: center center;
background-image: url('../shared/elements/gaia_grid/images/default_icon.png');
background-size: 8.4rem;
display: block;
margin-bottom: 2.4rem;
}
#bookmark-url {
font-size: 1.4rem;
color: rgb(95, 95, 95);
width: 100%;
margin-bottom: 1.4rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
box-sizing: border-box;
padding: 0 1.5rem;
font-style: italic;
}
/* App Installation section */
#app-installation {
padding: 1.5rem;
padding-top: 2rem;
}
#app-installation h2 {
text-align: center;
border-top: 0.2rem solid rgb(114, 114, 114);
font-size: 1.4rem;
color: rgb(114, 114, 114);
margin: 0.8rem 0 -0.8rem 0;
}
#app-installation h2 span {
position: relative;
top: -1rem;
background-color: #f4f4f4;
padding: 0 0.5rem;
margin-bottom: 0;
font-weight: normal;
text-transform: uppercase;
}
#app-icon, #app-icon-placeholder {
display: block;
width: 6rem;
height: 6rem;
margin: 0.5rem auto 0 auto;
}
#app-installation p {
font-size: 1.4rem;
color: rgb(80, 88, 89);
margin: 1rem;
}
#app-installation #app-name {
height: 2rem;
font-size: 1.6rem;
color: rgb(41, 41, 41);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0;
text-align: center;
}
#app-installation #install-app-button {
display: block;
width: auto;
margin: 0 auto;
padding: 0 5rem;
}
#app-icon.hidden,
#app-icon-placeholder.hidden,
#app-installation.hidden {
display: none;
}

View file

@ -0,0 +1,27 @@
'use strict';
/* jshint node: true, mocha: true */
/* global suiteSetup */
var helper = require('helper');
suite('Bookmark tests', function() {
suiteSetup(helper.cleanupWorkspace);
teardown(helper.cleanupWorkspace);
test('APP=bookmark make', function(done) {
helper.exec('APP=bookmark make', function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var zipPath = process.cwd() +
'/profile/webapps/bookmark.gaiamobile.org/application.zip';
var checkList = ['manifest.webapp'];
checkList.forEach(function(filePath) {
helper.checkFilePathInZip(zipPath, filePath);
});
done();
});
});
});

View file

@ -0,0 +1,7 @@
/* global module */
module.exports = {
settings: {
'devtools.pseudolocalization.enabled': false,
'language.current': 'en-US'
}
};

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App Title</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<header><h1>App</h1></header>
<section>I am an app</section>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,11 @@
{
"name": "My App Name",
"short_name": "My App Shortname",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64",
"type": "image/png"
}
]
}

View file

@ -0,0 +1,43 @@
'use strict';
/* global __dirname, require, marionette, suiteSetup, suiteTeardown */
/* global setup, test */
var Bookmark = require('../../../../apps/system/test/marionette/lib/bookmark');
var Server = require('../../../../shared/test/integration/server');
marionette('Bookmark -', function() {
var options = require(__dirname + '/client_options.js');
options.settings['dev.gaia.pinning_the_web'] = false;
var client = marionette.client({
profile: options,
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var bookmark, server, system;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
setup(function() {
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
bookmark = new Bookmark(client, server);
});
test('Install app from page', function() {
var url = server.url('app.html');
client.switchToFrame();
bookmark.openAndInstall(url, 'My App Shortname', '/favicon.ico');
});
});

View file

@ -0,0 +1,197 @@
'use strict';
/* global BookmarkEditor, msgHandler, realMozSetMessageHandler, utils,
BookmarkRemover */
/* global requireApp, require, suite, suiteTeardown, suiteSetup, test, assert,
MocksHelper, MockL10n, sinon */
require('/shared/test/unit/mocks/mock_l10n.js');
requireApp('bookmark/test/unit/mock_mozsetmessagehandler.js');
requireApp('bookmark/test/unit/mock_bookmark_editor.js');
requireApp('bookmark/js/bookmark_remover.js');
requireApp('bookmark/js/components/status.js');
requireApp('bookmark/js/activity_handler.js');
var mocksHelperForSaveBookmark = new MocksHelper([
'BookmarkEditor'
]);
mocksHelperForSaveBookmark.init();
suite('activity_handler.js >', function() {
mocksHelperForSaveBookmark.attachTestHelpers();
var realMozL10n;
suiteSetup(function() {
realMozL10n = navigator.mozL10n;
navigator.mozL10n = MockL10n;
});
suiteTeardown(function() {
navigator.mozSetMessageHandler = realMozSetMessageHandler;
navigator.mozL10n = realMozL10n;
msgHandler.activity = null;
});
function createSource(name, type) {
return {
name: name,
data: {
type: type
}
};
}
suite('Initialization >', function() {
test('Bookmark app is listening right now ', function() {
// The handler was defined
assert.isFunction(msgHandler.activity);
// The handler defines a parameter -> the activity
assert.equal(msgHandler.activity.length, 1);
});
});
suite('Adding and updating bookmarks >', function() {
suiteSetup(function() {
utils.status.init();
// We change the duration of the status in order to avoid timeout
utils.status.setDuration(400);
});
suiteTeardown(function() {
utils.status.destroy();
});
test.skip('The new bookmark has been added correctly ', function(done) {
msgHandler.activity({
source: createSource('save-bookmark', 'url'),
postResult: function(result) {
assert.equal(result, 'saved');
done();
}
});
// The user clicks on <save> button
BookmarkEditor.save(true);
});
test('The bookmark already exists and it is updated ', function(done) {
msgHandler.activity({
source: createSource('save-bookmark', 'url'),
postResult: function(result) {
assert.equal(result, 'updated');
done();
}
});
// The user clicks on <save> button
BookmarkEditor.save();
});
test('Bookmarking failed - request cancelled by user ', function(done) {
msgHandler.activity({
source: createSource('save-bookmark', 'url'),
postError: function(result) {
assert.equal(result, 'cancelled');
done();
}
});
// The user cancels the request
BookmarkEditor.cancel();
});
test('The status is displayed when a bookmark has been added correctly ',
function(done) {
msgHandler.activity({
source: createSource('save-bookmark', 'url'),
postResult: function() { }
});
window.addEventListener('status-showed', function showed() {
window.removeEventListener('status-showed', showed);
assert.isTrue(document.querySelector('[role="status"] p').textContent.
length > 0);
});
window.addEventListener('status-hidden', function hidden() {
window.removeEventListener('status-hidden', hidden);
done();
});
BookmarkEditor.save(true);
});
});
suite('Removing bookmarks >', function() {
var stub = null,
removed = true;
suiteSetup(function() {
stub = sinon.stub(BookmarkRemover, 'init', function(options) {
removed ? options.onremoved() : options.oncancelled();
});
});
suiteTeardown(function() {
stub.restore();
});
test('The bookmark has been removed correctly ', function(done) {
msgHandler.activity({
source: createSource('remove-bookmark', 'url'),
postResult: function(result) {
assert.equal(result, 'removed');
done();
}
});
});
test('Removing failed ', function(done) {
removed = false;
msgHandler.activity({
source: createSource('remove-bookmark', 'url'),
postError: done
});
});
});
suite('Common errors >', function() {
test('Name not supported ', function(done) {
// "save-watermelon" activities are not supported
msgHandler.activity({
source: createSource('save-watermelon', 'url'),
postError: function(result) {
assert.equal(result, 'name not supported');
done();
}
});
});
test('Type not supported for save-bookmark', function(done) {
// We don't support the type "cherry"
msgHandler.activity({
source: createSource('save-bookmark', 'cherry'),
postError: function(result) {
assert.equal(result, 'type not supported');
done();
}
});
});
test('Type not supported for remove-bookmark', function(done) {
// We don't support the type "lemon"
msgHandler.activity({
source: createSource('remove-bookmark', 'lemon'),
postError: function(result) {
assert.equal(result, 'type not supported');
done();
}
});
});
});
});

View file

@ -0,0 +1,222 @@
'use strict';
/* global loadBodyHTML, BookmarkEditor, BookmarksDatabase, Icon */
/* global requireApp, require, suite, suiteTeardown, suiteSetup, test, assert,
sinon, MocksHelper */
require('/shared/test/unit/load_body_html_helper.js');
require('/shared/test/unit/mocks/mock_l10n.js');
require('/shared/test/unit/mocks/mock_web_manifest_helper.js');
require('/shared/test/unit/mocks/mock_icons_helper.js');
require('/shared/js/homescreens/icon.js');
requireApp('bookmark/js/bookmark_editor.js');
require('/shared/js/bookmarks_database.js');
require('/shared/js/url_helper.js');
var mocksForBookmarkEditor = new MocksHelper([
'IconsHelper', 'WebManifestHelper'
]).init();
suite('bookmark_editor.js >', function() {
var getStub, iconRenderStub, realL10n;
mocksForBookmarkEditor.attachTestHelpers();
var name = 'Mozilla';
var url = 'http://www.mozilla.org/es-ES/firefox/new/';
var bookmark = {
name: name,
url: url
};
var databaseInError = false;
suiteSetup(function() {
realL10n = navigator.mozL10n;
navigator.mozL10n = window.MockL10n;
loadBodyHTML('/save.html');
getStub = sinon.stub(BookmarksDatabase, 'get', function(purl) {
return {
then: function(resolve, refect) {
databaseInError ? refect() : resolve (purl === url ? bookmark : null);
}
};
});
iconRenderStub = sinon.stub(Icon.prototype, 'render', function() {});
});
suiteTeardown(function() {
navigator.mozL10n = realL10n;
document.body.innerHTML = '';
databaseInError = false;
getStub.restore();
iconRenderStub.restore();
});
function noop() {
// Do nothing
}
function dispatchInputEvent() {
BookmarkEditor.form.dispatchEvent(new CustomEvent('input'));
}
suite('Add UI initialized correctly >', function() {
var expectedName = 'Telefonica';
suiteSetup(function() {
BookmarkEditor.init({
data: {
name: expectedName
},
oncancelled: noop
});
});
test('The title has to be defined from options.data.name >', function() {
assert.equal(document.getElementById('bookmark-title').value,
expectedName);
});
test('Checking styles', function() {
assert.equal(document.body.dataset.mode, 'add');
});
});
suite('Edit UI initialized correctly >', function() {
suiteSetup(function() {
BookmarkEditor.init({
data: bookmark,
oncancelled: noop
});
});
test('The title has to be defined from datastore >', function() {
assert.equal(document.getElementById('bookmark-title').value, name);
});
test('Checking styles', function() {
assert.equal(document.body.dataset.mode, 'put');
});
test('Checking "done" button after writing', function() {
document.getElementById('bookmark-form').
dispatchEvent(new CustomEvent('input'));
assert.isFalse(BookmarkEditor.saveButton.disabled);
});
});
suite('UI initialized correctly as "add" when database fails >', function() {
var expectedName = 'Telefonica';
var expectedURL = 'www.telefonica.es';
suiteSetup(function() {
databaseInError = false;
BookmarkEditor.init({
data: {
name: expectedName,
url: expectedURL
},
oncancelled: noop
});
});
test('The title has to be defined from options.data.name >', function() {
assert.equal(document.getElementById('bookmark-title').value,
expectedName);
});
test('Checking styles', function() {
assert.equal(document.body.dataset.mode, 'add');
});
test('Checking "add" button initially', function() {
assert.isFalse(BookmarkEditor.saveButton.disabled);
});
});
suite('Invalid website name >', function() {
suiteSetup(function() {
BookmarkEditor.init({
data: {
name: '',
url: url
},
oncancelled: noop
});
});
test('Bookmarks with blank title should not be saved >', function() {
assert.isTrue(BookmarkEditor.saveButton.disabled,
'Blank title, add button should be disabled');
var saveSpy = sinon.spy(BookmarkEditor, 'save');
BookmarkEditor._submit(new CustomEvent('submit'));
assert.equal(saveSpy.callCount, 0);
BookmarkEditor.save.restore();
});
test('Check save button typing website name ', function() {
BookmarkEditor.bookmarkTitle.value = 'Telefonica';
dispatchInputEvent();
assert.isFalse(BookmarkEditor.saveButton.disabled);
BookmarkEditor.bookmarkTitle.value = '';
dispatchInputEvent();
assert.isTrue(BookmarkEditor.saveButton.disabled);
BookmarkEditor.bookmarkTitle.value = 'Mozilla';
dispatchInputEvent();
assert.isFalse(BookmarkEditor.saveButton.disabled);
});
});
suite('Install App >', function() {
suiteSetup(function() {
BookmarkEditor.init({
data: {
name: 'My App',
url: 'http://example.com'
},
oncancelled: noop
});
});
test('_fetchManifest()', function(done) {
var stubRenderAppIcon = sinon.stub(BookmarkEditor, '_renderAppIcon',
function(manifest, size) {}
);
BookmarkEditor._fetchManifest().then(
function () {
assert.isFalse(
BookmarkEditor.appInstallationSection.classList.contains(
'hidden'));
assert.equal(BookmarkEditor.appNameText.textContent, 'App');
done();
stubRenderAppIcon.restore();
},
function (err) {
done(err);
console.error(err);
});
});
test('_renderAppIcon()', function() {
this.sinon.stub(window.IconsHelper, 'getBestIconFromWebManifest',
function() {
return new URL('http://example.com/icon.png');
});
BookmarkEditor._renderAppIcon({}, 60);
assert.equal(BookmarkEditor.appIcon.getAttribute('src'),
'http://example.com/icon.png');
});
});
});

View file

@ -0,0 +1,153 @@
'use strict';
/* global BookmarkRemover, loadBodyHTML, BookmarksDatabase */
/* global requireApp, require, suite, suiteTeardown, suiteSetup, test, assert,
sinon, teardown, setup, MockL10n */
require('/shared/js/bookmarks_database.js');
require('/shared/test/unit/load_body_html_helper.js');
require('/shared/test/unit/mocks/mock_l10n.js');
requireApp('bookmark/js/bookmark_remover.js');
suite('bookmark_remover.js >', function() {
var getStub;
var realMozL10n;
var name = 'Mozilla';
var url = 'http://www.mozilla.org/es-ES/firefox/new/';
var bookmark = {
name: name,
url: url
};
var databaseInError = false;
suiteSetup(function() {
getStub = sinon.stub(BookmarksDatabase, 'get', function(purl) {
return {
then: function(resolve, reject) {
databaseInError ? reject('rejected') :
resolve (purl === url ? bookmark : null);
}
};
});
realMozL10n = navigator.mozL10n;
navigator.mozL10n = MockL10n;
});
suiteTeardown(function() {
getStub.restore();
navigator.mozL10n = realMozL10n;
});
setup(function() {
loadBodyHTML('/remove.html');
});
teardown(function() {
document.body.innerHTML = '';
databaseInError = false;
});
test('UI initialized correctly >', function() {
BookmarkRemover.init({
id: bookmark.url
});
assert.isTrue(document.getElementById('title').dataset.l10nArgs
.contains(name));
assert.isTrue(document.getElementById('message').dataset.l10nArgs
.contains(name));
});
test('Handles quotes in bookmark name >', function() {
bookmark.name = name = 'Mozilla"\'';
BookmarkRemover.init({
id: bookmark.url
});
var titleArgs =
JSON.parse(document.getElementById('title').dataset.l10nArgs);
var messageArgs =
JSON.parse(document.getElementById('message').dataset.l10nArgs);
assert.equal(titleArgs.name, name);
assert.equal(messageArgs.name, name);
});
test('Bookmark does not exist >', function(done) {
BookmarkRemover.init({
id: bookmark.url + 'blabla',
oncancelled: function(e) {
assert.equal(e, 'bookmark does not exist');
done();
}
});
});
test('Getting bookmark failed >', function(done) {
databaseInError = true;
BookmarkRemover.init({
id: bookmark.url,
oncancelled: function(e) {
assert.equal(e, 'rejected');
done();
}
});
});
test('User cancels >', function(done) {
BookmarkRemover.init({
id: bookmark.url,
oncancelled: function(e) {
assert.equal(e, 'cancelled');
done();
}
});
document.getElementById('cancel-action').click();
});
test('User confirms and bookmark is removed >', function(done) {
var stub = sinon.stub(BookmarksDatabase, 'remove', function(id) {
assert.equal(id, bookmark.url);
return {
then: function(resolve) {
resolve();
}
};
});
BookmarkRemover.init({
id: bookmark.url,
onremoved: function() {
stub.restore();
done();
}
});
document.getElementById('remove-action').click();
});
test('User confirms but it fails >', function(done) {
var stub = sinon.stub(BookmarksDatabase, 'remove', function(id) {
assert.equal(id, bookmark.url);
return {
then: function(resolve, reject) {
reject('failed');
}
};
});
BookmarkRemover.init({
id: bookmark.url,
oncancelled: function(e) {
assert.equal(e, 'failed');
stub.restore();
done();
}
});
document.getElementById('remove-action').click();
});
});

View file

@ -0,0 +1,311 @@
'use strict';
/* global BookmarksDatabase, MockDatastore, MockNavigatorDatastore */
/* global require, suite, suiteTeardown, suiteSetup, test, assert, teardown,
setup */
require('/shared/test/unit/mocks/mock_navigator_datastore.js');
require('/shared/js/bookmarks_database.js');
suite('bookmarks_database.js >', function() {
var realDatastore = null,
url = 'http://mozilla.org',
name = 'mozilla',
data = {
url: url,
name: name
};
suiteSetup(function() {
realDatastore = navigator.getDataStores;
});
suiteTeardown(function() {
navigator.getDataStores = realDatastore;
});
setup(function() {
navigator.getDataStores = MockNavigatorDatastore.getDataStores;
});
teardown(function() {
MockDatastore._inError = false;
MockDatastore._records = Object.create(null);
});
function assertEvent(type, event) {
assert.equal(event.type, type);
assert.equal(event.target.id, url);
assert.equal(event.target.url, url);
assert.equal(event.target.name, name);
}
test('testing not getDataStores defined >', function(done) {
navigator.getDataStores = undefined;
BookmarksDatabase.getRevisionId().then(function(id) {
// Do noting
}, function(e) {
assert.equal(e.name, 'NO_DATASTORE');
done();
});
});
test('testing no access to datastore >', function(done) {
navigator.getDataStores = function() {
return {
then: function(resolve, reject) {
resolve({
length: 0
});
}
};
};
BookmarksDatabase.getRevisionId().then(function(id) {
// Do noting
}, function(e) {
assert.equal(e.name, 'NO_ACCESS_TO_DATASTORE');
done();
});
});
test('calling to add method - added >', function(done) {
BookmarksDatabase.add(data).then(function(value) {
assert.isTrue(value);
done();
});
});
test('calling to add method - updated >', function(done) {
var expectedName = name + 'corporation';
BookmarksDatabase.add(data).then(function(value) {
assert.isTrue(value);
BookmarksDatabase.add({
url: url,
name: expectedName
}).then(function(value) {
assert.isUndefined(value);
done();
});
});
});
test('calling to add method - failed >', function(done) {
MockDatastore._inError = true;
BookmarksDatabase.add(data).then(function() {
// Do nothing here
}, function() {
done();
});
});
test('calling to put method - OK >', function(done) {
var id = url;
var expectedName = 'Telefonica';
var expectedURL = 'http://www.telefonica.es';
MockDatastore._records[id] = {
id: id,
url: url,
name: name
};
BookmarksDatabase.put({
id: id,
url: expectedURL,
name: expectedName
}).then(function onSuccess() {
BookmarksDatabase.get(id).then(function(bookmark) {
assert.equal(bookmark.name, expectedName);
assert.equal(bookmark.url, expectedURL);
assert.equal(bookmark.id, id);
done();
});
});
});
test('calling to put method - failed >', function(done) {
MockDatastore._inError = true;
BookmarksDatabase.put(data).then(function() {
// Do nothing here
}, function() {
done();
});
});
test('calling to remove method - OK >', function(done) {
var id = url;
MockDatastore._records[id] = {
id: id,
url: url,
name: name
};
BookmarksDatabase.remove(id).then(function(bookmark) {
assert.equal(Object.keys(MockDatastore._records).length, 0);
done();
});
});
test('calling to remove method - failed >', function(done) {
MockDatastore._inError = true;
BookmarksDatabase.remove(url).then(function() {
// Do nothing here
}, function() {
done();
});
});
test('calling to get method - OK >', function(done) {
BookmarksDatabase.add(data).then(function() {
BookmarksDatabase.get(url).then(function(bookmark) {
assert.equal(bookmark.name, name);
assert.equal(bookmark.url, url);
assert.equal(bookmark.id, url);
done();
});
});
});
test('calling to get method - failed >', function(done) {
MockDatastore._inError = true;
BookmarksDatabase.get('www.telefonica.es').then(function() {
// Do nothing here
}, function() {
done();
});
});
test('calling to getRevisionId method - OK >', function(done) {
BookmarksDatabase.getRevisionId().then(function(id) {
assert.equal(id, MockDatastore.revisionId);
done();
});
});
test('calling to getAll method - OK >', function(done) {
MockDatastore.sync = function() {
var cursor = {
next: function() {
cursor.next = function() {
return new window.Promise(function(resolve, reject) {
resolve({
operation: 'done'
});
});
};
return new window.Promise(function(resolve, reject) {
resolve({
operation: 'add',
data: {
id: url,
url: url,
name: name
}
});
});
}
};
return cursor;
};
BookmarksDatabase.getAll().then(function(bookmarks) {
assert.equal(Object.keys(bookmarks).length, 1);
assert.equal(bookmarks[url].name, name);
assert.equal(bookmarks[url].id, url);
assert.equal(bookmarks[url].url, url);
done();
});
});
test('calling to getAll method - failed >', function(done) {
MockDatastore.sync = function() {
var cursor = {
next: function() {
return new window.Promise(function(resolve, reject) {
reject();
});
}
};
return cursor;
};
BookmarksDatabase.getAll().then(function(bookmarks) {
// Do nothing
}, function() {
done();
});
});
test('testing addEventListener for "added" event >', function(done) {
BookmarksDatabase.addEventListener('added', function onAdded(event) {
BookmarksDatabase.removeEventListener('added', onAdded);
assertEvent('added', event);
done();
});
BookmarksDatabase.add(data);
});
test('testing addEventListener for context >', function(done) {
var myObject = {
handleEvent: function(event) {
BookmarksDatabase.removeEventListener('added', myObject);
assert.equal(this, myObject);
assertEvent('added', event);
done();
}
};
BookmarksDatabase.addEventListener('added', myObject);
BookmarksDatabase.add(data);
});
test('testing addEventListener for "updated" event >', function(done) {
// Testing handleEvent instead of a callback directly
var handler = {
handleEvent: function(event) {
BookmarksDatabase.removeEventListener('updated', handler);
assertEvent('updated', event);
done();
}
};
BookmarksDatabase.addEventListener('updated', handler);
BookmarksDatabase.add(data).then(function onAdded() {
BookmarksDatabase.add(data);
});
});
test('testing removeEventListener >', function(done) {
var onAdded = function() {
done(new Error('The "added" listener should NOT be called!!!'));
};
BookmarksDatabase.addEventListener('added', onAdded);
var ret = BookmarksDatabase.removeEventListener('added', onAdded);
assert.isTrue(ret);
BookmarksDatabase.add(data).then(function() {
done();
});
});
test('testing removeEventListener unknown type >', function() {
assert.isFalse(BookmarksDatabase.removeEventListener('dog', function() {
// Do nothing...
}));
});
test('testing removeEventListener unknown callback >', function() {
assert.isFalse(BookmarksDatabase.removeEventListener('added', function() {
// Do nothing...
}));
});
});

View file

@ -0,0 +1,22 @@
'use strict';
/* exported MockBookmarkEditor */
var MockBookmarkEditor = {
init: function mbe_init(options) {
this.onsaved = options.onsaved;
this.oncancelled = options.oncancelled;
},
close: function mbe_close() {
this.oncancelled();
},
save: function mbe_save(value) {
this.onsaved(value);
},
cancel: function mbe_cancel() {
this.oncancelled();
}
};

View file

@ -0,0 +1,9 @@
'use strict';
/* exported realMozSetMessageHandler, msgHandler */
var realMozSetMessageHandler = navigator.mozSetMessageHandler;
var msgHandler = {};
navigator.mozSetMessageHandler = function(type, handler) {
msgHandler[type] = handler;
};

View file

@ -0,0 +1,19 @@
'use strict';
/* exported MockXMLHttpRequest */
var MockXMLHttpRequest = function() {
this.response = 'foo';
};
MockXMLHttpRequest.prototype.send = function() {
this.status = '200';
this.onload();
};
MockXMLHttpRequest.prototype.setRequestHeader = function() {
};
MockXMLHttpRequest.prototype.open = function() {
};

View file

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