Publish Vulpes Preview 15 sources for desktop, Android and Tundra

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

View file

@ -0,0 +1,19 @@
{
"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]);
}
});
});