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

View file

@ -0,0 +1,19 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"__dirname",
"module",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished",
"require",
"define",
"requireApp",
"sinon"
]
}

View file

@ -0,0 +1,65 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Appcache installed download', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/appcache';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('shows spinner while downloading', function() {
// correctly install the app...
client.switchToFrame();
// ensure appcache path is delayed
server.cork(server.manifest.appcache_path);
appInstall.install(server.manifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.manifestURL);
// wait until we see the nice spinner thing
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// let the rest of the app come through
server.uncork(server.manifest.appcache_path);
// wait until it is no longer loading
client.waitFor(function() {
return !home.iconIsLoading(icon);
});
// ensure the app launches!
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,38 @@
'use strict';
var assert = require('assert');
marionette('Homescreen - App Blacklist', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var home, system;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
client.apps.launch(home.URL);
home.waitForLaunch();
// Fail finding elements quickly.
client.setSearchTimeout(20);
});
function lookForIcon(manifestURL) {
try {
home.getIcon(manifestURL);
} catch(e) {
return false;
}
return true;
}
test('app does not appear on home screen if in blacklist', function() {
// Ensure that a normal icon is there.
var calendarApp = lookForIcon('app://calendar.gaiamobile.org');
assert.ok(calendarApp);
});
});

View file

@ -0,0 +1,54 @@
'use strict';
/* global __dirname */
var assert = require('assert');
marionette('Homescreen - Default app order', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var home, system;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('Default app order is respected', function() {
var expectedOrder = [
'Phone',
'Messages',
'Contacts',
'E-Mail',
'Browser',
'Camera',
'Gallery',
'Music',
'Video',
'Marketplace',
'Calendar',
'Clock',
'Settings',
'FM Radio',
'BuddyUp',
'Bugzilla Lite',
'Facebook',
'Twitter',
'Notes',
'Calculator',
'Usage'
];
var length = expectedOrder.length;
var iconNames = home.visibleIcons.map(function(icon) {
return home.getIconText(icon);
}).slice(0, length);
for (var i = 0; i < length; i++) {
assert.equal(iconNames[i], expectedOrder[i]);
}
});
});

View file

@ -0,0 +1,60 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Hosted app failed icon fetch', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('fallback to default icon', function() {
var iconURL = server.manifest.icons['128'];
// correctly install the app...
client.switchToFrame();
// ensure the icon fails to download!
server.fail(iconURL);
appInstall.install(server.manifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.manifestURL);
// ensure the default icon is shown
home.waitForIconImageUrl(icon, 'default');
// ensure the icon can be launched!
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,44 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Hosted App Install', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('install app', function() {
client.switchToFrame();
appInstall.install(server.manifestURL);
client.switchToFrame(system.getHomescreenIframe());
home.launchIcon(home.getIcon(server.manifestURL));
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,113 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Hosted app failed icon fetch', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var actions, home, system, appInstall;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
function tapAndWaitFor(icon, element) {
client.waitFor(function() {
icon.tap();
actions.wait(0.5).perform();
return element.scriptWith(function(el) {
return getComputedStyle(el).display !== 'none';
});
});
}
test('shows icon after a restart', function() {
// go to the system app
client.switchToFrame();
// don't let the server send the zip archive
server.cork(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
home.scrollIconToCenter(icon);
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// stop the download
tapAndWaitFor(icon, home.cancelDownloadDialog);
home.actionDialog(home.cancelDownloadDialog, 'stop-download-action');
home.waitForIconImageUrl(icon, 'app_install_canceled');
// Restart the download
server.uncork(server.applicationZipUri);
// resume the download from the ui
tapAndWaitFor(icon, home.resumeDownloadDialog);
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
// wait until we are showing our desired icon
home.waitForIconImageUrl(icon, 'app-icon');
});
test('fallback to default icon when icon fails', function() {
var iconURL = server.manifest.icons['128'];
// correctly install the app...
client.switchToFrame();
// ensure the icon fails to download!
server.fail(iconURL);
appInstall.install(server.manifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.manifestURL);
// ensure the default icon is shown
home.waitForIconImageUrl(icon, 'default');
server.unfail(iconURL);
// XXX: We don't have real network access but we can sorta emulate it by
// failing then switching back to an online state.
client.executeScript(function() {
window.dispatchEvent(new CustomEvent('online'));
});
// wait until we are showing our desired icon
home.waitForIconImageUrl(icon, 'app-icon');
// ensure the icon can be launched!
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,86 @@
/* global __dirname */
'use strict';
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Hosted app cached icon fetch', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('fallback to default icon', function() {
var iconURL = server.manifest.icons['128'];
// correctly install the app...
client.switchToFrame();
appInstall.install(server.manifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.manifestURL);
// ensure it is cached
home.waitForIconImageUrl(icon, 'app-icon');
var id = home.getIconId(icon);
client.waitFor(function() {
var metadata = client.executeAsyncScript(function() {
window.wrappedJSObject.appWindow.apps.metadata.getAll().then(
marionetteScriptFinished);
});
for (var i = 0, iLen = metadata.length; i < iLen; i++) {
if (metadata[i].id.startsWith(id) &&
typeof metadata[i].icon !== 'undefined') {
return true;
}
}
return false;
});
// ensure http fails so we use the cached icon
server.fail(iconURL);
// Reload the homescreen
home.restart();
icon = home.getIcon(server.manifestURL);
// check for the cached icon...
home.waitForIconImageUrl(icon, 'user-set');
// allow the request to succeed
server.unfail(iconURL);
// trigger the download of a new icon
client.executeScript(function() {
window.wrappedJSObject.dispatchEvent(new CustomEvent('online'));
});
// now shows the freshly redownloaded icon
home.waitForIconImageUrl(icon, 'app-icon');
});
});

View file

@ -0,0 +1,66 @@
'use strict';
/* global __dirname */
var assert = require('assert');
marionette('Homescreen - App order', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var actions, home, system;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('App order is retained after restart', function() {
// Drag icon to a different place
var icons = home.visibleIcons;
var location1 = icons[0].location();
var location2 = icons[1].location();
actions.wait(0.5).press(icons[0]).wait(0.5).
move(icons[1]).release().wait(0.5).perform();
client.waitFor(function() {
return icons[0].location().x === location2.x &&
icons[1].location().x === location1.x;
});
// Wait for the icon order to be saved
var ids = home.getIconIdentifiers();
var numIcons = ids.length;
client.waitFor(function() {
var order = client.executeAsyncScript(function() {
window.wrappedJSObject.appWindow.apps.metadata.getAll().then(
marionetteScriptFinished);
});
// The order array is stored in order, but also contains non-visible
// icons, so just skip unknown entries.
var correctlyPlacedIcons = 0;
for (var i = 0, iLen = order.length; i < iLen; i++) {
if (order[i].id.startsWith(ids[correctlyPlacedIcons])) {
++ correctlyPlacedIcons;
}
}
return correctlyPlacedIcons === numIcons;
});
// Test that icon ordering is retained after a restart
home.restart();
client.waitFor(function() {
return home.visibleIcons.length === numIcons;
});
var newIds = home.getIconIdentifiers();
for (var i = 0, iLen = ids.length; i < iLen; i++) {
assert.equal(ids[i], newIds[i]);
}
});
});

View file

@ -0,0 +1,59 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Failed Download', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('failed state then retry and launch', function() {
client.switchToFrame();
server.fail(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
appInstall.dismissToast();
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
home.waitForIconImageUrl(icon, 'failed');
server.unfail(server.applicationZipUri);
icon.tap();
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
home.waitForIconImageUrl(icon, 'app-icon');
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,45 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Install', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('install app', function() {
client.switchToFrame();
appInstall.installPackage(server.packageManifestURL);
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
home.waitForIconImageUrl(icon, 'app-icon');
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,61 @@
/* global __dirname */
'use strict';
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Pending', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('app loading spinner', function() {
// go to the system app
client.switchToFrame();
// don't let the server send the zip archive
server.cork(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
// wait until the icon is spinning!
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// let the rest of the app come through
server.uncork(server.applicationZipUri);
// wait until it is no longer loading
client.waitFor(function() {
return !home.iconIsLoading(icon);
});
});
});

View file

@ -0,0 +1,63 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Pending Update', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
// wait for the system app to be running
system.waitForFullyLoaded();
client.apps.launch(home.URL);
home.waitForLaunch();
// install the app
client.switchToFrame();
appInstall.installPackage(server.packageManifestURL);
// switch to the homescreen
client.switchToFrame(system.getHomescreenIframe());
});
teardown(function(done) {
server.close(done);
});
test('update an installed app', function() {
var icon = home.getIcon(server.packageManifestURL);
// ensure the app is installed before updating it
home.waitForIconImageUrl(icon, 'app-icon');
// Update the manifests.
server.setRoot(__dirname + '/fixtures/template_app_updated');
// Stage the update
appInstall.stageUpdate(server.packageManifestURL);
// ensure we can still launch an app in the older version
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,93 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Resuming Downloads', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server, actions, home, system, appInstall;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
done(err);
});
});
teardown(function(done) {
server.close(done);
});
function tapAndWaitFor(icon, element) {
client.waitFor(function() {
icon.tap();
actions.wait(0.5).perform();
return element.scriptWith(function(el) {
return getComputedStyle(el).display !== 'none';
});
});
}
test('failed state then retry and launch', function() {
client.switchToFrame();
server.cork(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
client.switchToFrame(system.getHomescreenIframe());
// Get the icon
var icon = home.getIcon(server.packageManifestURL);
// Scroll so the icon is in the middle of the screen and won't be
// obscured by the system toast
home.scrollIconToCenter(icon);
// pause the download
tapAndWaitFor(icon, home.cancelDownloadDialog);
home.actionDialog(home.cancelDownloadDialog, 'stop-download-action');
home.waitForIconImageUrl(icon, 'app_install_canceled');
// resume the download
tapAndWaitFor(icon, home.resumeDownloadDialog);
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// pause it again!
tapAndWaitFor(icon, home.cancelDownloadDialog);
home.actionDialog(home.cancelDownloadDialog, 'stop-download-action');
home.waitForIconImageUrl(icon, 'app_install_canceled');
// uncork so next resume works...
server.uncork(server.applicationZipUri);
// finally download the entire app
tapAndWaitFor(icon, home.resumeDownloadDialog);
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
home.waitForIconImageUrl(icon, 'app-icon');
// now it should work!
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,101 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Resume Update', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var actions, home, system, appInstall;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
// wait for the system app to be running
system.waitForFullyLoaded();
home.waitForLaunch();
// install the app
client.switchToFrame();
appInstall.installPackage(server.packageManifestURL);
// switch to the homescreen
client.switchToFrame(system.getHomescreenIframe());
});
teardown(function(done) {
server.close(done);
});
function tapAndWaitFor(icon, element) {
client.waitFor(function() {
icon.tap();
actions.wait(0.5).perform();
return element.scriptWith(function(el) {
return getComputedStyle(el).display !== 'none';
});
});
}
test('resume update', function() {
var icon = home.getIcon(server.packageManifestURL);
// Scroll so the icon is in the middle of the screen and won't be
// obscured by the system toast
home.scrollIconToCenter(icon);
// ensure the app is installed before updating it
home.waitForIconImageUrl(icon, 'app-icon');
// Cork the zip so we can see the loading spinner.
server.cork('/app.zip');
// Update the manifests.
server.setRoot(__dirname + '/fixtures/template_app_updated');
// Ensure we have an update.
appInstall.update(server.packageManifestURL);
// Ensure we see the loading signs...
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// pause the update
tapAndWaitFor(icon, home.cancelDownloadDialog);
home.actionDialog(home.cancelDownloadDialog, 'stop-download-action');
home.waitForIconImageUrl(icon, 'app_install_canceled');
// resume the update
tapAndWaitFor(icon, home.resumeDownloadDialog);
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// Wait for the download to be complete.
server.uncork('/app.zip');
home.waitForIconImageUrl(icon, 'app-icon');
// verify the app was really updated
home.launchIcon(icon);
assert.equal(client.title(), 'updatedwow');
});
});

View file

@ -0,0 +1,59 @@
/* global __dirname */
'use strict';
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Pending', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/role_homescreen';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('app is displayed until determined role=homescreen', function() {
// go to the system app
client.switchToFrame();
// don't let the server send the zip archive
server.cork(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
// wait until the icon is spinning!
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// let the rest of the app come through
server.uncork(server.applicationZipUri);
// wait until the icon is hidden
client.helper.waitForElementToDisappear(icon);
});
});

View file

@ -0,0 +1,93 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Update', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
// wait for the system app to be running
system.waitForFullyLoaded();
// Launch the homescreen first, then go to the system app.
// Make sure we do this before installing an application.
home.waitForLaunch();
client.switchToFrame();
// install the app with a broken icon
appInstall.installPackage(server.packageManifestURL);
// switch to the homescreen
client.switchToFrame(system.getHomescreenIframe());
});
teardown(function(done) {
server.close(done);
});
test('update an installed app', function() {
var icon = home.getIcon(server.packageManifestURL);
// ensure the app is installed before updating it
home.waitForIconImageUrl(icon, 'app-icon');
// Prepare to screenshot
var iconImage = home.getIconImage(icon);
var iconTitle = home.getIconText(icon);
// Cork the zip so we can see the loading spinner.
server.cork('/app.zip');
// Update the manifests.
server.setRoot(__dirname + '/fixtures/template_app_updated');
// Ensure we have an update.
appInstall.update(server.packageManifestURL);
// Ensure we see the loading signs...
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// Wait for the download to be complete.
server.uncork('/app.zip');
home.waitForIconImageUrl(icon, 'app-icon');
// See bug 826555 for rationale for the title not changing.
assert.equal(
home.getIconText(icon),
iconTitle,
'app name should not be updated'
);
// Make sure the icon changed
assert.notEqual(
home.getIconImage(icon),
iconImage,
'app icon should have updated');
// verify the app was really updated
home.launchIcon(icon);
assert.equal(client.title(), 'updatedwow');
});
});

View file

@ -0,0 +1,45 @@
'use strict';
/* global __dirname */
marionette('Homescreen - App reordering', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var actions, home, system;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('App reordering has a visual hint', function() {
var icons = home.visibleIcons;
var location1 = icons[0].location();
var location3 = icons[2].location();
// Drag second icon over third icon
actions.wait(0.5).press(icons[1]).wait(0.5).
move(icons[2]).perform();
// Test that the third icon moves out of the way in the right direction
client.waitFor(function() {
return icons[2].location().x < location3.x;
});
// Drag second icon over first icon
actions.move(icons[0]).perform();
// Test that the third icon returns to its original position
client.waitFor(function() {
return icons[2].location().x === location3.x;
});
// Test that the first icon moves out of the way in the right direction
client.waitFor(function() {
return icons[0].location().x > location1.x;
});
});
});

View file

@ -0,0 +1,30 @@
/* global __dirname */
'use strict';
var assert = require('assert');
marionette('Homescreen - App manifest', function() {
var manifestName = 'shortnameapp.gaiamobile.org';
var manifestUrl = 'app://' + manifestName + '/manifest.webapp';
var clientOptions = require(__dirname + '/client_options.js');
clientOptions.apps = {};
clientOptions.apps[manifestName] = __dirname + '/fixtures/short_name_app';
var client = marionette.client({ profile: clientOptions });
var home, system;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
client.apps.launch(home.URL);
home.waitForLaunch();
});
test('short_name', function() {
assert.equal(home.getIconText(home.getIcon(manifestUrl)), 'Short name');
});
});

View file

@ -0,0 +1,78 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - App uninstall while pending', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var actions, home, system, appInstall;
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
setup(function() {
appInstall = new AppInstall(client);
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
// ensure that the zip file does not get sent
server.cork(server.applicationZipUri);
// wait for the system app to be running
system.waitForFullyLoaded();
client.apps.launch(home.URL);
home.waitForLaunch();
// install the app
client.switchToFrame();
appInstall.installPackage(server.packageManifestURL);
// switch to the homescreen
client.switchToFrame(system.getHomescreenIframe());
});
test('uninstall the app', function() {
var icon = home.getIcon(server.packageManifestURL);
home.scrollIconToCenter(icon);
// remove the icon
actions.longPress(icon, 1).perform();
home.waitForEditBar();
home.removeButton.tap();
// confirm the dialog to ensure it was removed.
client.switchToFrame();
home.confirmDialog('remove');
// ensure the icon disappears
client.switchToFrame(system.getHomescreenIframe());
client.helper.waitForElementToDisappear(icon);
// make sure app has gone after restart
home.restart();
client.setSearchTimeout(20);
icon = null;
try {
icon = home.getIcon(server.packageManifestURL);
} catch(e) { }
assert.ok(!icon, 'app was not removed');
});
});

View file

@ -0,0 +1,85 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - App Uninstall', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var actions, home, system, appInstall;
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
setup(function() {
appInstall = new AppInstall(client);
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
client.apps.launch(home.URL);
home.waitForLaunch();
});
function test_app_uninstall(manifestURL) {
// Install the app
client.switchToFrame();
appInstall.install(manifestURL);
appInstall.dismissToast();
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(manifestURL);
// XXX: work around issues where the icon is hidden by other
// status messages on the system app.
icon.scriptWith(function(el) {
// effectively scroll to the bottom of the screen.
el.scrollIntoView(false);
});
// remove the icon
actions.longPress(icon, 1).perform();
home.removeButton.tap();
// confirm the dialog to ensure it was removed.
client.switchToFrame();
home.confirmDialog('remove');
// ensure the icon disappears
client.switchToFrame(system.getHomescreenIframe());
client.helper.waitForElementToDisappear(icon);
// make sure app has gone after restart
home.restart();
client.setSearchTimeout(20);
icon = null;
try {
icon = home.getIcon(server.packageManifestURL);
} catch(e) { }
assert.ok(!icon, 'app was not removed');
}
test('uninstall hosted app', function() {
test_app_uninstall(server.manifestURL);
});
test('uninstall packaged app', function() {
test_app_uninstall(server.packageManifestURL);
});
});

View file

@ -0,0 +1,69 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - App unrecoverable error', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('remove an app that was in an unrecoverable state', function() {
client.switchToFrame();
server.serverError(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
appInstall.dismissToast();
client.switchToFrame(system.getHomescreenIframe());
// if the server returns a 500 error the app is assumed to be in an
// unrecoverable state.
var icon = home.getIcon(server.packageManifestURL);
home.waitForIconImageUrl(icon, 'unrecoverable');
// agree to uninstall the app
icon.tap();
client.switchToFrame();
home.confirmDialog('unrecoverable');
// ensure the icon disappears
client.switchToFrame(system.getHomescreenIframe());
client.helper.waitForElementToDisappear(icon);
// make sure app has gone after restart
home.restart();
client.setSearchTimeout(20);
icon = null;
try {
icon = home.getIcon(server.packageManifestURL);
} catch(e) { }
assert.ok(!icon, 'app was not removed');
});
});

View file

@ -0,0 +1,10 @@
module.exports = {
prefs: {
'dom.webcomponents.enabled': true
},
settings: {
'devtools.pseudolocalization.enabled': false,
'language.current': 'en-US',
'homescreen.manifestURL': 'app://homescreen.gaiamobile.org/manifest.webapp'
}
};

View file

@ -0,0 +1,12 @@
module.exports = {
prefs: {
'dom.webcomponents.enabled': true,
'dom.mozApps.homescreenURL':
'app://homescreen.gaiamobile.org/manifest.webapp'
},
settings: {
'devtools.pseudolocalization.enabled': false,
'language.current': 'en-US',
'homescreen.manifestURL': 'app://homescreen.gaiamobile.org/manifest.webapp'
}
};

View file

@ -0,0 +1,13 @@
module.exports = {
prefs: {
'dom.webcomponents.enabled': true,
'dom.mozApps.homescreenURL':
'app://homescreen.gaiamobile.org/manifest.webapp'
},
settings: {
'devtools.pseudolocalization.enabled': false,
'language.current': 'en-US',
'homescreen.manifestURL':
'app://verticalhome.gaiamobile.org/manifest.webapp'
}
};

View file

@ -0,0 +1,4 @@
CACHE MANIFEST
# v1 2014-06-10
index.html
style/icons/Blank.png

View file

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>iwrotethis</title>
</head>
<body>
<div id="test-app"></div>
</body>
</html>

View file

@ -0,0 +1,61 @@
{
"name": "Template",
"description": "Web app template",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"ar": {
"name": "Template",
"description": "Web app template"
},
"de": {
"name": "Template",
"description": "Web app template"
},
"el": {
"name": "Template",
"description": "Web app template"
},
"en-US": {
"name": "Template",
"description": "Web app template"
},
"es": {
"name": "Template",
"description": "Web app template"
},
"fr": {
"name": "Template",
"description": "Web app template"
},
"it": {
"name": "Template",
"description": "Web app template"
},
"pt-BR": {
"name": "Template",
"description": "Web app template"
},
"ru": {
"name": "Template",
"description": "Web app template"
},
"tr": {
"name": "Template",
"description": "Web app template"
},
"zh-TW": {
"name": "Template",
"description": "Web app template"
}
},
"default_locale": "en-US",
"appcache_path": "/cache.appcache",
"icons": {
"128": "/style/icons/Blank.png"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
This is an invalid favicon for testing purposes.

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sample page</title>
</head>
<body>
<header><h1>Mozilla</h1></header>
<section>We are global community with a common purpose</section>
</body>
</html>

View file

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>iwrotethis</title>
</head>
<body>
<div id="test-app"></div>
</body>
</html>

View file

@ -0,0 +1,61 @@
{
"name": "Template",
"description": "Web app template",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"ar": {
"name": "Template",
"description": "Web app template"
},
"de": {
"name": "Template",
"description": "Web app template"
},
"el": {
"name": "Template",
"description": "Web app template"
},
"en-US": {
"name": "Template",
"description": "Web app template"
},
"es": {
"name": "Template",
"description": "Web app template"
},
"fr": {
"name": "Template",
"description": "Web app template"
},
"it": {
"name": "Template",
"description": "Web app template"
},
"pt-BR": {
"name": "Template",
"description": "Web app template"
},
"ru": {
"name": "Template",
"description": "Web app template"
},
"tr": {
"name": "Template",
"description": "Web app template"
},
"zh-TW": {
"name": "Template",
"description": "Web app template"
}
},
"role": "homescreen",
"default_locale": "en-US",
"icons": {
"128": "/style/icons/Blank.png"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sample page</title>
</head>
<body>
<header><h1>Mozilla</h1></header>
<section>We are global community with a common purpose</section>
</body>
</html>

View file

@ -0,0 +1,9 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>App with a short name</title>
</head>
<body>
</body>
</html>

View file

@ -0,0 +1,14 @@
{
"name": "Loooooooooooooooooooooooooooooong name",
"short_name": "Short name",
"description": "Web app with a short name in the manifest",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"icons": {
"128": "/style/icons/Blank.png"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>iwrotethis</title>
</head>
<body>
<div id="test-app"></div>
</body>
</html>

View file

@ -0,0 +1,60 @@
{
"name": "Template",
"description": "Web app template",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"ar": {
"name": "Template",
"description": "Web app template"
},
"de": {
"name": "Template",
"description": "Web app template"
},
"el": {
"name": "Template",
"description": "Web app template"
},
"en-US": {
"name": "Template",
"description": "Web app template"
},
"es": {
"name": "Template",
"description": "Web app template"
},
"fr": {
"name": "Template",
"description": "Web app template"
},
"it": {
"name": "Template",
"description": "Web app template"
},
"pt-BR": {
"name": "Template",
"description": "Web app template"
},
"ru": {
"name": "Template",
"description": "Web app template"
},
"tr": {
"name": "Template",
"description": "Web app template"
},
"zh-TW": {
"name": "Template",
"description": "Web app template"
}
},
"default_locale": "en-US",
"icons": {
"128": "/style/icons/Blank.png"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>updatedwow</title>
</head>
<body>
<div id="test-app"></div>
</body>
</html>

View file

@ -0,0 +1,59 @@
{
"name": "Template",
"description": "Web app template",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"ar": {
"name": "Template",
"description": "Web app template"
},
"de": {
"name": "Template",
"description": "Web app template"
},
"el": {
"name": "Template",
"description": "Web app template"
},
"en-US": {
"name": "Template",
"description": "Web app template"
},
"es": {
"name": "Template",
"description": "Web app template"
},
"fr": {
"name": "Template",
"description": "Web app template"
},
"it": {
"name": "Template",
"description": "Web app template"
},
"pt-BR": {
"name": "Template",
"description": "Web app template"
},
"ru": {
"name": "Template",
"description": "Web app template"
},
"tr": {
"name": "Template",
"description": "Web app template"
},
"zh-TW": {
"name": "Template",
"description": "Web app template"
}
},
"default_locale": "en-US",
"icons": {
"128": "/style/icons/MagicIcon.png"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,72 @@
'use strict';
/* global __dirname */
var Pinning = require(
'../../../../apps/system/test/marionette/lib/pinning_the_web');
var Bookmark = require('../../../../apps/system/test/marionette/lib/bookmark');
var Server = require('../../../../shared/test/integration/server');
var assert = require('assert');
marionette('Homescreen - Home button', function() {
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var actions, bookmark, pinning, home, server, system;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
var url;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
pinning = new Pinning(client);
bookmark = new Bookmark(client, server);
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('scroll to the top of the screen', function() {
client.executeScript(function(scrollable) {
scrollable.scrollTop = 100;
}, [home.appsScrollable]);
system.tapHome();
home.waitForLaunch();
assert.equal(client.executeScript(function(scrollable) {
return scrollable.scrollTop;
}, [home.appsScrollable]), 0);
});
test('exit edit mode', function() {
url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinSiteFromBrowser(url);
system.dismissBanner();
system.tapHome();
home.waitForLaunch();
var icon = home.getIcon(url);
home.scrollIconToCenter(icon);
actions.longPress(icon, 1).perform();
system.tapHome();
home.waitForLaunch();
assert.equal(client.executeScript(function() {
return document.body.classList.contains('edit-mode');
}), false);
});
});

View file

@ -0,0 +1,112 @@
'use strict';
var assert = require('assert');
marionette('Homescreen - Layout', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var actions, home, system;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('apps grid is LTR in LTR language', function() {
var icons = home.visibleIcons;
var app1Location = icons[0].location();
var app2Location = icons[1].location();
var app3Location = icons[2].location();
var gridWidth = home.container.size().width;
// check that they are all on the same line
assert.equal(app1Location.y, app2Location.y,
'1st and 2nd apps should be on the same line.');
assert.equal(app2Location.y, app3Location.y,
'2nd and 3rd apps should be on the same line.');
// check left-alignement
assert.ok(app1Location.x < gridWidth / 3, 'First app is left-aligned');
// check the ordering
assert.ok(app1Location.x < app2Location.x,
'App1 icon should be on the left of app2 icon');
assert.ok(app2Location.x < app3Location.x,
'App2 icon should be on the left of app3 icon');
});
test('apps grid is RTL in RTL language', function() {
client.switchToFrame();
client.settings.set('language.current', 'ar-x-psbidi');
client.switchToFrame(system.getHomescreenIframe());
// Localization can be async, wait for the content to update
var localizedPhoneName =
home.localizedAppName('communications', 'dialer', 'ar-x-psbidi');
client.waitFor(function() {
return !!home.getIconByName(localizedPhoneName);
});
// Relayout on localisation change is animated, so wait for it to finish
actions.wait(1).perform();
var icons = home.visibleIcons;
var app1Location = icons[0].location();
var app2Location = icons[1].location();
var app3Location = icons[2].location();
var gridWidth = home.container.size().width;
// check that they are all on the same line
assert.equal(app1Location.y, app2Location.y,
'1st and 2nd apps should be on the same line.');
assert.equal(app2Location.y, app3Location.y,
'2st and 3nd apps should be on the same line.');
// check right-alignement
assert.ok(app1Location.x > gridWidth / 2, 'First app is right-aligned');
// check the ordering
assert.ok(app1Location.x > app2Location.x,
'App1 icon should be on the right of app2 icon');
assert.ok(app2Location.x > app3Location.x,
'App2 icon should be on the right of app3 icon');
});
test('switching to 4-column layout works', function() {
// check that we're in 3-column layout
client.waitFor(function() {
var icons = home.visibleIcons;
var app1Location = icons[0].location();
var app2Location = icons[1].location();
var app3Location = icons[2].location();
var app4Location = icons[3].location();
return ((app1Location.y === app2Location.y) &&
(app2Location.y === app3Location.y) &&
(app4Location.y > app3Location.y));
});
// Switch the setting for 4-column layout
client.executeScript(function () {
navigator.getDataStores('homescreen_settings').then(
function(stores) {
stores[0].put(4, 'grid.cols');
});
});
// check that we're in 4-column layout
client.waitFor(function() {
var icons = home.visibleIcons;
var app1Location = icons[0].location();
var app2Location = icons[1].location();
var app3Location = icons[2].location();
var app4Location = icons[3].location();
return ((app1Location.y === app2Location.y) &&
(app2Location.y === app3Location.y) &&
(app3Location.y === app4Location.y));
});
});
});

View file

@ -0,0 +1,455 @@
'use strict';
/**
* Abstraction around homescreen.
* @constructor
*/
function Homescreen(client) {
this.client = client;
this.system = client.loader.getAppClass('system');
}
Homescreen.URL = 'app://homescreen.gaiamobile.org';
Homescreen.Selectors = {
appsScrollable: '#apps-panel .scrollable',
apps: '#apps',
pages: '#pages',
icon: '#apps gaia-app-icon',
card: '#pages gaia-pin-card',
bottomBar: '#bottombar',
remove: '#remove',
rename: '#rename',
done: '#done',
cancelDownload: '#cancel-download',
resumeDownload: '#resume-download'
};
Homescreen.prototype = {
URL: Homescreen.URL,
Selectors: Homescreen.Selectors,
get appsScrollable() {
return this.client.findElement(Homescreen.Selectors.appsScrollable);
},
get container() {
return this.client.findElement(Homescreen.Selectors.apps);
},
get icons() {
return this.client.findElements(Homescreen.Selectors.icon);
},
get cards() {
return this.client.findElements(Homescreen.Selectors.card);
},
get visibleIcons() {
return this.icons.filter(function(el) {
return el.scriptWith(function(el) {
return el.parentNode.style.display !== 'none';
});
});
},
get visibleCards() {
return this.client.executeScript(function(selector) {
var cards = document.body.querySelectorAll(selector);
var visibles = [];
for (var i = cards.length - 1; i >= 0; i--) {
if (cards[i].parentNode.style.display !== 'none') {
visibles.push(cards[i]);
}
}
return visibles;
}, [Homescreen.Selectors.card]);
},
get removeButton() {
return this.client.helper.waitForElement(Homescreen.Selectors.remove);
},
get renameButton() {
return this.client.helper.waitForElement(Homescreen.Selectors.rename);
},
get doneButton() {
return this.client.helper.waitForElement(Homescreen.Selectors.done);
},
get cancelDownloadDialog() {
return this.client.findElement(Homescreen.Selectors.cancelDownload);
},
get resumeDownloadDialog() {
return this.client.findElement(Homescreen.Selectors.resumeDownload);
},
/**
* Waits for the homescreen to launch and switches to the frame.
*/
waitForLaunch: function() {
var client = this.client;
client.helper.waitForElement('body');
client.apps.switchToApp(Homescreen.URL);
// Wait for startup to complete
client.waitFor(function() {
return client.executeScript(function() {
return window.wrappedJSObject.appWindow.apps.startupMetadata === null;
});
});
},
/**
* Waits for the edit bar to fully appear.
*/
waitForEditBar: function() {
var body = this.client.findElement('body');
var bar = this.client.helper.waitForElement(Homescreen.Selectors.bottomBar);
this.client.waitFor(function() {
var barRect = bar.rect();
return barRect.y === (body.rect().height - barRect.height);
});
},
/**
* Find and return every id for all the items on the grid... Each element
* can be used with `.getIcon` to find the element for a given id.
*
* @return {Array[String]}
*/
getIconIdentifiers: function() {
return this.visibleIcons.map(function(el) {
return this.getIconId(el);
}, this);
},
/**
* Fetch an icon element on the homescreen by its identifier.
* For apps, the identifier is the manifestURL, or its manifestURL,
* followed by a '/' followed by its entry point. For bookmarks, the
* identifier is the bookmarked URL.
*
* @param {String} identifier The identifier of the icon.
* @return {Marionette.Element}
*/
getIcon: function(identifier) {
return this.client.findElement(
Homescreen.Selectors.apps + ' [data-identifier*="' + identifier + '"]');
},
/**
* Fetch a card element on the homescreen by its identifier.
* For apps, the identifier is the manifestURL, or its manifestURL,
* followed by a '/' followed by its entry point. For pin, the
* identifier is the pinned URL.
*/
getCard: function(identifier) {
return this.client.findElement(
Homescreen.Selectors.pages + ' [data-id="' + identifier + '"]');
},
/**
* Fetch an icon element on the homescreen by its name.
*
* @param {String} name The name of the icon.
* @return {Marionette.Element}
*/
getIconByName: function(name) {
function getName(icon) {
return icon.shadowRoot.querySelector('#subtitle').textContent;
}
var icons = this.icons;
for (var i = 0, iLen = icons.length; i < iLen; i++) {
if (icons[i].scriptWith(getName) === name) {
return icons[i];
}
}
return null;
},
/**
* Returns a homescreen icon element's identifier.
*
* @param {Marionette.Element} icon A homescreen icon element reference.
*/
getIconId: function(icon) {
return icon.scriptWith(function(el) {
return el.dataset.identifier;
});
},
/**
* Returns a homescreen icon element's text.
*
* @param {Marionette.Element} icon A homescreen icon element reference.
*/
getIconText: function(icon) {
return icon.scriptWith(function(el) {
return el.shadowRoot.querySelector('#subtitle').textContent;
});
},
/**
* Returns a data-url of the image element in a homescreen icon, if it exists.
*
* @param {Marionette.Element} icon A homescreen icon element reference.
*/
getIconImage: function(icon) {
return icon.scriptWith(function(el) {
var image = el.shadowRoot.querySelector('#image-container img');
if (!image) {
return null;
}
var canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
var ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(image, 0, 0);
return canvas.toDataURL();
});
},
/**
* Returns a homescreen icon element's image URL.
*
* @param {Marionette.Element} icon A homescreen icon element reference.
*/
getIconImageUrl: function(icon) {
return icon.scriptWith(function(el) {
return el.dataset.testIconUrl;
});
},
/**
* Returns a homescreen card element's image URL.
*
* @param {Marionette.Element} card A homescreen card element reference.
*/
getCardImageUrl: function(card) {
return card.scriptWith(function(el) {
var icon = el.shadowRoot.querySelector('.icon-container i');
return icon.style.backgroundImage;
});
},
/**
* Waits for an icon's image URL to contain a certain value.
*
* @param {Marionette.Element} icon A homescreen icon element reference.
* @0aram {String} string A string to search for in the image URL.
*/
waitForIconImageUrl: function(icon, string) {
this.client.waitFor(function() {
var src = this.getIconImageUrl(icon);
return src && src.indexOf(string) !== -1;
}.bind(this));
},
/**
* Returns true if a homescreen icon is in a loading state, false otherwise.
*
* @param {Marionette.Element} icon A homescreen icon element reference.
*/
iconIsLoading: function(icon) {
return icon.scriptWith(function(el) {
return el.shadowRoot.querySelector('#image-container').
classList.contains('downloading');
});
},
/**
* Launches an icon and switches to the corresponding application.
*
* @param {Marionette.Element} icon A homescreen icon element reference.
*/
launchIcon: function(icon) {
var identifier =
icon.scriptWith(function(el) { return el.dataset.identifier; });
icon.tap();
var identifierWithoutEntryPoint = identifier.replace(/\/[^\/]*$/, '');
var client = this.client.scope({ searchTimeout: 100 });
var frame;
client.waitFor(function() {
// wait for the app to show up
client.switchToFrame();
try {
frame = client.findElement('iframe[mozapp="' + identifier + '"]');
} catch (e1) {
try {
frame = client.findElement(
'iframe[mozapp="' + identifierWithoutEntryPoint + '"]');
} catch(e2) {
try {
frame = client.findElement(
'iframe[data-frame-origin*="' + identifier + '"]');
} catch (e3) {
// try again...
client.switchToFrame(this.system.getHomescreenIframe());
icon.tap();
return false;
}
}
}
client.switchToFrame(frame);
return true;
}.bind(this));
},
/**
* Scrolls the homescreen so that the given icon is located roughly in
* the center of the screen, vertically.
*
* @param {Marionette.Element} icon A homescreen icon element reference.
*/
scrollIconToCenter: function(icon) {
this.client.executeScript(function(icon, scrollable, position) {
var midPoint = scrollable.clientHeight / 2;
scrollable.scrollTop = scrollable.scrollTop + (position - midPoint);
}, [icon, this.appsScrollable, icon.location().y]);
},
/**
* Restart the homescreen then refocus on it.
*/
restart: function() {
this.client.executeScript(function() {
window.close();
});
// initialize our frames again since we killed the iframe
this.client.switchToFrame();
this.client.switchToFrame(this.system.getHomescreenIframe());
},
/**
* Perform an action in an action dialog.
*
* @param {Marionette.Element} dialog An action dialog element reference.
* @param {String} action The data-l10n-id of the action to perform.
*/
actionDialog: function(dialog, action) {
var button = this.client.helper.waitForElement(
'button[data-l10n-id="' + action + '"]');
button.tap();
this.client.helper.waitForElementToDisappear(button);
},
/**
* Click confirm on a particular type of confirmation dialog.
*
* @param {String} type of dialog.
* @param {String} selector of the button. Defaults to .confirm.
*/
confirmDialog: function(type, button) {
var selector = 'gaia-confirm[data-type="' + type + '"]';
var dialog = this.client.helper.waitForElement(selector);
var confirm;
this.client.waitFor(function() {
confirm = dialog.findElement(button || '.confirm');
return confirm && confirm.displayed();
});
// XXX: Hack to use faster polling
var quickly = this.client.scope({ searchTimeout: 50 });
confirm.client = quickly;
// tricky logic to ensure the dialog has been removed and clicked
this.client.waitFor(function() {
try {
// click the dialog to dismiss it
confirm.click();
// ensure it is either hidden or hits the stale element ref
return !confirm.displayed();
} catch (e) {
if (e.type === 'StaleElementReference') {
// element was successfully removed
return true;
}
throw e;
}
});
},
/**
* Emulates pressing of the hardware home button.
*/
pressHomeButton: function() {
this.client.executeScript(function() {
var home = new CustomEvent('home');
window.dispatchEvent(home);
});
this.client.apps.switchToApp(Homescreen.URL);
},
/**
* Gets a localized application name from a manifest.
* @param {String} app to open
* @param {String} entryPoint to open
* @param {String} locale
*/
localizedAppName: function(app, entryPoint, locale) {
if (!locale) {
locale = entryPoint;
entryPoint = null;
}
var file = 'app://' + app + '.gaiamobile.org/manifest.webapp';
// use a chrome-scoped Marionette client for the cross-domain XHR
var chromeClient = this.client.scope({context: 'chrome'});
var manifest = chromeClient.executeAsyncScript(function(file) {
var xhr = new XMLHttpRequest();
xhr.open('GET', file, true);
xhr.onload = function(o) {
var data = JSON.parse(xhr.response);
marionetteScriptFinished(data);
};
xhr.send(null);
}, [file]);
var locales;
if (entryPoint) {
locales = manifest.entry_points[entryPoint].locales;
} else {
locales = manifest.locales;
}
if (!locales) {
return false;
}
if (locale.indexOf('-x-ps') > -1) {
return this.client.executeScript(function(locale, name) {
var mozL10n = window.wrappedJSObject.navigator.mozL10n;
return mozL10n.qps[locale].translate(name);
}, [locale, locales['en-US'].name]);
}
return locales[locale].name;
},
/**
* Returns a localized string from a properties file.
* @param {String} key of the string to lookup.
*/
l10n: function(key) {
var string = this.client.executeScript(function(key) {
return window.wrappedJSObject.navigator.mozL10n.get(key);
}, [key]);
return string;
}
};
module.exports = Homescreen;

View file

@ -0,0 +1,39 @@
'use strict';
var assert = require('assert');
marionette('Homescreen - Localization', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var actions, home, system;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('Localization updates icon names', function() {
var settingsIcon = home.getIconByName('Settings');
assert.equal(
home.getIconText(settingsIcon),
home.localizedAppName('settings', 'en-US'));
client.switchToFrame();
client.executeScript(function() {
navigator.mozSettings.createLock().set({
'language.current': 'fr-x-psaccent'
});
});
client.switchToFrame(system.getHomescreenIframe());
// Localization can be async, wait for the content to update
var newIconName = home.localizedAppName('settings', 'fr-x-psaccent');
client.waitFor(function() {
return !!home.getIconByName(newIconName);
});
});
});

View file

@ -0,0 +1,61 @@
'use strict';
/* global __dirname */
var Pinning = require(
'../../../../apps/system/test/marionette/lib/pinning_the_web');
var Bookmark = require('../../../../apps/system/test/marionette/lib/bookmark');
var Server = require('../../../../shared/test/integration/server');
marionette('Homescreen - Pinned Site Edit', function() {
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var actions, bookmark, pinning, home, server, system;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
var url;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
pinning = new Pinning(client);
bookmark = new Bookmark(client, server);
system.waitForFullyLoaded();
home.waitForLaunch();
url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinSiteFromBrowser(url);
system.dismissBanner();
system.tapHome();
home.waitForLaunch();
});
test('pressing enter after renaming the pinned site', function() {
var icon = home.getIcon(url);
home.scrollIconToCenter(icon);
actions.longPress(icon, 1).perform();
home.waitForEditBar();
home.renameButton.tap();
client.switchToFrame();
bookmark.renameAndPressEnter('renamed');
client.switchToFrame(system.getHomescreenIframe());
client.waitFor(function() {
return home.getIconText(icon) === 'renamed';
});
});
});

View file

@ -0,0 +1,50 @@
'use strict';
/* global __dirname */
var Pinning = require(
'../../../../apps/system/test/marionette/lib/pinning_the_web');
var Server = require('../../../../shared/test/integration/server');
var assert = require('assert');
marionette('Homescreen - Pinning Favicon Failure', function() {
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var pinning, home, server, system;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/invalid_favicon/',
function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
var url;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
pinning = new Pinning(client, server);
system.waitForFullyLoaded();
home.waitForLaunch();
url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinPage(url);
system.tapHome();
client.switchToFrame(system.getHomescreenIframe());
});
test('Invalid icons get default icon assigned', function() {
var card = home.getCard(url);
assert.equal('', home.getCardImageUrl(card), 'Card has no bg image');
});
});

View file

@ -0,0 +1,90 @@
'use strict';
/* global __dirname */
var assert = require('assert');
var Pinning = require(
'../../../../apps/system/test/marionette/lib/pinning_the_web');
var Server = require('../../../../shared/test/integration/server');
marionette('Homescreen - Pin order', function() {
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var actions, pinning, home, server, system;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
setup(function() {
actions = client.loader.getActions();
pinning = new Pinning(client);
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('Pin order is retained after restart', function() {
// This test is very similar to app_order_test, but as bookmarks are
// sourced from a different database and initialised separately to apps,
// we need to test that their metadata is also retained across homescreen
// restarts.
// Add a bookmark
var numIcons = home.visibleIcons.length;
var url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinSiteFromBrowser(url);
system.tapHome();
client.switchToFrame(system.getHomescreenIframe());
client.waitFor(function() {
return numIcons + 1 === home.visibleIcons.length;
});
// Drag pin to a different place
var icons = home.visibleIcons;
icons[numIcons].scriptWith(function(el) {
el.scrollIntoView();
});
var location1 = icons[numIcons].location();
var location2 = icons[numIcons - 1].location();
actions.wait(0.5).press(icons[numIcons]).wait(0.5).
move(icons[numIcons - 1]).release().wait(0.5).perform();
assert.equal(icons[numIcons].location().x, location2.x);
assert.equal(icons[numIcons - 1].location().x, location1.x);
// Test that icon order is retained after restart
icons = home.visibleIcons.map(function(icon) {
return home.getIconText(icon);
});
numIcons = icons.length;
home.restart();
client.waitFor(function() {
return home.visibleIcons.length === numIcons;
});
var newIcons = home.visibleIcons.map(function(icon) {
return home.getIconText(icon);
});
for (var i = 0, iLen = icons.length; i < iLen; i++) {
assert.equal(icons[i], newIcons[i]);
}
});
});

View file

@ -0,0 +1,49 @@
'use strict';
/* global __dirname */
var Pinning = require(
'../../../../apps/system/test/marionette/lib/pinning_the_web');
var Server = require('../../../../shared/test/integration/server');
marionette('Homescreen - Pinned Sites', function() {
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var pinning, home, server, system;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
setup(function() {
home = client.loader.getAppClass('homescreen');
pinning = new Pinning(client);
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('Pinning a site adds pin to homescreen', function() {
var numIcons = home.visibleIcons.length;
var url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinSiteFromBrowser(url);
system.dismissBanner();
system.tapHome();
client.switchToFrame(system.getHomescreenIframe());
client.waitFor(function() {
return numIcons + 1 === home.visibleIcons.length;
});
});
});

View file

@ -0,0 +1,74 @@
'use strict';
/* global __dirname */
var Server = require('../../../../shared/test/integration/server');
var Rocketbar = require('../../../system/test/marionette/lib/pinning_the_web');
marionette('Homescreen - Pin the web', function() {
var options = require(__dirname + '/client_options.js');
options.settings['layers.async-pan-zoom.enabled'] = false;
options.settings['dev.gaia.pinning_the_web'] = true;
var client = marionette.client({
profile: options,
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, server, system, pinning;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
pinning = new Rocketbar(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
// Skip test since we are disabling pinning door hanger in 2.5
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1207710
test.skip('Pinning a site from the site icon', function() {
var url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinSite(url);
system.tapHome();
client.switchToFrame(system.getHomescreenIframe());
home.getIcon(url);
});
test('Pinning a site from the browser context menu', function() {
var url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinSiteFromBrowser(url);
system.tapHome();
client.switchToFrame(system.getHomescreenIframe());
home.getIcon(url);
});
test('Pinning a page adds a card to the homescreen', function() {
var url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinPage(url);
system.tapHome();
client.switchToFrame(system.getHomescreenIframe());
client.waitFor(function() {
return home.visibleCards.length === 1;
});
});
});

View file

@ -0,0 +1,76 @@
'use strict';
/* global __dirname */
var assert = require('assert');
var Pinning = require(
'../../../../apps/system/test/marionette/lib/pinning_the_web');
var Bookmark = require('../../../../apps/system/test/marionette/lib/bookmark');
var Server = require('../../../../shared/test/integration/server');
marionette('Homescreen - Pinned Site Uninstall', function() {
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var actions, pinning, home, server, system, bookmark;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
var url;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
pinning = new Pinning(client);
bookmark = new Bookmark(client);
system.waitForFullyLoaded();
home.waitForLaunch();
url = server.url('sample.html');
client.switchToFrame();
pinning.openAndPinSiteFromBrowser(url);
system.dismissBanner();
system.tapHome();
home.waitForLaunch();
});
test('removal of pinned site', function() {
var icon = home.getIcon(url);
home.scrollIconToCenter(icon);
actions.longPress(icon, 1).perform();
home.waitForEditBar();
home.removeButton.tap();
// Confirm the dialog to ensure it was removed.
// Note, remove dialog for pins still lives in bookmark app.
client.switchToFrame();
client.switchToFrame(system.getAppIframe(Bookmark.URL));
bookmark.pinConfirmDialog('remove');
// ensure the icon disappears
client.switchToFrame();
client.switchToFrame(system.getHomescreenIframe());
client.helper.waitForElementToDisappear(icon);
// ensure pinned site is gone upon restart
home.restart();
client.setSearchTimeout(20);
icon = null;
try {
icon = home.getIcon(server.packageManifestURL);
} catch(e) { }
assert.ok(!icon, 'pinned site was not removed');
});
});

View file

@ -0,0 +1,315 @@
/* global Buffer, process */
'use strict';
/**
App server for fixtures. Serves up packaged apps.
*/
var debug = require('debug')('appserver:child');
var http = require('http');
var fs = require('fs');
var fsPath = require('path');
var mime = require('mime');
var archiver = require('archiver');
var EventEmitter = require('events').EventEmitter;
function readdirRecursive(dir) {
dir = fsPath.join(dir, '/');
var results = [];
var files = fs.readdirSync(dir);
var file;
while((file = files.shift())) {
file = fsPath.join(dir, file);
var stat = fs.statSync(file);
if (stat.isDirectory()) {
results = results.concat(readdirRecursive(file));
} else {
results.push(file);
}
}
return results;
}
function writeJSON(res, object) {
var json = JSON.stringify(object);
res.writeHead(200, {
'Content-Length': Buffer.byteLength(json),
'Content-Type': 'application/json'
});
res.end(json);
}
function decorateHandlerForJSON(method) {
return function(req, res) {
// if it is not json just skip it
var type = req.headers['content-type'];
if (!type || type.indexOf('json') === -1) {
return method.call(this, req, res);
}
var buffer = '';
req.on('data', function(data) {
buffer += data;
});
req.once('end', function() {
req.body = JSON.parse(buffer);
method.call(this, req, res);
}.bind(this));
};
}
function defaultSettings() {
return {
corked: false,
fail: false
};
}
/**
* @param {String} root directory where content is.@
* @param {Object} routes for the app server.
* @constructor
*/
function AppServer(root, routes) {
EventEmitter.call(this);
// ensure we always have the slash suffix
if (root[root.length - 1] !== '/') {
root += '/';
}
this.root = root;
this.routes = routes;
this._settings = {};
}
AppServer.prototype = {
__proto__: EventEmitter.prototype,
/**
* @return {Object} default settings for a given url.
*/
settings: function(url) {
return (this._settings[url] = this._settings[url] || defaultSettings());
},
requestHandler: function() {
var routes = this.routes;
return function(req, res) {
debug('request', req.method, req.url);
var handler = routes[req.url] || routes['*'];
var settings = this.settings(req.url);
// If this url has a http error on it just return without calling the
// handler.
if (settings.response) {
res.writeHead(settings.response.status, settings.response.headers);
res.end(settings.response.body);
return;
}
// routes are always invoked with the context of the app server.
return handler.call(this, req, res);
}.bind(this);
},
};
var routes = {
/**
Change the root serving directory for this server.
*/
'/settings/set_root': decorateHandlerForJSON(function(req, res) {
this.root = req.body;
writeJSON(res, req.body);
}),
/**
Override or set the response of a particular url.
*/
'/settings/set_response': decorateHandlerForJSON(function(req, res) {
var options = req.body;
var settings = this.settings(options.path);
settings.response = options;
writeJSON(res, options.path);
}),
/**
Clear the response override.
*/
'/settings/clear_response': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
delete settings.response;
writeJSON(res, url);
}),
/**
A 'corked' request will send headers but no body until 'uncork' is used.
*/
'/settings/cork': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
settings.corked = true;
// issue a cork to the particular endpoint
var event = 'corked ' + url;
debug('cork event', event);
this.emit('corked ' + url);
writeJSON(res, url);
}),
/**
The inverse of 'cork' will uncork a uri and send the body through.
*/
'/settings/uncork': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
settings.corked = false;
var event = 'uncorked ' + url;
debug('uncork event', event);
this.emit(event);
writeJSON(res, url);
}),
/**
Fail is a combination of 'cork' and a flag which will cause the incoming http
request to be closed after a short delay.
*/
'/settings/fail': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
// must be corked to ensure we don't send the body
settings.corked = true;
settings.fail = true;
writeJSON(res, url);
}),
/**
The inverse to fail but unlike uncork this cannot be called in the middle of
a request (for obvious reasons).
*/
'/settings/unfail': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
settings.fail = settings.corked = false;
writeJSON(res, url);
}),
'/package.manifest': function(req, res) {
var json = JSON.parse(fs.readFileSync(this.root + '/manifest.webapp'));
json.package_path = 'http://' + req.headers.host + '/app.zip';
// TODO: Remove this once we add the ability to override requests
delete json.role;
var body = JSON.stringify(json, null, 2);
res.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'application/x-web-app-manifest+json'
});
res.end(body);
},
'/app.zip': function(req, res) {
var url = req.url;
var settings = this.settings(url);
// always write the head we need it in all cases
res.writeHead(200, {
'Content-Type': 'application/zip'
});
var root = this.root;
function writeZip() {
// read the entire app directory
var files = readdirRecursive(root);
var zip = archiver.create('zip');
files.forEach(function(file) {
debug('packaged app zip file', file);
var zipPath = file.replace(root, '');
zip.append(fs.createReadStream(file), { name: zipPath });
});
zip.finalize();
zip.pipe(res);
}
if (settings.fail) {
process.nextTick(function() {
req.socket.destroy();
});
return;
}
// if we are not corked just write the zip and we are done
if (!settings.corked) {
return writeZip();
} else {
this.once('uncorked ' + url, writeZip);
}
},
/**
Catch all handler which serves up static assets.
*/
'*': function(req, res) {
var url = req.url;
var settings = this.settings(url);
var filePath = fsPath.join(this.root, url);
// XXX: This is not a production quality server don't use sync methods in
// in servers in your user facing node code.
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end();
return;
}
var contentType = mime.lookup(filePath);
var contentLength = fs.statSync(filePath).size;
debug('static asset', filePath, contentType, contentLength);
// write out the head
res.writeHead(200, {
'Content-Type': contentType,
'Content-Length': contentLength
});
function writeContent() {
fs.createReadStream(filePath).pipe(res);
}
if (settings.fail) {
process.nextTick(function() {
req.socket.destroy();
});
return;
}
if (!settings.corked) {
// if we are not corked just return the stream
return writeContent();
} else {
// otherwise wait for the uncork
this.once('uncorked ' + url, writeContent);
}
}
};
var port = parseInt((process.argv[3] || 0), 10);
var appServer = new AppServer(process.argv[2], routes);
var server = http.createServer(appServer.requestHandler());
server.listen(port, function() {
if (process.send) {
process.send({ type: 'started', port: server.address().port });
}
});

View file

@ -0,0 +1,74 @@
/* global process, __dirname */
'use strict';
// server cli stuff
var fork = require('child_process').fork;
var fs = require('fs');
var fsPath = require('path');
function exit() {
console.error();
console.error.apply(console, arguments);
commandHelp(console.error);
process.exit(1);
}
function commandHelp(log) {
log = log || console.log;
log();
log(' App server cli');
log();
log(' Comamnds: ');
log();
log(' help - Your looking at it');
log(' interactive <directory> [port] - ' +
'Begin an interactive session with the app server');
log();
}
function commandInteractive(directory, port) {
port = parseInt((port || 60234), 10);
if (!directory) {
return exit('Directory must be given');
}
directory = fsPath.resolve(directory);
if (!fs.existsSync(directory)) {
return exit('Given app directory "%s" does not exist', directory);
}
// start the server up
console.log('Starting server...');
function onError(e) {
exit('server process failed to start: ', e);
}
var proc = fork(__dirname + '/child.js', [directory, port]);
proc.once('error', onError);
proc.on('message', function(msg) {
if (msg.type !== 'started') {
return;
}
proc.removeListener('error', onError);
var interactive = require('./cli/interactive');
interactive(proc, 'http://localhost:' + msg.port + '/');
});
}
switch (process.argv[2]) {
case 'interactive':
commandInteractive(process.argv[3]);
break;
case 'help':
commandHelp();
break;
default:
commandHelp();
}

View file

@ -0,0 +1,175 @@
/* global process, Buffer, module */
'use strict';
var http = require('http');
var url = require('url');
function Interactive(serverProc, url, commands) {
this.server = serverProc;
this.url = url;
this.stream = process.stdin;
this.commands = commands;
}
Interactive.prototype = {
help: function() {
console.log(' Commands: ');
console.log();
Object.keys(this.commands).forEach(function(command) {
var desc = this.commands[command].description;
console.log(' %s - %s', command, desc);
}, this);
console.log();
},
get: function(uri) {
var req = http.get(this.url + uri);
console.log('>> GET ' + uri);
req.once('response', function(res) {
console.log(' response %s [%s]', uri, res.statusCode);
for (var key in res.headers) {
console.log(' response header %s : %s', key, res.headers[key]);
}
}.bind(this));
},
post: function(uri, body, callback) {
var json = JSON.stringify(body);
var options = url.parse(this.url + uri);
options.method = 'POST';
var req = http.request(options);
req.setHeader('Content-Length', Buffer.byteLength(json));
req.setHeader('Content-Type', 'application/json');
req.once('response', function(res) {
var data = '';
res.on('data', function(buffer) {
data += buffer;
});
res.once('end', function() {
callback(null, JSON.parse(data));
});
});
req.end(json);
},
onReadable: function() {
var stream = this.stream;
var content = stream.read();
// noting to read return...
if (!content) {
return;
}
// read until we have a newline
if (content.indexOf('\n') === -1) {
stream.unshift(content);
return;
}
// ignore blank lines
if (content.trim() === '') {
return;
}
// the first word is the command parse it out...
var commandParts = content.split(' ').map(function(part) {
return part.trim();
});
var commandName = commandParts.shift();
var command = this.commands[commandName];
if (!command) {
console.error(commandName, ' is not a recognized command');
return this.help();
}
command.handler.apply(this, commandParts);
},
start: function() {
// don't stop reading from stdin
this.stream.setEncoding('utf8');
this.stream.on('readable', this.onReadable.bind(this));
},
};
var commands = {
help: {
description: 'Shows all commands',
handler: function() { this.help(); }
},
cork: {
description: '<uri> Cork a particular url',
handler: function(uri) {
// leading slash for consistency with get
this.post('settings/cork', '/' + uri, function(err, body) {
console.log('corked : %s', body);
});
}
},
uncork: {
description: '<uri> uncork a particular uri',
handler: function(uri) {
// leading slash for consistency with get
this.post('settings/uncork', '/' + uri, function(err, body) {
console.log('uncorked : %s', body);
});
}
},
fail: {
description: '<uri> Cause a particular url to fail',
handler: function(uri) {
// leading slash for consistency with get
this.post('settings/fail', '/' + uri, function(err, body) {
console.log('failed: %s', body);
});
}
},
unfail: {
description: '<uri> remove failng status from particular url',
handler: function(uri) {
// leading slash for consistency with get
this.post('settings/unfail', '/' + uri, function(err, body) {
console.log('unfail: %s', body);
});
}
},
get: {
description: '<uri> Issue a http get an show the body',
handler: function(uri) {
this.get(uri);
}
},
url: {
description: 'display the server url',
handler: function() {
console.log(this.url);
},
}
};
function main(process, url) {
var handler = new Interactive(process, url, commands);
handler.start();
process.once('SIGINT', function() {
process.stderr.write('Shutting down...\n');
process.exit();
});
}
module.exports = main;

View file

@ -0,0 +1,191 @@
'use strict';
/* global module, __dirname */
var fork = require('child_process').fork;
/**
* Issue a POST request via marionette.
*/
function post(client, url, json) {
// must run in chrome so we can do cross domain xhr
client = client.scope({ context: 'chrome' });
return client.executeAsyncScript(function(url, json) {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
marionetteScriptFinished(xhr.response);
};
xhr.send(json);
}, [url, JSON.stringify(json)]);
}
function AppServer(root, marionette, port, proc) {
this.root = root;
this.marionette = marionette;
this.url = 'http://localhost:' + port;
this.process = proc;
}
AppServer.prototype = {
/**
issues a GET request to the server and returns the parsed body.
*/
get: function(uri) {
// must run in chrome so we can do cross domain xhr
var client = this.marionette.scope({ context: 'chrome' });
return client.executeAsyncScript(function(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
marionetteScriptFinished(xhr.response);
};
xhr.send();
}, [this.url + uri]);
},
/**
Changes the root of the server.
@param {String} path relative to the file system.
*/
setRoot: function(path) {
return post(this.marionette, this.url + '/settings/set_root', path);
},
/**
Set the http response of a particular url.
@param {String} path to set the response of.
@param {Number} status code to set.
@param {Object} headers to override.
@param {String} body of the request.
*/
setResponse: function(path, status, headers, body) {
var options = {
path: path,
status: status,
headers: headers || {},
body: body
};
return post(this.marionette, this.url + '/settings/set_response', options);
},
/**
Clear the response override set by `setResponse`
*/
clearResponse: function(url) {
return post(
this.marionette, this.url + '/settings/clear_response', url
);
},
/**
Trigger a 500 error for a particular url and return an empty body.
@param {String} url to fail (/index.html).
*/
serverError: function(url) {
return this.setResponse(url, 500, {}, '');
},
/**
Clears the server error for a particular url.
@param {String} url to fail (/index.html).
*/
clearServerError: function(url) {
return this.clearResponse();
},
/**
Indicate to the server that all requests to the given url should be
given a response with headers but then the socket should be closed shortly
after that time.
@param {String} url to ban (/index.html).
*/
fail: function(url) {
return post(this.marionette, this.url + '/settings/fail', url);
},
/**
Allow requests to the given url to proceed without failure after calling
`.fail`.
@param {String} url to unban.
*/
unfail: function(url) {
return post(this.marionette, this.url + '/settings/unfail', url);
},
/**
Cork the response body of the given url while allowing headers.
@param {String} url to cork.
*/
cork: function(url) {
return post(this.marionette, this.url + '/settings/cork', url);
},
/**
Allow the body to be sent after calling `.cork`.
@param {String} url to uncork.
*/
uncork: function(url) {
return post(this.marionette, this.url + '/settings/uncork', url);
},
close: function(callback) {
this.process.kill();
this.process.once('exit', callback.bind(this, null));
},
get manifest() {
return JSON.parse(this.get('/manifest.webapp'));
},
/**
URI where the application zip lives this defined in child.js
*/
get applicationZipUri() {
return '/app.zip';
},
get manifestURL() {
return this.url + '/manifest.webapp';
},
get packageManifestURL() {
return this.url + '/package.manifest';
},
get webappUpdateURL() {
return this.url + '/update.webapp';
}
};
/**
* Create a app server for use in marionette tests.
*
* @param {String} appRoot path to the root of the app.
* @param {Marionette.Client} client for marionette.
* @param {Function} callback [Error]
*/
module.exports = function create(appRoot, client, callback) {
var proc = fork(__dirname + '/child.js', [appRoot]);
proc.once('error', callback);
proc.on('message', function(msg) {
if (msg.type !== 'started') {
return;
}
proc.removeListener('error', callback);
callback(null, new AppServer(appRoot, client, msg.port, proc));
});
};
module.exports.AppServer;

View file

@ -0,0 +1,60 @@
'use strict';
/* global __dirname */
var assert = require('assert');
marionette('Homescreen - verticalhome migration', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options_verticalhome.js')
});
var actions, verticalhome, home, system;
setup(function() {
actions = client.loader.getActions();
verticalhome = client.loader.getAppClass('verticalhome');
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
verticalhome.waitForLaunch();
});
test('App order is imported from verticalhome', function() {
// Drag the second icon to the first position so we differ from the
// default settings.
var icon1 = verticalhome.getNthIcon(1);
var icon2 = verticalhome.getNthIcon(2);
actions.press(icon2).wait(1).move(icon1).release().wait(1).perform();
verticalhome.exitEditMode();
// Restart the home screen so that icon DOM order corresponds with actual
// order.
verticalhome.restart();
// Record the names of all the apps and bookmarks (these are the only
// items that are migrated)
var iconNames =
client.findElements('#icons div.icon:not(.placeholder)').
map(function(icon) {
return icon.findElement('.title').text();
});
// Request migration to new home screen
client.executeScript(function() {
window.wrappedJSObject.app.itemStore.migrate(
'app://homescreen.gaiamobile.org/manifest.webapp');
});
client.switchToFrame();
home.waitForLaunch();
// Verify icon order matches
var importedIconNames = home.visibleIcons.map(function(icon) {
return home.getIconText(icon);
});
var nIcons = iconNames.length;
assert.equal(importedIconNames.length, nIcons);
for (var i = 0; i < nIcons; i++) {
assert.equal(importedIconNames[i], iconNames[i]);
}
});
});

View file

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

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<body>
<header id="page-indicator-header"><div id="page-indicator"><span></span><span></span></div></header>
<div id="shadow"></div>
<div id="panels">
<div id="apps-panel">
<div class="scrollable">
<div id="apps"></div>
</div>
<div id="cancel-download" class="dialog">
<section>
<p class="body">stop-download-body</p>
</section>
<button class="action">stop-download-action</button>
<button>cancel-action</button>
</div>
<div id="resume-download" class="dialog">
<section>
<p class="body">resume-download-body</p>
</section>
<button class="action">resume-download-action</button>
<button>cancel-action</button>
</div>
</div>
<div id="pages-panel">
<div class="scrollable">
<div id="pages"></div>
</div>
</div>
<div id="bottombar">
<div id="remove"></div>
<div id="rename"></div>
<div id="done"></div>
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,271 @@
/* global MockIndexedDB, AppsMetadata */
'use strict';
require('/shared/test/unit/mocks/mock_indexedDB.js');
require('/js/appsmetadata.js');
suite('AppsMetadata', () => {
var mockIndexedDB;
var metadata;
setup(() => {
mockIndexedDB = new MockIndexedDB();
metadata = new AppsMetadata();
});
teardown(() => {
metadata = null;
mockIndexedDB.mTearDown();
});
suite('AppsMetadata#init()', () => {
test('should open an indexedDB instance', done => {
assert.equal(mockIndexedDB.dbs.length, 0);
metadata.init().then(() => {
assert.equal(mockIndexedDB.dbs.length, 1);
done();
});
});
});
suite('AppsMetadata#upgradeSchema()', () => {
var names = [];
var onUpgradeNeededEvent = version => {
return {
target: {
result: {
createObjectStore: (name, options) => {
names.push(name);
return { createIndex: (name, keyPath, options) => {} };
}
}
},
oldVersion: version
};
};
setup(() => {
names = [];
});
test('should create 2 object stores with older versions', () => {
metadata.upgradeSchema(onUpgradeNeededEvent(0));
assert.equal(names.length, 2);
assert.isTrue(names.indexOf('order') > -1);
assert.isTrue(names.indexOf('icon') > -1);
});
test('should not create object stores with newer versions', () => {
metadata.upgradeSchema(onUpgradeNeededEvent(2));
assert.equal(names.length, 0);
});
});
suite('indexedDB backed methods', () => {
var objectStores = {};
var deletedValues = [];
var attachDBTransaction = objectStoreName => {
metadata.db.transaction = (objectStoreNames, IDBTransactionMode) => {
return {
objectStore: name => {
return {
put: value => {
if (objectStoreName && name !== objectStoreName) {
return;
}
objectStores[name] = value;
},
get: id => {
return {
set onsuccess(cb) {
if (name === 'icon') {
switch(id) {
case 'abc/':
cb({ target: { result: { id: 'abc/', icon: 'abc' } } });
return;
case 'def/':
cb({ target: { result: { id: 'def/', icon: 'def' } } });
return;
}
}
cb({ target: {} });
}
};
},
delete: value => {
if (name !== objectStoreName) {
return;
}
deletedValues.push(value);
},
clear: () => {
if (name !== objectStoreName) {
return;
}
},
openCursor: () => {
return {
set onsuccess(cb) {
if (objectStoreNames.indexOf(name) === -1) {
return;
}
var value = (name === 'order') ?
{ id: 'abc/', order: 0 } :
{ id: 'abc/', icon: 'abc' };
cb({
target: {
result: {
value: value,
continue: () => {
var value = (name === 'order') ?
{ id: 'def/', order: 1 } :
{ id: 'def/', icon: 'def' };
cb({
target: {
result: {
value: value,
continue: () => {}
}
}
});
}
}
}
});
}
};
},
index: function() {
return this;
}
};
},
set oncomplete(cb) {
cb();
},
onerror: () => {
}
};
};
};
setup(() => {
metadata.db = {};
objectStores = {};
deletedValues = [];
});
suite('AppsMetadata#set()', () => {
var setMetadata = (id, order, icon) => {
metadata.set(
[{
id: id,
order: order,
icon: icon
}]);
};
test('should ignore apps without id', () => {
attachDBTransaction('order');
var order = 0, icon = 'abc';
setMetadata(undefined, order, icon);
assert.equal(Object.keys(objectStores).length, 0);
});
test('should store apps with order', () => {
attachDBTransaction('order');
var id = 'abc/', order = 0;
setMetadata(id, order, undefined);
assert.equal(Object.keys(objectStores).length, 1);
assert.equal(objectStores.order.id, id);
assert.equal(objectStores.order.order, order);
});
test('should store apps with icon', () => {
attachDBTransaction('icon');
var id = 'abc/', icon = 'abc';
setMetadata(id, undefined, icon);
assert.equal(Object.keys(objectStores).length, 1);
assert.equal(objectStores.icon.id, id);
assert.equal(objectStores.icon.icon, icon);
});
test('updating the order should not change the icon', () => {
attachDBTransaction();
var id = 'abc/', order = 0, icon = 'abc';
setMetadata(id, order, icon);
assert.equal(objectStores.order.id, id);
assert.equal(objectStores.order.order, order);
assert.equal(objectStores.icon.id, id);
assert.equal(objectStores.icon.icon, icon);
order = 2;
setMetadata(id, order, undefined);
assert.equal(Object.keys(objectStores).length, 2);
assert.equal(objectStores.order.id, id);
assert.equal(objectStores.order.order, order);
assert.equal(objectStores.icon.id, id);
assert.equal(objectStores.icon.icon, icon);
});
test('updating the icon should not change the order', () => {
attachDBTransaction();
var id = 'abc/', order = 0, icon = 'abc';
setMetadata(id, order, icon);
assert.equal(objectStores.order.id, id);
assert.equal(objectStores.order.order, order);
assert.equal(objectStores.icon.id, id);
assert.equal(objectStores.icon.icon, icon);
icon = 'def';
setMetadata(id, undefined, icon);
assert.equal(Object.keys(objectStores).length, 2);
assert.equal(objectStores.order.id, id);
assert.equal(objectStores.order.order, order);
assert.equal(objectStores.icon.id, id);
assert.equal(objectStores.icon.icon, icon);
});
});
suite('AppsMetadata#remove()', () => {
var id = 'abc/';
test('should remove data from order object store', done => {
attachDBTransaction('order');
metadata.remove(id)
.then(() => {
done(() => {
assert.deepEqual(deletedValues, [id]);
});
});
});
test('should remove data from icon object store', done => {
attachDBTransaction('icon');
metadata.remove(id)
.then(() => {
done(() => {
assert.deepEqual(deletedValues, [id]);
});
});
});
});
suite('AppsMetadata#getAll()', () => {
test('should combine data from both object stores', done => {
attachDBTransaction('order');
metadata.getAll()
.then(results => {
done(() => {
assert.equal(results.length, 2);
assert.deepEqual(results[0],
{ id: 'abc/', order: 0, icon: 'abc' });
assert.deepEqual(results[1],
{ id: 'def/', order: 1, icon: 'def' });
});
});
});
});
});
});

View file

@ -0,0 +1,457 @@
/* global MockIndexedDB, MockNavigatorDatastore, mockLocalStorage, Datastore */
'use strict';
require('/shared/test/unit/mocks/mock_indexedDB.js');
require('/shared/test/unit/mocks/mock_navigator_datastore.js');
require('mocks/mock_localStorage.js');
require('/js/datastore.js');
suite('Datastore', () => {
var mockIndexedDB;
var realNavigatorDatastores;
var realLocalStorage;
var datastoreName = 'datastore_name_example';
var bookmarks;
setup(() => {
mockIndexedDB = new MockIndexedDB();
mockLocalStorage.mSetup();
Object.defineProperty(window, 'localStorage', {
configurable: true,
get: () => mockLocalStorage
});
realNavigatorDatastores = navigator.getDataStores;
navigator.getDataStores = MockNavigatorDatastore.getDataStores;
bookmarks = new Datastore(datastoreName);
});
teardown(() => {
mockIndexedDB.mTearDown();
Object.defineProperty(window, 'localStorage', {
configurable: true,
get: () => realLocalStorage
});
navigator.getDataStores = realNavigatorDatastores;
bookmarks = null;
});
suite('Datastore constructor', () => {
test('should store the name of the datastore', () => {
assert.equal(bookmarks.name, datastoreName);
assert.isTrue(bookmarks.revisionName.includes(datastoreName));
assert.isTrue(bookmarks.idbName.includes(datastoreName));
assert.isTrue(bookmarks.storeName.includes(datastoreName));
});
});
suite('Datastore#init()', () => {
test('should open indexedDB and Datastore instances', done => {
assert.isNull(bookmarks.db);
assert.isNull(bookmarks.datastore);
bookmarks.init().then(() => {
assert.isObject(bookmarks.db);
assert.isObject(bookmarks.datastore);
done();
});
});
});
suite('Datastore#upgradeSchema()', () => {
var names = [];
var onUpgradeNeededEvent = version => {
return {
target: {
result: {
createObjectStore: (name, options) => {
names.push(name);
return {
createIndex: (name, keyPath, options) => {}
};
}
}
},
oldVersion: version
};
};
setup(() => {
names = [];
});
test('should create an object store with older versions', () => {
bookmarks.upgradeSchema(onUpgradeNeededEvent(0));
assert.deepEqual(names, [bookmarks.storeName]);
});
test('should not create object stores with newer versions', () => {
bookmarks.upgradeSchema(onUpgradeNeededEvent(2));
assert.equal(names.length, 0);
});
});
suite('Datastore#synchronise()', () => {
var stub;
setup(done => {
// Bookmarks calls synchronise once the datastore is open and the
// event handler is registered.
stub = sinon.stub(bookmarks, 'synchronise', () => {
stub.restore();
done();
});
bookmarks.init();
});
teardown(() => {
stub.restore();
});
test('should call set() on update', done => {
stub = sinon.stub(bookmarks, 'set');
bookmarks.datastore._tasks[0] = {
operation: 'update',
id: 0,
data: {}
};
bookmarks.synchronise().then(() => {
assert.isTrue(stub.calledOnce);
done();
});
});
test('should call set() on add', done => {
stub = sinon.stub(bookmarks, 'set');
bookmarks.datastore._tasks[0] = {
operation: 'add',
id: 0,
data: {}
};
bookmarks.synchronise().then(() => {
assert.isTrue(stub.calledOnce);
done();
});
});
test('should call remove() on remove', done => {
stub = sinon.stub(bookmarks, 'remove');
bookmarks.datastore._tasks[0] = {
operation: 'remove',
id: 0,
data: {}
};
bookmarks.synchronise().then(() => {
assert.isTrue(stub.calledOnce);
done();
});
});
test('should call clear() on clear', done => {
stub = sinon.stub(bookmarks, 'clear');
bookmarks.datastore._tasks[0] = {
operation: 'clear',
id: 0,
data: {}
};
bookmarks.synchronise().then(() => {
assert.isTrue(stub.calledOnce);
done();
});
});
test('should call updateRevision() when done', done => {
stub = sinon.stub(bookmarks, 'updateRevision');
bookmarks.datastore._tasks = [{
operation: 'done',
id: 0,
data: null
}];
bookmarks.synchronise().then(() => {
assert.isTrue(stub.calledOnce);
done();
});
});
});
suite('indexedDB backed methods', () => {
var objectStores = {};
var deletedValues = [];
var attachDBTransaction = objectStoreName => {
objectStores = {};
deletedValues = [];
bookmarks.db.transaction = (objectStoreNames, IDBTransactionMode) => {
var oncomplete = null;
return {
objectStore: name => {
return {
put: value => {
if (name !== objectStoreName) {
return;
}
objectStores[name] = value;
if (oncomplete) {
oncomplete();
oncomplete = null;
}
},
delete: value => {
if (name !== objectStoreName) {
return;
}
deletedValues.push(value);
if (oncomplete) {
oncomplete();
oncomplete = null;
}
},
clear: () => {
if (name !== objectStoreName) {
return;
}
if (oncomplete) {
oncomplete();
oncomplete = null;
}
},
get: () => {
if (name !== objectStoreName) {
return;
}
return {
set onsuccess(cb) {
cb({
target: {
result: { id: 'abc/', data: {} }
}
});
}
};
},
openCursor: () => {
return {
set onsuccess(cb) {
if (objectStoreNames.indexOf(name) === -1) {
return;
}
cb({
target: {
result: {
value: { id: 'abc/', data: {} },
continue: () => {
cb({
target: {
result: {
value: { id: 'def/', data: {} },
continue: () => {}
}
}
});
}
}
}
});
if (oncomplete) {
oncomplete();
oncomplete = null;
}
}
};
}
};
},
set oncomplete(cb) {
oncomplete = cb;
},
onerror: () => {}
};
};
};
setup(() => {
bookmarks.db = {};
});
suite('Datastore#set()', () => {
var data = {
id: 'abc/',
misc: 'def'
};
setup(() => {
attachDBTransaction(bookmarks.storeName);
});
test('should store data', done => {
bookmarks.set(data)
.then(() => {
assert.equal(Object.keys(objectStores).length, 1);
assert.equal(objectStores[bookmarks.storeName].id, data.id);
assert.equal(objectStores[bookmarks.storeName].data, data);
done();
});
});
test('should emit an event', done => {
document.addEventListener(bookmarks.name + '-set', evt => {
assert.equal(evt.detail.id, data.id);
done();
});
bookmarks.set(data);
});
test('handles exception', done => {
var realTransaction = bookmarks.db.transaction;
bookmarks.db.transaction = (objectStoreNames, IDBTransactionMode) => {
var txn = realTransaction(objectStores, IDBTransactionMode);
var realObjectStore = txn.objectStore;
txn.objectStore = objectStoreName => {
var objectStore = realObjectStore(objectStoreName);
sinon.stub(objectStore, 'put', () => {
throw 'this exception should be caught';
});
return objectStore;
};
return txn;
};
bookmarks.set(data).then(done, done);
});
});
suite('Datastore#remove()', () => {
var id = 'abc/';
setup(() => {
attachDBTransaction(bookmarks.storeName);
});
test('should remove data from object store', done => {
bookmarks.remove(id)
.then(() => {
assert.deepEqual(deletedValues, [id]);
done();
});
});
test('should emit an event', done => {
document.addEventListener(bookmarks.name + '-removed', evt => {
assert.equal(evt.detail.id, id);
done();
});
bookmarks.remove(id);
});
test('handles exception', done => {
var realTransaction = bookmarks.db.transaction;
bookmarks.db.transaction = (objectStoreNames, IDBTransactionMode) => {
var txn = realTransaction(objectStores, IDBTransactionMode);
var realObjectStore = txn.objectStore;
txn.objectStore = objectStoreName => {
var objectStore = realObjectStore(objectStoreName);
sinon.stub(objectStore, 'delete', () => {
throw 'this exception should be caught';
});
return objectStore;
};
return txn;
};
bookmarks.remove(id).then(done, done);
});
});
suite('Datastore#clear()', () => {
test('should remove all data from object store', done => {
attachDBTransaction(bookmarks.storeName);
bookmarks.clear()
.then(() => {
done();
});
});
test('should emit an event', done => {
attachDBTransaction(bookmarks.storeName);
document.addEventListener(bookmarks.name + '-cleared', () => {
done();
});
bookmarks.clear();
});
});
suite('Datastore#get()', () => {
test('should retrieve data from object store', done => {
attachDBTransaction(bookmarks.storeName);
bookmarks.get('abc/')
.then(results => {
assert.deepEqual(results, { id: 'abc/', data: {} });
done();
});
});
});
suite('Datastore#getAll()', () => {
test('should retrieve data from object store', done => {
attachDBTransaction(bookmarks.storeName);
bookmarks.getAll()
.then(results => {
assert.equal(results.length, 2);
assert.deepEqual(results[0], { id: 'abc/', data: {} });
assert.deepEqual(results[1], { id: 'def/', data: {} });
done();
});
});
test('should accept a filtering function as argument', done => {
attachDBTransaction(bookmarks.storeName);
bookmarks.getAll(entry => entry.id.includes('def/'))
.then(results => {
assert.equal(results.length, 1);
assert.deepEqual(results[0], { id: 'def/', data: {} });
done();
});
});
});
});
suite('Datastore#updateRevision()', () => {
setup(done => {
bookmarks.init().then(done);
});
test('should set the datastore revision ID in lastRevision', () => {
assert.isNull(bookmarks.lastRevision);
bookmarks.updateRevision();
assert.equal(bookmarks.lastRevision, bookmarks.datastore.revisionId);
});
test('should update the datastore revision ID in lastRevision', done => {
var newRevisionId = 'xxx-zzz';
bookmarks.datastore._tasks = [{
operation: 'done',
id: 0,
data: null
}];
bookmarks.datastore.revisionId = newRevisionId;
bookmarks.synchronise().then(() => {
bookmarks.init().then(() => {
assert.equal(bookmarks.lastRevision, newRevisionId);
done();
});
});
});
});
suite('Datastore#onChange()', () => {
test('should call synchronise()', done => {
var synchroniseStub = sinon.stub(bookmarks, 'synchronise',
() => Promise.resolve());
bookmarks.onChange();
setTimeout(() => {
assert.isTrue(synchroniseStub.calledOnce);
done();
});
});
});
});

View file

@ -0,0 +1,272 @@
/* global FirstRun */
'use strict';
require('/js/firstrun.js');
suite('FirstRun', () => {
const COLS_PREF = 'grid.cols';
function testExpectedResults(results, expected) {
var string = '';
var order = 0;
results.order.forEach(entry => {
string += entry.id;
assert.equal(entry.order, order++);
});
assert.equal(string, expected);
}
suite('default settings', () => {
var json;
setup(() => {
window.LazyLoader = {
getJSON: (file) => {
assert.equal(file, 'js/init.json');
return Promise.resolve(json);
}
};
});
test('handles null JSON', (done) => {
json = null;
FirstRun().then(results => {
done(() => {
assert.isTrue(Array.isArray(results.order));
assert.equal(results.order.length, 0);
assert.equal(results.small, false);
});
}, done);
});
test('handles empty JSON', (done) => {
json = {};
FirstRun().then(results => {
done(() => {
assert.isTrue(Array.isArray(results.order));
assert.equal(results.order.length, 0);
assert.equal(results.small, false);
});
}, done);
});
test('handle null values in the JSON', (done) => {
json = { grid:
[[ null,
{ manifestURL: '1' },
null,
null,
{ manifestURL: '2', entry_point: '3' } ],
[ { manifestURL: '4' } ]
]};
FirstRun().then(results => {
done(() => {
testExpectedResults(results, '1/2/34/');
});
});
});
test('treats columns <= 3 as default columns setting', (done) => {
json = { preferences: {} };
json.preferences[COLS_PREF] = 2;
FirstRun().then(results1 => {
json.preferences[COLS_PREF] = 3;
FirstRun().then(results2 => {
done(() => {
assert.equal(results1.small, false);
assert.equal(results2.small, false);
});
}, done);
}, done);
});
test('treats columns > 3 as small columns setting', (done) => {
json = { preferences: {} };
json.preferences[COLS_PREF] = 4;
FirstRun().then(results => {
done(() => {
assert.equal(results.small, true);
});
});
});
test('transforms grid into compatible format', (done) => {
json = { grid:
[[ { manifestURL: '1' },
{ manifestURL: '2', entry_point: '3' } ],
[ { manifestURL: '4' } ]
]};
FirstRun().then(results => {
done(() => {
testExpectedResults(results, '1/2/34/');
});
});
});
test('omits objects with no manifestURL', (done) => {
json = { grid:
[[ { manifestURL: '1' },
{ manifestURL: '2', entry_point: '3' } ],
[ { invalidObject: true } ],
[ { manifestURL: '4' } ]
]};
FirstRun().then(results => {
done(() => {
testExpectedResults(results, '1/2/34/');
});
});
});
});
suite('verticalhome import', () => {
const VERTICALHOME_ITEMS = 'verticalhome_items_migration';
const VERTICALHOME_PREFS = 'vertical_preferences_store';
const HOMESCREEN_PREFS = 'homescreen_settings';
var realNavigatorDatastores, realConsoleWarn;
var itemsDatastore, verticalPrefsDatastore, homescreenPrefsDatastore,
noDatastore;
setup(() => {
realNavigatorDatastores = navigator.getDataStores;
noDatastore = false;
navigator.getDataStores = name => {
assert.isTrue((name === VERTICALHOME_ITEMS) ||
(name === VERTICALHOME_PREFS) ||
(name === HOMESCREEN_PREFS));
if (noDatastore) {
return Promise.resolve([]);
}
switch(name) {
case VERTICALHOME_ITEMS:
return Promise.resolve([itemsDatastore]);
case VERTICALHOME_PREFS:
return Promise.resolve([verticalPrefsDatastore]);
case HOMESCREEN_PREFS:
return Promise.resolve([homescreenPrefsDatastore]);
}
};
// Initialise the three datastores as fake empty datastores
itemsDatastore = verticalPrefsDatastore = homescreenPrefsDatastore = {
getLength: () => { return Promise.resolve(0); },
get: () => { return Promise.reject(); }
};
// Overload console.warn to disable FirstRun's built-in error fallback
realConsoleWarn = console.warn;
console.warn = (message, error) => {
throw(error);
};
});
teardown(() => {
navigator.getDataStores = realNavigatorDatastores;
console.warn = realConsoleWarn;
});
test('handles inaccessible datastore', done => {
noDatastore = true;
FirstRun().then(() => {
done('should not succeed with inaccessible datastore');
}, (e) => {
done(() => {
assert.equal(e, 'verticalhome datastores inaccessible');
});
});
});
test('handles empty datastore', done => {
FirstRun().then(() => {
done('should not succeed with empty datastore');
}, (e) => {
done(() => {
assert.equal(e, 'No verticalhome app order to import');
});
});
});
function populateItemStore() {
var items = {
'2': { index: '2', type: 'app', manifestURL: '2', entryPoint: '3' },
'1': { index: '1', type: 'app', manifestURL: '1' },
'4': { index: '4', type: 'divider' },
'6': { index: '6', type: 'bookmark', id: '4' }
};
var keys = Object.getOwnPropertyNames(items);
var length = keys.length;
itemsDatastore = {
getLength: () => { return Promise.resolve(length); },
sync: () => {
var i = 0;
return {
next: () => {
return Promise.resolve((i++ < length) ? {
operation: 'put',
id: keys[i - 1],
data: items[keys[i - 1]]
} : {
operation: 'done'
});
}
};
}
};
}
test('converts verticalhome format to homescreen format', done => {
populateItemStore();
FirstRun().then(results => {
done(() => {
testExpectedResults(results, '1/2/34');
});
}, done);
});
test('reads column preferences', done => {
populateItemStore();
verticalPrefsDatastore.get = pref => {
return (pref === COLS_PREF) ?
Promise.resolve(4) : Promise.reject();
};
FirstRun().then(results => {
done(() => {
testExpectedResults(results, '1/2/34');
assert.isTrue(results.small);
});
}, done);
});
test('stores column preference if cols > 3', done => {
populateItemStore();
verticalPrefsDatastore.get = pref => {
return (pref === COLS_PREF) ?
Promise.resolve(4) : Promise.reject();
};
homescreenPrefsDatastore.put = (pref, id) => {
done(() => {
assert.equal(id, COLS_PREF);
assert.equal(pref, 4);
});
return Promise.resolve();
};
FirstRun().catch(done);
});
});
});

View file

@ -0,0 +1,19 @@
/* exported Apps */
'use strict';
(function(exports) {
function Apps() {
this.panel = document.getElementById('apps-panel');
this.scrollable = document.querySelector('#apps-panel .scrollable');
this.editMode = false;
this.dialogs = [];
}
Apps.prototype = {
exitEditMode: function() {}
};
exports.Apps = Apps;
})(window);

View file

@ -0,0 +1,42 @@
/* exported AppsMetadata */
'use strict';
(function(exports) {
let _data = [];
function AppsMetadata() {
}
AppsMetadata.prototype = {
db: null,
init: () => {
return Promise.resolve();
},
upgradeSchema: () => {},
set: data=> {
_data = data;
return Promise.resolve();
},
remove: () => {
return Promise.resolve();
},
getAll: () => {
return Promise.resolve(_data);
},
get _data() {
return _data;
},
set _data(data) {
_data = data;
},
mSetup: () => {
_data = [];
}
};
exports.AppsMetadata = AppsMetadata;
})(window);

View file

@ -0,0 +1,58 @@
/* exported Datastore */
'use strict';
(function(exports) {
let _name = '';
function Datastore(name) {
_name = name;
}
Datastore.prototype = {
init: () => {
return Promise.resolve();
},
upgradeSchema: () => {
return Promise.resolve();
},
synchronise: () => {
return Promise.resolve();
},
set: (data) => {
return Promise.resolve();
},
remove: (id) => {
return Promise.resolve();
},
clear: () => {
return Promise.resolve();
},
updateRevision: () => {
return Promise.resolve();
},
onChange: () => {},
get: (id) => {
return Promise.resolve();
},
getAll: () => {
return Promise.resolve([]);
},
datastore: {
put: (data, id) => Promise.resolve()
},
get _name() {
return _name;
},
set _name(name) {
_name = name;
},
mSetup: () => {
_name = [];
}
};
exports.Datastore = Datastore;
})(window);

View file

@ -0,0 +1,26 @@
/* exported mockLocalStorage */
'use strict';
var _rawContent = {};
var mockLocalStorage = {
setItem: (key, val) => {
_rawContent[key] = val;
},
getItem: key => {
return _rawContent[key];
},
removeItem: key => {
delete _rawContent[key];
},
set mRawContent(val) {
_rawContent = val;
},
get mRawContent() {
return _rawContent;
},
mSetup: () => {
_rawContent = {};
}
};

View file

@ -0,0 +1,19 @@
/* exported Pages */
'use strict';
(function(exports) {
function Pages() {
this.panel = document.getElementById('pages-panel');
this.scrollable = document.querySelector('#pages-panel .scrollable');
this.editMode = false;
this.dialogs = [];
}
Pages.prototype = {
exitEditMode: function() {}
};
exports.Pages = Pages;
})(window);

View file

@ -0,0 +1,17 @@
/* exported Settings */
'use strict';
(function(exports) {
function Settings() {
this.small = false;
this.scrollSnapping = false;
this.firstRun = false;
}
Settings.prototype = {
};
exports.Settings = Settings;
})(window);

View file

@ -0,0 +1,414 @@
/* jshint nonew: false */
/* global Pages, PagesStore */
'use strict';
require('/shared/test/unit/load_body_html_helper.js');
require('mocks/mock_datastore.js');
require('/js/pages.js');
suite('Pages', () => {
var pages;
setup(() => {
window.PagesStore = window.Datastore;
loadBodyHTML('_index.html');
pages = new Pages();
});
suite('Pages constructor', () => {
var stub;
teardown(() => {
stub.restore();
});
test('should initialise the pages store', done => {
stub = sinon.stub(PagesStore.prototype, 'init', () => {
done();
return Promise.reject();
});
new Pages();
});
test('should get the list of pinned pages', done => {
stub = sinon.stub(PagesStore.prototype, 'getAll', () => {
done();
return Promise.reject();
});
new Pages();
});
test('should mark the pages panel empty if no pages', done => {
stub = sinon.stub(PagesStore.prototype, 'getAll', () => {
return { then: callback => {
callback([]);
done(() => {
assert.isTrue(document.getElementById('pages-panel').classList.
contains('empty'));
});
}};
});
new Pages();
});
});
suite('Pages#addPinnedPage()', () => {
var pinCard;
var createElementStub;
var updatePinnedPageStub;
var appendChildStub;
setup(() => {
pinCard = document.createElement('div');
createElementStub = sinon.stub(document, 'createElement', () => pinCard);
updatePinnedPageStub = sinon.stub(pages, 'updatePinnedPage');
appendChildStub = sinon.stub(pages.pages, 'appendChild');
});
teardown(() => {
createElementStub.restore();
updatePinnedPageStub.restore();
appendChildStub.restore();
});
test('should remove .empty from the pages panel', () => {
var pagesPanel = document.getElementById('pages-panel');
assert.isTrue(pages.empty);
assert.isTrue(pagesPanel.classList.contains('empty'));
pages.addPinnedPage();
assert.isFalse(pages.empty);
assert.isFalse(pagesPanel.classList.contains('empty'));
});
test('should add a card to the pages list', () => {
pages.addPinnedPage();
assert.isTrue(createElementStub.calledWith('gaia-pin-card'));
assert.isTrue(updatePinnedPageStub.calledOnce);
assert.isTrue(appendChildStub.calledOnce);
});
test('should make card accessible', () => {
pages.addPinnedPage();
assert.equal(pinCard.tabIndex, 0);
assert.equal(pinCard.getAttribute('role'), 'link');
});
});
suite('Pages#updatePinnedPage()', () => {
var clock;
var createObjectURLStub;
var mockPinnedPage = {
title: 'Mock pinned page',
url: 'Mock URL',
pinTime: 4242,
screenshot: 'Mock screenshot',
meta: {
'theme-color': 'Mock color'
}
};
var mockCard = {
title: null,
dataset: { id: null },
style: { order: 0 },
background: null,
meta: null,
icon: null
};
var mockIconBlob = 'Mock icon blob';
setup(() => {
clock = sinon.useFakeTimers();
createObjectURLStub = sinon.stub(URL, 'createObjectURL', blob => blob);
});
teardown(() => {
clock.restore();
createObjectURLStub.restore();
});
test('should set title, url and order', () => {
pages.updatePinnedPage(mockCard, mockPinnedPage);
assert.equal(mockCard.title, mockPinnedPage.title);
assert.equal(mockCard.dataset.id, mockPinnedPage.url);
var pinTime = -Math.round(mockPinnedPage.pinTime / 1000);
assert.equal(mockCard.style.order, pinTime);
});
test('should set screenshot, themeColor and icon after timeout', done => {
window.IconsHelper = {
getIconBlob: (url, size, page) => {
return {
then: (callback) => {
callback({ blob: mockIconBlob });
assert.equal(mockCard.icon, 'url(' + mockIconBlob + ')');
done();
return { catch: () => {} };
}
};
}
};
pages.updatePinnedPage(mockCard, mockPinnedPage);
clock.tick();
var meta = mockCard.meta;
assert.equal(meta['theme-color'], mockPinnedPage.meta['theme-color']);
assert.equal(meta.screenshot, mockPinnedPage.screenshot);
});
});
suite('Pages#enterEditMode()', () => {
var mockCard;
setup(() => {
mockCard = document.createElement('div');
pages.enterEditMode(mockCard);
});
teardown(() => {
pages.exitEditMode();
});
test('card is marked as selected', () => {
assert.isTrue(mockCard.classList.contains('selected'));
});
test('document is marked as in edit-mode', () => {
assert.isTrue(document.body.classList.contains('edit-mode'));
});
test('delete button is activated', () => {
assert.isTrue(pages.remove.classList.contains('active'));
});
test('previous card is unselected', () => {
var mockCard2 = document.createElement('div');
pages.enterEditMode(mockCard2);
assert.isFalse(mockCard.classList.contains('selected'));
assert.isTrue(mockCard2.classList.contains('selected'));
});
});
suite('Pages#exitEditMode()', () => {
var mockCard;
setup(() => {
mockCard = document.createElement('div');
pages.enterEditMode(mockCard);
pages.exitEditMode();
});
test('card is not marked as selected', () => {
assert.isFalse(mockCard.classList.contains('selected'));
assert.equal(pages.selectedCard, null);
});
test('document is not marked as in edit-mode', () => {
assert.isFalse(document.body.classList.contains('edit-mode'));
});
test('delete button is not activated', () => {
assert.isFalse(pages.remove.classList.contains('active'));
});
});
suite('Pages#removeCard()', () => {
var mockCard;
setup(() => {
mockCard = document.createElement('div');
});
test('removes child from container', () => {
var removeStub = sinon.stub(pages.pages, 'removeChild');
pages.removeCard(mockCard);
assert.isTrue(removeStub.calledWith(mockCard));
});
test('deselects card if card is selected', () => {
pages.selectedCard = mockCard;
var removeStub = sinon.stub(pages.pages, 'removeChild');
pages.removeCard(mockCard);
assert.equal(pages.selectedCard, null);
removeStub.restore();
});
test('shows the empty pinned pages panel when removing last card', done => {
pages.empty = false;
Object.defineProperty(pages.pages, 'children', {
value: { length: 0 },
configurable: true
});
var removeChildStub = sinon.stub(pages.pages, 'removeChild',
(card, callback) => {
callback();
done(() => {
delete pages.pages.children;
removeChildStub.restore();
assert.isTrue(pages.empty);
assert.isTrue(pages.panel.classList.contains('empty'));
});
});
pages.removeCard(mockCard);
});
test('exits edit mode when removing last card', done => {
pages.empty = false;
pages.editMode = true;
Object.defineProperty(pages.pages, 'children', {
value: { length: 0 },
configurable: true
});
var removeChildStub = sinon.stub(pages.pages, 'removeChild',
(card, callback) => {
callback();
done(() => {
delete pages.pages.children;
removeChildStub.restore();
assert.isFalse(pages.editMode);
});
});
pages.removeCard(mockCard);
});
});
suite('Pages#unpinSelectedCard()', () => {
var removeCardStub, storeGetStub, storePutStub, mockEntry, callback;
var mockCard = {
dataset: {
id: 'abc'
}
};
setup(() => {
mockEntry = { data: { pinned: true } };
removeCardStub = sinon.stub(pages, 'removeCard');
storeGetStub =
sinon.stub(pages.pagesStore, 'get', () => {
return Promise.resolve(mockEntry);
});
storePutStub =
sinon.stub(pages.pagesStore.datastore, 'put', (data, id) => {
if (callback) {
callback(data, id);
}
return Promise.resolve();
});
pages.selectedCard = mockCard;
});
teardown(() => {
removeCardStub.restore();
storeGetStub.restore();
storePutStub.restore();
pages.selectedCard = null;
});
test('removes card from container', () => {
pages.unpinSelectedCard();
assert.isTrue(removeCardStub.calledWith(mockCard));
});
test('unpins card in datastore', done => {
callback = (data, id) => {
done(() => {
assert.equal(id, mockCard.dataset.id);
assert.isFalse(mockEntry.data.pinned);
});
};
pages.unpinSelectedCard();
});
});
suite('Pages#handleEvent()', () => {
var mockCard = {
nodeName: 'GAIA-PIN-CARD',
title: 'Mock title',
icon: 'Mock icon',
dataset: {
id: 'Mock URL'
}
};
var mockNotCard = {
nodeName: 'MOCK-NOT-CARD'
};
suite('click', () => {
test('clicking a card should launch a remote window', () => {
var windowOpenStub = sinon.stub(window, 'open',
(url, target, features) => {
assert.equal(url, mockCard.dataset.id);
assert.equal(target, '_blank');
var expectedFeatures = {
name: mockCard.title,
icon: mockCard.icon,
remote: true
};
expectedFeatures = Object.keys(expectedFeatures).
map(function eachFeature(key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(expectedFeatures[key]);
}).join(',');
assert.equal(features, expectedFeatures);
});
pages.handleEvent({ type: 'click', target: mockCard });
windowOpenStub.restore();
});
test('clicking outside a card should do nothing', () => {
var windowOpenStub = sinon.stub(window, 'open');
pages.handleEvent({ type: 'click', target: mockNotCard });
assert.isFalse(windowOpenStub.called);
windowOpenStub.restore();
});
test('pressing a card in edit mode selects it', () => {
var editModeStub = sinon.stub(pages, 'enterEditMode');
pages.editMode = true;
pages.handleEvent({ type: 'click', target: mockCard });
assert.isTrue(editModeStub.calledWith(mockCard));
editModeStub.restore();
});
test('pressing delete in edit mode unpins a card', () => {
var unpinStub = sinon.stub(pages, 'unpinSelectedCard');
pages.selectedCard = mockCard;
pages.remove.dispatchEvent(new CustomEvent('click'));
assert.isTrue(unpinStub.called);
});
});
suite('contextmenu', () => {
var editModeStub;
setup(() => {
editModeStub = sinon.stub(pages, 'enterEditMode');
});
teardown(() => {
editModeStub.restore();
});
test('long-pressing a card initiates edit mode', () => {
pages.handleEvent({ type: 'contextmenu', target: mockCard });
assert.isTrue(editModeStub.calledWith(mockCard));
});
test('long-pressing not on a card does nothing', () => {
pages.handleEvent({ type: 'contextmenu', target: mockNotCard });
assert.isFalse(editModeStub.called);
});
});
suite('resize', () => {
test('should call pages.synchronise()', () => {
var synchroniseCalled = false;
pages.pages.synchronise = () => { synchroniseCalled = true; };
pages.handleEvent(new CustomEvent('resize'));
assert.isTrue(synchroniseCalled);
});
});
});
});

View file

@ -0,0 +1,197 @@
/* global MockIndexedDB, MockNavigatorDatastore, mockLocalStorage, PagesStore */
'use strict';
require('/shared/test/unit/mocks/mock_indexedDB.js');
require('/shared/test/unit/mocks/mock_navigator_datastore.js');
require('mocks/mock_localStorage.js');
require('/js/datastore.js');
require('/js/pagesstore.js');
suite('PagesStore', () => {
var mockIndexedDB;
var realNavigatorDatastores;
var realLocalStorage;
var datastoreName = 'pagesstore_name_example';
var pagesstore;
setup(() => {
mockIndexedDB = new MockIndexedDB();
mockLocalStorage.mSetup();
Object.defineProperty(window, 'localStorage', {
configurable: true,
get: () => mockLocalStorage
});
realNavigatorDatastores = navigator.getDataStores;
navigator.getDataStores = MockNavigatorDatastore.getDataStores;
pagesstore = new PagesStore(datastoreName);
});
teardown(() => {
mockIndexedDB.mTearDown();
Object.defineProperty(window, 'localStorage', {
configurable: true,
get: () => realLocalStorage
});
navigator.getDataStores = realNavigatorDatastores;
pagesstore = null;
});
suite('PagesStore constructor', () => {
test('should store the name of the datastore', () => {
assert.equal(pagesstore.name, datastoreName);
assert.isTrue(pagesstore.revisionName.includes(datastoreName));
assert.isTrue(pagesstore.idbName.includes(datastoreName));
assert.isTrue(pagesstore.storeName.includes(datastoreName));
});
});
suite('indexedDB backed methods', () => {
var data1 = {
url: 'http://www.theguardian.com/uk/culture',
title: 'Culture | The Guardian',
icons: {},
frecency: 4,
visits: [1440494813199],
screenshot: null,
themeColor: null,
visited: 1440494813199,
pinned: true,
pinTime: 1440426834317,
id: 'http://www.theguardian.com/uk/culture'
};
var data2 = {
url: 'http://www.cnet.com/',
title: 'Product reviews and prices, software downloads, and tech news',
icons: {},
frecency: 1,
visits: [1440427988417],
screenshot: null,
themeColor: null,
visited: 1440427988417,
id: 'http://www.cnet.com/'
};
var objectStores = {};
var deletedValues = [];
var attachDBTransaction = objectStoreName => {
objectStores = {};
deletedValues = [];
pagesstore.db.transaction = (objectStoreNames, IDBTransactionMode) => {
return {
objectStore: name => {
return {
put: value => {
if (name !== objectStoreName) {
return;
}
objectStores[name] = value;
},
delete: value => {
if (name !== objectStoreName) {
return;
}
deletedValues.push(value);
},
clear: () => {
if (name !== objectStoreName) {
return;
}
},
get: () => {
if (name !== objectStoreName) {
return;
}
return {
set onsuccess(cb) {
cb({
target: {
result: data1
}
});
}
};
},
openCursor: () => {
return {
set onsuccess(cb) {
if (objectStoreNames.indexOf(name) === -1) {
return;
}
cb({
target: {
result: {
value: { id: data1.url, data: data1 },
continue: () => {
cb({
target: {
result: {
value: { id: data2.url, data: data2 },
continue: () => {}
}
}
});
}
}
}
});
}
};
}
};
},
set oncomplete(cb) {
cb();
},
onerror: () => {
}
};
};
};
setup(() => {
pagesstore.db = {};
});
suite('PagesStore#set()', () => {
setup(() => {
attachDBTransaction(pagesstore.storeName);
});
test('should use the url as the id', done => {
pagesstore.set(data1)
.then(() => {
assert.equal(Object.keys(objectStores).length, 1);
assert.equal(objectStores[pagesstore.storeName].id, data1.url);
assert.equal(objectStores[pagesstore.storeName].data, data1);
done();
});
});
test('should emit an event', done => {
document.addEventListener(pagesstore.name + '-set', evt => {
assert.equal(evt.detail.id, data1.id);
done();
});
pagesstore.set(data1);
});
});
suite.only('PagesStore#getAll()', () => {
test('should not accept any argument', () => {
assert.equal(pagesstore.getAll.length, 0);
});
test('should retrieve data from object store', done => {
attachDBTransaction(pagesstore.storeName);
pagesstore.getAll()
.then(results => {
assert.equal(results.length, 1);
assert.deepEqual(results[0].id, data1.url);
assert.deepEqual(results[0].data, data1);
done();
});
});
});
});
});

View file

@ -0,0 +1,238 @@
/* global mockLocalStorage, MockNavigatorDatastore, MockDatastore, Settings */
/* jshint nonew: false */
'use strict';
require('mocks/mock_localStorage.js');
require('/shared/test/unit/mocks/mock_navigator_datastore.js');
require('/js/settings.js');
suite('Settings', () => {
const DEFAULT_SETTINGS = '{"version":0,"small":false,"scrollSnapping":false}';
var realLocalStorage;
var realNavigatorDatastores;
setup(() => {
realNavigatorDatastores = navigator.getDataStores;
navigator.getDataStores = MockNavigatorDatastore.getDataStores;
realLocalStorage = Object.getOwnPropertyDescriptor(window, 'localStorage');
Object.defineProperty(window, 'localStorage', {
configurable: true,
get: () => mockLocalStorage
});
});
teardown(() => {
navigator.getDataStores = realNavigatorDatastores;
Object.defineProperty(window, 'localStorage', realLocalStorage);
});
suite('Settings constructor', () => {
test('attempts to retrieve existing settings', () => {
var getItemStub = sinon.stub(window.localStorage, 'getItem');
new Settings();
assert.isTrue(getItemStub.calledWith('settings'));
getItemStub.restore();
});
test('sets firstRun to true if no settings', () => {
mockLocalStorage.removeItem('settings');
var settings = new Settings();
assert.isTrue(settings.firstRun);
});
suite('settings exist', () => {
setup(() => {
mockLocalStorage.setItem('settings', DEFAULT_SETTINGS);
});
test('firstRun is false', () => {
var settings = new Settings();
assert.isFalse(settings.firstRun);
});
test('restores settings', () => {
var settings = new Settings();
assert.isFalse(settings.small);
assert.isFalse(settings.scrollSnapping);
mockLocalStorage.setItem('settings',
'{"version":0,"small":true,"scrollSnapping":true}');
settings = new Settings();
assert.isTrue(settings.small);
assert.isTrue(settings.scrollSnapping);
});
test('should not restore data if version number differs', () => {
mockLocalStorage.setItem('settings', '{"version":500,"small":true}');
var settings = new Settings();
assert.isFalse(settings.small);
});
test('opens settings datastore', () => {
var getDataStoresStub = sinon.stub(navigator, 'getDataStores',
() => Promise.reject());
new Settings();
assert.isTrue(getDataStoresStub.calledWith('homescreen_settings'));
getDataStoresStub.restore();
});
suite('datastore settings', () => {
var target;
var cols;
var page;
var afterGetCall;
var datastoreGetStub;
setup(() => {
cols = 3;
page = false;
datastoreGetStub = sinon.stub(MockDatastore, 'get', id => {
var returnData;
var returnObject = {
then: callback => {
callback(returnData);
if (id === target) {
afterGetCall();
}
}
};
switch (id) {
case 'grid.cols':
returnData = cols;
break;
case 'grid.paging':
returnData = page;
break;
}
return returnObject;
});
});
teardown(() => {
datastoreGetStub.restore();
});
test('restores columns setting', done => {
var settings;
target = 'grid.cols';
afterGetCall = () => {
try {
assert.isFalse(settings.small);
} catch(e) {
done(e);
}
afterGetCall = () => {
done(() => {
assert.isTrue(settings.small);
});
};
cols = 4;
settings = new Settings();
};
settings = new Settings();
});
test('responds to columns setting change', done => {
var settings;
target = 'grid.cols';
afterGetCall = () => {
try {
assert.isFalse(settings.small);
} catch(e) {
done(e);
}
var dispatchEventStub = sinon.stub(window, 'dispatchEvent');
afterGetCall = () => {
done(() => {
assert.isTrue(dispatchEventStub.called);
assert.isTrue(settings.small);
dispatchEventStub.restore();
});
};
cols = 4;
MockDatastore._cb({ id: 'grid.cols' });
};
settings = new Settings();
});
test('restores paging setting', done => {
var settings;
target = 'grid.paging';
afterGetCall = () => {
try {
assert.isFalse(settings.scrollSnapping);
} catch(e) {
done(e);
}
afterGetCall = () => {
done(() => {
assert.isTrue(settings.scrollSnapping);
});
};
page = true;
settings = new Settings();
};
settings = new Settings();
});
test('responds to paging setting change', done => {
var settings;
target = 'grid.paging';
afterGetCall = () => {
try {
assert.isFalse(settings.scrollSnapping);
} catch(e) {
done(e);
}
var dispatchEventStub = sinon.stub(window, 'dispatchEvent');
afterGetCall = () => {
done(() => {
assert.isTrue(dispatchEventStub.called);
assert.isTrue(settings.scrollSnapping);
dispatchEventStub.restore();
});
};
page = true;
MockDatastore._cb({ id: 'grid.paging' });
};
settings = new Settings();
});
});
});
});
suite('Settings#save()', () => {
test('saves columns setting', () => {
mockLocalStorage.setItem('settings', DEFAULT_SETTINGS);
var settings = new Settings();
settings.small = true;
settings.save();
var settingsObj = JSON.parse(mockLocalStorage.getItem('settings'));
assert.isTrue(settingsObj.small);
});
});
});

View file

@ -0,0 +1,328 @@
/* jshint nonew: false */
/* global AppWindow */
'use strict';
require('/shared/test/unit/load_body_html_helper.js');
require('mocks/mock_pages.js');
require('mocks/mock_apps.js');
require('/js/window.js');
suite('AppWindow', () => {
var appWindow;
setup(() => {
loadBodyHTML('_index.html');
appWindow = new AppWindow();
});
suite('AppWindow#updatePanelIndicator()', () => {
var indicatorToggleStubs;
var realPanels;
var mockPanels;
setup(() => {
mockPanels = {
scrollLeft: 0,
scrollWidth: 200
};
indicatorToggleStubs = [
sinon.stub(appWindow.indicator.children[0].classList, 'toggle'),
sinon.stub(appWindow.indicator.children[1].classList, 'toggle')];
realPanels = appWindow.panels;
appWindow.panels = mockPanels;
});
teardown(() => {
indicatorToggleStubs.forEach((stub) => { stub.restore(); });
appWindow.panels = realPanels;
});
test('should update indicator when apps visible', () => {
appWindow.appsVisible = false;
appWindow.updatePanelIndicator();
assert.isTrue(indicatorToggleStubs[0].calledWith('active', true));
assert.isTrue(indicatorToggleStubs[1].calledWith('active', false));
assert.equal(appWindow.header.getAttribute('data-l10n-id'), 'apps-panel');
});
test('should update indicator when apps visible in RTL', () => {
appWindow.appsVisible = true;
mockPanels.scrollLeft = -100;
appWindow.updatePanelIndicator();
assert.isTrue(indicatorToggleStubs[0].calledWith('active', false));
assert.isTrue(indicatorToggleStubs[1].calledWith('active', true));
assert.equal(appWindow.header.getAttribute('data-l10n-id'),
'pages-panel');
});
test('should update aria-hidden on both panels', () => {
var appPanelSetAttributeStub =
sinon.stub(appWindow.apps.panel, 'setAttribute');
var pagesPanelSetAttributeStub =
sinon.stub(appWindow.pages.panel, 'setAttribute');
appWindow.appsVisible = false;
appWindow.updatePanelIndicator();
assert.isTrue(appPanelSetAttributeStub.calledWith('aria-hidden', false));
assert.isTrue(pagesPanelSetAttributeStub.calledWith('aria-hidden', true));
appPanelSetAttributeStub.restore();
pagesPanelSetAttributeStub.restore();
});
test('should update indicator when pages visible', () => {
var setAttributeSpy = sinon.spy(appWindow.indicator, 'setAttribute');
var spy1 = setAttributeSpy.withArgs('aria-valuenow', 2);
var spy2 = setAttributeSpy.withArgs('data-l10n-args', JSON.stringify({
currentPage: 2,
totalPages: 2
}));
mockPanels.scrollLeft = mockPanels.scrollLeftMax;
appWindow.updatePanelIndicator();
assert.isTrue(indicatorToggleStubs[0].calledWith('active', false));
assert.isTrue(indicatorToggleStubs[1].calledWith('active', true));
assert.isTrue(spy1.called);
assert.isTrue(spy2.called);
assert.equal(appWindow.header.getAttribute('data-l10n-id'),
'pages-panel');
});
test('should do nothing when visibility is unchanged', () => {
var setAttributeSpy = sinon.spy(appWindow.indicator, 'setAttribute');
appWindow.appsVisible = true;
mockPanels.scrollLeft = 0;
appWindow.updatePanelIndicator();
assert.isFalse(indicatorToggleStubs[0].called);
assert.isFalse(indicatorToggleStubs[1].called);
assert.isFalse(setAttributeSpy.called);
setAttributeSpy.restore();
});
});
suite('AppWindow#handleEvent()', () => {
suite('keypress', () => {
var scrollObject, realPanels;
var event = {
type: 'keypress',
ctrlKey: true,
DOM_VK_RIGHT: 'right',
DOM_VK_LEFT: 'left'
};
var mockPanels = {
scrollLeftMax: 100,
scrollTo: obj => { scrollObject = obj; }
};
setup(() => {
realPanels = appWindow.panels;
appWindow.panels = mockPanels;
});
teardown(() => {
appWindow.panels = realPanels;
});
test('right should display pinned pages', () => {
event.keyCode = 'right';
appWindow.handleEvent(event);
assert.equal(scrollObject.left, appWindow.panels.scrollLeftMax);
assert.equal(scrollObject.top, 0);
assert.equal(scrollObject.behavior, 'smooth');
});
test('left should display apps', () => {
event.keyCode = 'left';
appWindow.handleEvent(event);
assert.equal(scrollObject.left, 0);
assert.equal(scrollObject.top, 0);
assert.equal(scrollObject.behavior, 'smooth');
});
});
suite('scroll', () => {
test('should show and hide the drop shadow accordingly', () => {
appWindow.appsVisible = true;
appWindow.apps.scrollable = {
scrollTop: 50
};
appWindow.handleEvent(new CustomEvent('scroll'));
assert.isTrue(appWindow.shadow.classList.contains('visible'));
appWindow.apps.scrollable.scrollTop = 0;
appWindow.handleEvent(new CustomEvent('scroll'));
assert.isFalse(appWindow.shadow.classList.contains('visible'));
appWindow.appsVisible = false;
appWindow.pages.scrollable = {
scrollTop: 50
};
appWindow.handleEvent(new CustomEvent('scroll'));
assert.isTrue(appWindow.shadow.classList.contains('visible'));
appWindow.pages.scrollable.scrollTop = 0;
appWindow.handleEvent(new CustomEvent('scroll'));
assert.isFalse(appWindow.shadow.classList.contains('visible'));
});
test('should update the panel indicator', () => {
var updatePanelStub = sinon.stub(appWindow, 'updatePanelIndicator');
appWindow.panels.dispatchEvent(new CustomEvent('scroll'));
assert.isTrue(updatePanelStub.called);
updatePanelStub.restore();
});
});
suite('hashchange', () => {
var realDocumentHidden;
setup(() => {
realDocumentHidden =
Object.getOwnPropertyDescriptor(document, 'hidden');
Object.defineProperty(document, 'hidden', {
value: false,
configurable: true
});
});
teardown(() => {
if (realDocumentHidden) {
Object.defineProperty(document, 'hidden', realDocumentHidden);
} else {
delete document.hidden;
}
});
test('should scroll to the top of the apps page when visible', done => {
var realScrollable = appWindow.apps.scrollable;
appWindow.apps.scrollable = {
scrollTo: (obj) => {
done(() => {
assert.equal(obj.top, 0);
assert.equal(obj.left, 0);
});
},
scrollLeft: 0,
parentNode: {
offsetLeft: 0
}
};
appWindow.appsVisible = true;
appWindow.handleEvent(new CustomEvent('hashchange'));
appWindow.apps.scrollable = realScrollable;
});
test('should scroll to the top of the pages page when visible', done => {
var realScrollable = appWindow.pages.scrollable;
appWindow.pages.scrollable = {
scrollTop: 0,
scrollLeft: 0,
parentNode: {
offsetLeft: 0
}
};
var realPanels = appWindow.panels;
appWindow.panels = {
scrollTo: (obj) => {
done(() => {
assert.equal(obj.top, 0);
assert.equal(obj.left, 0);
});
},
};
appWindow.appsVisible = false;
appWindow.handleEvent(new CustomEvent('hashchange'));
appWindow.pages.scrollable = realScrollable;
appWindow.panels = realPanels;
});
test('should scroll to apps panel when at top of pages panel', done => {
var realScrollable = appWindow.pages.scrollable;
appWindow.pages.scrollable = {
scrollTo: (obj) => {
done(() => {
assert.equal(obj.top, 0);
assert.equal(obj.left, 0);
});
},
scrollLeft: 0,
parentNode: {
offsetLeft: 0
}
};
appWindow.appsVisible = false;
appWindow.handleEvent(new CustomEvent('hashchange'));
appWindow.pages.scrollable = realScrollable;
});
test('should cancel apps-panel dialogs', done => {
var realDialogs = appWindow.apps.dialogs;
appWindow.apps.dialogs = [{
close: () => { done(); },
opened: () => { return true; }
}];
appWindow.handleEvent(new CustomEvent('hashchange'));
appWindow.apps.dialogs = realDialogs;
});
test('should cancel pages-panel dialogs', done => {
var realDialogs = appWindow.pages.dialogs;
appWindow.pages.dialogs = [{
close: () => { done(); },
opened: () => { return true; }
}];
appWindow.handleEvent(new CustomEvent('hashchange'));
appWindow.pages.dialogs = realDialogs;
});
test('should exit edit mode', () => {
var exitEditModeStub;
appWindow.apps.editMode = true;
exitEditModeStub = sinon.stub(appWindow.apps, 'exitEditMode');
appWindow.handleEvent(new CustomEvent('hashchange'));
assert.isTrue(exitEditModeStub.called);
exitEditModeStub.restore();
appWindow.ignoreHashChangeTimeout = null;
appWindow.apps.editMode = false;
appWindow.pages.editMode = true;
exitEditModeStub = sinon.stub(appWindow.pages, 'exitEditMode');
appWindow.handleEvent(new CustomEvent('hashchange'));
assert.isTrue(exitEditModeStub.called);
exitEditModeStub.restore();
});
});
suite('visibilitychange', () => {
var appsExitEditModeStub, pagesExitEditModeStub;
setup(() => {
appsExitEditModeStub = sinon.stub(appWindow.apps, 'exitEditMode');
pagesExitEditModeStub = sinon.stub(appWindow.pages, 'exitEditMode');
Object.defineProperty(document, 'hidden', {
value: true,
configurable: true
});
});
teardown(() => {
appsExitEditModeStub.restore();
pagesExitEditModeStub.restore();
delete document.hidden;
});
test('should exit edit mode', () => {
appWindow.handleEvent(new CustomEvent('visibilitychange'));
assert.isTrue(appsExitEditModeStub.called);
assert.isTrue(pagesExitEditModeStub.called);
});
});
});
});