Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
1263 lines
38 KiB
JavaScript
Executable file
1263 lines
38 KiB
JavaScript
Executable file
/* jshint nonew: false */
|
|
/* global MockNavigatormozApps, MockMozActivity, MockIconsHelper, IconsHelper,
|
|
AppsMetadata, Datastore, Settings, Apps */
|
|
'use strict';
|
|
|
|
require('/shared/test/unit/load_body_html_helper.js');
|
|
require('/shared/test/unit/mocks/mock_navigator_moz_apps.js');
|
|
require('/shared/test/unit/mocks/mock_moz_activity.js');
|
|
require('/shared/test/unit/mocks/mock_icons_helper.js');
|
|
require('mocks/mock_appsmetadata.js');
|
|
require('mocks/mock_datastore.js');
|
|
require('mocks/mock_settings.js');
|
|
require('/shared/js/l10n.js');
|
|
require('/js/apps.js');
|
|
|
|
suite('Homescreen app', () => {
|
|
var sandbox;
|
|
var realNavigatorMozApps;
|
|
var realMozActivity;
|
|
var realLocalStorage;
|
|
var realIconsHelper;
|
|
var app;
|
|
|
|
var realCreateElement;
|
|
var createElementStub;
|
|
var gaiaAppIconEl;
|
|
|
|
realCreateElement = document.createElement.bind(document);
|
|
|
|
var getIcon = manifestURL => {
|
|
var iconChild = document.createElement('div');
|
|
iconChild.style.height = '100px';
|
|
iconChild.style.width = '100px';
|
|
var icon = document.createElement('div');
|
|
icon.style.display = 'block';
|
|
icon.app = { manifestURL: manifestURL };
|
|
icon.entryPoint = '';
|
|
icon.bookmark = null;
|
|
icon.icon = null;
|
|
icon.size = 100;
|
|
icon.appendChild(iconChild);
|
|
var container = document.createElement('div');
|
|
container.appendChild(icon);
|
|
return container;
|
|
};
|
|
|
|
var stubCreateElement = () => {
|
|
realCreateElement = document.createElement.bind(document);
|
|
createElementStub = sinon.stub(document, 'createElement');
|
|
createElementStub.withArgs('div').returns(realCreateElement('div'));
|
|
var iconChild = realCreateElement('div');
|
|
gaiaAppIconEl = realCreateElement('div');
|
|
gaiaAppIconEl.app = null;
|
|
gaiaAppIconEl.entryPoint = null;
|
|
gaiaAppIconEl.bookmark = null;
|
|
gaiaAppIconEl.icon = null;
|
|
gaiaAppIconEl.refresh = () => {};
|
|
gaiaAppIconEl.updateName = () => {};
|
|
gaiaAppIconEl.appendChild(iconChild);
|
|
createElementStub.withArgs('gaia-app-icon').returns(gaiaAppIconEl);
|
|
};
|
|
|
|
var restoreCreateElement = () => {
|
|
createElementStub.restore();
|
|
gaiaAppIconEl = null;
|
|
};
|
|
|
|
setup(() => {
|
|
sandbox = sinon.sandbox.create();
|
|
realNavigatorMozApps = navigator.mozApps;
|
|
navigator.mozApps = MockNavigatormozApps;
|
|
realMozActivity = window.MozActivity;
|
|
window.MozActivity = MockMozActivity;
|
|
realIconsHelper = window.IconsHelper;
|
|
window.IconsHelper = MockIconsHelper;
|
|
|
|
loadBodyHTML('_index.html');
|
|
document.head.innerHTML = `<meta name="theme-color" content="transparent">`;
|
|
for (var dialog of document.querySelectorAll('.dialog')) {
|
|
dialog.hide = function() {
|
|
this.style.display = 'none';
|
|
};
|
|
}
|
|
var icons = document.getElementById('apps');
|
|
icons.freeze = icons.thaw = () => {};
|
|
icons.getChildOffsetRect = child => {
|
|
return {
|
|
top: child.offsetTop,
|
|
right: child.offsetLeft + child.offsetWidth,
|
|
bottom: child.offsetTop + child.offsetHeight,
|
|
left: child.offsetLeft,
|
|
width: child.offsetWidth,
|
|
height: child.offsetHeight
|
|
};
|
|
};
|
|
app = new Apps();
|
|
});
|
|
|
|
teardown(() => {
|
|
sandbox.restore();
|
|
navigator.mozApps = realNavigatorMozApps;
|
|
window.MozActivity = realMozActivity;
|
|
window.IconsHelper = realIconsHelper;
|
|
|
|
MockNavigatormozApps.mTeardown();
|
|
|
|
Object.defineProperty(window, 'localStorage', {
|
|
configurable: true,
|
|
get: () => realLocalStorage
|
|
});
|
|
|
|
// Remove all icons.
|
|
while (app.icons.children.length) {
|
|
app.icons.removeChild(app.icons.firstElementChild);
|
|
}
|
|
app = null;
|
|
});
|
|
|
|
suite('App constructor', () => {
|
|
var stub;
|
|
|
|
setup(() => {
|
|
stubCreateElement();
|
|
});
|
|
|
|
teardown(() => {
|
|
restoreCreateElement();
|
|
MockNavigatormozApps.mApps = [];
|
|
stub.restore();
|
|
});
|
|
|
|
test('should call freeze and thaw on icon container', done => {
|
|
stub = sinon.stub(app.icons, 'freeze', () => {
|
|
stub.restore();
|
|
stub = sinon.stub(app.icons, 'thaw', () => {
|
|
stub.restore();
|
|
done();
|
|
});
|
|
});
|
|
new Apps();
|
|
});
|
|
|
|
test('should initialise the metadata store', done => {
|
|
stub = sinon.stub(AppsMetadata.prototype, 'init', () => {
|
|
done();
|
|
return Promise.resolve();
|
|
});
|
|
new Apps();
|
|
});
|
|
|
|
test('should initialise the bookmark stores', done => {
|
|
stub = sinon.stub(Datastore.prototype, 'init', () => {
|
|
done();
|
|
return Promise.resolve();
|
|
});
|
|
new Apps();
|
|
});
|
|
|
|
test('should get the list of installed apps', done => {
|
|
stub = sinon.stub(MockNavigatormozApps.mgmt, 'getAll', () => {
|
|
done();
|
|
return Promise.resolve();
|
|
});
|
|
new Apps();
|
|
});
|
|
|
|
test('should get the list of bookmarked pages', done => {
|
|
stub = sinon.stub(Datastore.prototype, 'getAll', () => {
|
|
done();
|
|
return Promise.resolve();
|
|
});
|
|
new Apps();
|
|
});
|
|
|
|
test('should remove unknown app entries from metadata store', done => {
|
|
stub = sinon.stub(AppsMetadata.prototype, 'remove', id => {
|
|
metadataGetAllStub.restore();
|
|
done(() => {
|
|
assert.equal(id, 'def/');
|
|
});
|
|
return Promise.resolve();
|
|
});
|
|
var metadataGetAllStub = sinon.stub(AppsMetadata.prototype, 'getAll',
|
|
callback => {
|
|
var results = [
|
|
{ id: 'abc/', icon: 'abc', order: 0 },
|
|
{ id: 'def/', icon: 'def', order: 1 }
|
|
];
|
|
for (var result of results) {
|
|
callback(result);
|
|
}
|
|
return Promise.resolve(results);
|
|
});
|
|
MockNavigatormozApps.mApps = [{
|
|
manifest: {},
|
|
manifestURL: 'abc',
|
|
order: 0,
|
|
addEventListener: () => {}
|
|
}];
|
|
|
|
app = new Apps();
|
|
});
|
|
|
|
test('should add pending apps that were missed during startup', done => {
|
|
var metadataGetAllStub = sinon.stub(AppsMetadata.prototype, 'getAll',
|
|
callback => {
|
|
var results = [
|
|
{ id: 'abc/', icon: 'abc', order: 0 },
|
|
];
|
|
for (var result of results) {
|
|
callback(result);
|
|
}
|
|
|
|
stub = sinon.stub(app, 'addAppIcon', icon => {
|
|
metadataGetAllStub.restore();
|
|
done(() => {
|
|
assert.equal(icon, results[0]);
|
|
});
|
|
});
|
|
app.pendingIcons[results[0].id] = [results[0]];
|
|
|
|
return Promise.resolve(results);
|
|
});
|
|
|
|
app = new Apps();
|
|
});
|
|
|
|
suite('first run', () => {
|
|
setup(() => {
|
|
window.LazyLoader = {
|
|
load: (files, callback) => {
|
|
callback();
|
|
}
|
|
};
|
|
|
|
window.FirstRun = () => {
|
|
return Promise.resolve({ order: [], small: false });
|
|
};
|
|
|
|
Settings.firstRun = true;
|
|
});
|
|
|
|
teardown(() => {
|
|
Settings.firstRun = false;
|
|
});
|
|
|
|
test('should initialise the bookmark stores', done => {
|
|
stub = sinon.stub(Datastore.prototype, 'init', () => {
|
|
done();
|
|
return Promise.resolve();
|
|
});
|
|
new Apps();
|
|
});
|
|
|
|
test('should get the list of installed apps', done => {
|
|
stub = sinon.stub(MockNavigatormozApps.mgmt, 'getAll', () => {
|
|
done();
|
|
return Promise.resolve();
|
|
});
|
|
new Apps();
|
|
});
|
|
|
|
test('should get the list of bookmarked pages', done => {
|
|
stub = sinon.stub(Datastore.prototype, 'getAll', () => {
|
|
done();
|
|
return Promise.resolve();
|
|
});
|
|
new Apps();
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('App#iconSize', () => {
|
|
test('should be 0 by default', () => {
|
|
assert.equal(app.iconSize, 0);
|
|
});
|
|
|
|
test('should be updated at each call when it equals 0', () => {
|
|
assert.equal(app.iconSize, 0);
|
|
app.icons.appendChild(getIcon('abc'));
|
|
assert.equal(app.iconSize, 100);
|
|
});
|
|
|
|
test('should be the size of the first icon', () => {
|
|
app.icons.appendChild(getIcon('abc'));
|
|
assert.equal(app.iconSize, 100);
|
|
});
|
|
|
|
test('should be the size of the first visible icon', () => {
|
|
app.icons.appendChild(getIcon('abc'));
|
|
app.icons.appendChild(getIcon('def'));
|
|
app.icons.firstChild.firstChild.style.display = 'none';
|
|
app.icons.firstChild.firstChild.firstChild.style.width = '200px';
|
|
assert.equal(app.iconSize, 100);
|
|
});
|
|
});
|
|
|
|
suite('App#addApp()', () => {
|
|
var addAppIconStub;
|
|
|
|
setup(() => {
|
|
addAppIconStub = sinon.stub(app, 'addAppIcon');
|
|
});
|
|
|
|
teardown(() => {
|
|
addAppIconStub.restore();
|
|
});
|
|
|
|
test('should only accept valid apps', () => {
|
|
app.addApp({});
|
|
assert.isFalse(addAppIconStub.called, 'Empty app objects');
|
|
|
|
app.addApp({ manifest: {} });
|
|
assert.isTrue(addAppIconStub.calledOnce);
|
|
});
|
|
|
|
test('should add all entry points', () => {
|
|
app.addApp({ manifest: { entry_points: { a: {}, b: {}, c: {} } } });
|
|
assert.isTrue(addAppIconStub.calledThrice);
|
|
});
|
|
|
|
test('should be called on app install', () => {
|
|
var addAppStub = sinon.stub(app, 'addApp');
|
|
app.handleEvent(new CustomEvent('install'));
|
|
assert.isTrue(addAppStub.calledOnce);
|
|
addAppStub.restore();
|
|
});
|
|
|
|
test('should not be called when installing a pre-existing app', () => {
|
|
var refreshCalled = false;
|
|
Object.defineProperty(app.icons, 'children', {
|
|
value: [{
|
|
firstElementChild: {
|
|
app: {
|
|
manifestURL: 'abc'
|
|
},
|
|
refresh: () => {
|
|
refreshCalled = true;
|
|
}
|
|
}
|
|
}],
|
|
configurable: true
|
|
});
|
|
|
|
var addAppStub = sinon.stub(app, 'addApp');
|
|
app.handleEvent({
|
|
type: 'install',
|
|
application: { manifestURL: 'abc' }
|
|
});
|
|
|
|
assert.isFalse(addAppStub.called);
|
|
assert.isTrue(refreshCalled);
|
|
|
|
addAppStub.restore();
|
|
delete app.icons.children;
|
|
});
|
|
});
|
|
|
|
suite('App#addIconContainer()', () => {
|
|
var refreshGridSizeStub, iconAddedSpy;
|
|
setup(() => {
|
|
refreshGridSizeStub = sinon.stub(app, 'refreshGridSize');
|
|
iconAddedSpy = sinon.spy(app, 'iconAdded');
|
|
});
|
|
|
|
teardown(() => {
|
|
refreshGridSizeStub.restore();
|
|
iconAddedSpy.restore();
|
|
});
|
|
|
|
suite('element properties', () => {
|
|
var appendChildStub, childIsVisible;
|
|
setup(() => {
|
|
childIsVisible = true;
|
|
appendChildStub = sinon.stub(app.icons, 'appendChild', (el, cb) => {
|
|
if (!childIsVisible) {
|
|
el.style.display = 'none';
|
|
}
|
|
if (cb) {
|
|
cb();
|
|
}
|
|
});
|
|
});
|
|
|
|
teardown(() => {
|
|
appendChildStub.restore();
|
|
});
|
|
|
|
test('should return a HTML element with an order property', () => {
|
|
app.startupMetadata = [{ order: 1 }];
|
|
var container = app.addIconContainer(document.createElement('div'), 0);
|
|
|
|
assert.isTrue(container instanceof HTMLDivElement);
|
|
assert.isTrue(container instanceof HTMLElement);
|
|
assert.isNumber(container.order);
|
|
});
|
|
|
|
test('should call iconAdded() when adding children', () => {
|
|
app.addIconContainer(document.createElement('div'), -1);
|
|
app.addIconContainer(document.createElement('div'), -1);
|
|
|
|
assert.isTrue(appendChildStub.calledTwice);
|
|
assert.isTrue(iconAddedSpy.calledTwice);
|
|
});
|
|
|
|
test('should call refreshGridSize() when adding visible children', () => {
|
|
app.addIconContainer(document.createElement('div'), -1);
|
|
|
|
assert.isTrue(appendChildStub.called);
|
|
assert.isTrue(refreshGridSizeStub.called);
|
|
});
|
|
|
|
test('should not call refreshGridSize() when adding ' +
|
|
'invisible children', () => {
|
|
childIsVisible = false;
|
|
app.addIconContainer(document.createElement('div'), -1);
|
|
assert.isFalse(refreshGridSizeStub.called);
|
|
});
|
|
});
|
|
|
|
test('should insert a container in the right order', () => {
|
|
app.startupMetadata = [{ order: 1 }, { order: 2 }, { order: 3 }];
|
|
app.addIconContainer(document.createElement('div'), 0);
|
|
app.addIconContainer(document.createElement('div'), 2);
|
|
|
|
assert.equal(app.icons.children.length, 2);
|
|
|
|
app.addIconContainer(document.createElement('div'), 1);
|
|
|
|
assert.equal(app.icons.children.length, 3);
|
|
assert.equal(app.icons.children[0].order, 1);
|
|
assert.equal(app.icons.children[1].order, 2);
|
|
assert.equal(app.icons.children[2].order, 3);
|
|
});
|
|
});
|
|
|
|
suite('App#addAppIcon()', () => {
|
|
var appIcon;
|
|
|
|
setup(() => {
|
|
appIcon = { manifestURL: {}, addEventListener: () => {} };
|
|
stubCreateElement();
|
|
});
|
|
|
|
teardown(() => {
|
|
restoreCreateElement();
|
|
});
|
|
|
|
test('should call refresh() on a gaia-app-icon element', () => {
|
|
var refreshStub = sinon.stub(gaiaAppIconEl, 'refresh');
|
|
app.addAppIcon(appIcon);
|
|
assert.isTrue(refreshStub.called);
|
|
});
|
|
|
|
test('should set entry points', () => {
|
|
app.addAppIcon(appIcon, { entryPoint1: {}, entryPoint2: {} });
|
|
assert.isNotNull(gaiaAppIconEl.entryPoint);
|
|
});
|
|
|
|
test('should be an app', () => {
|
|
app.addAppIcon(appIcon);
|
|
assert.isNotNull(gaiaAppIconEl.app);
|
|
assert.isNull(gaiaAppIconEl.bookmark);
|
|
});
|
|
|
|
test('should be a bookmark', () => {
|
|
app.addAppIcon({ id: 'abc', manifestURL: 'def' });
|
|
assert.isNull(gaiaAppIconEl.app);
|
|
assert.isNotNull(gaiaAppIconEl.bookmark);
|
|
});
|
|
|
|
test('should set the best icon according to the size available', () => {
|
|
var getIconBlobSpy = sinon.spy(IconsHelper, 'setElementIcon');
|
|
app.addAppIcon({ id: 'abc', addEventListener: () => {} });
|
|
assert.isTrue(getIconBlobSpy.called);
|
|
getIconBlobSpy.restore();
|
|
});
|
|
|
|
test('icon activated signal should be forwarded', () => {
|
|
var event;
|
|
var handleEventStub = sinon.stub(app, 'handleEvent', e => { event = e; });
|
|
app.addAppIcon({ id: 'abc', manifestURL: 'def' });
|
|
gaiaAppIconEl.dispatchEvent(new CustomEvent('activated'));
|
|
|
|
assert.isTrue(handleEventStub.called);
|
|
assert.equal(event.type, 'activate');
|
|
handleEventStub.restore();
|
|
});
|
|
});
|
|
|
|
suite('App#refreshIcon()', () => {
|
|
var mockAppIcon, mockBookmarkIcon;
|
|
setup(() => {
|
|
mockAppIcon = { refresh: () => {} };
|
|
mockBookmarkIcon = { bookmark: {}, refresh: () => {} };
|
|
});
|
|
|
|
test('sets icon size', () => {
|
|
app._iconSize = 10;
|
|
app.refreshIcon(mockAppIcon);
|
|
assert.equal(mockAppIcon.size, 10);
|
|
});
|
|
|
|
test('calls refresh() on app icons', () => {
|
|
var refreshStub = sinon.stub(mockAppIcon, 'refresh');
|
|
app.refreshIcon(mockAppIcon);
|
|
assert.isTrue(refreshStub.calledOnce);
|
|
});
|
|
|
|
test('calls IconsHelper.setElementIcon() on bookmark icons', () => {
|
|
var setElementIconStub = sinon.stub(MockIconsHelper, 'setElementIcon',
|
|
Promise.resolve.bind(Promise));
|
|
app.refreshIcon(mockBookmarkIcon);
|
|
assert.isTrue(setElementIconStub.calledOnce);
|
|
setElementIconStub.restore();
|
|
});
|
|
|
|
test('calls refresh if setElementIcon() Promise rejects', done => {
|
|
var setElementIconStub = sinon.stub(MockIconsHelper, 'setElementIcon',
|
|
Promise.reject.bind(Promise));
|
|
sinon.stub(mockBookmarkIcon, 'refresh', () => {
|
|
setElementIconStub.restore();
|
|
done();
|
|
});
|
|
app.refreshIcon(mockBookmarkIcon);
|
|
});
|
|
});
|
|
|
|
suite('App#storeAppOrder()', () => {
|
|
setup(() => {
|
|
app.metadata.mSetup();
|
|
});
|
|
|
|
test('should persist apps in sorted order', () => {
|
|
app.icons.appendChild(getIcon('abc'));
|
|
app.icons.appendChild(getIcon('def'));
|
|
app.icons.appendChild(getIcon('ghi'));
|
|
app.storeAppOrder();
|
|
assert.deepEqual(app.metadata._data, [
|
|
{ id: 'abc/', order: 0 },
|
|
{ id: 'def/', order: 1 },
|
|
{ id: 'ghi/', order: 2 }
|
|
]);
|
|
});
|
|
});
|
|
|
|
suite('App#refreshGridSize()', () => {
|
|
var realScrollable;
|
|
setup(() => {
|
|
realScrollable = app.scrollable;
|
|
app.scrollable = {
|
|
clientHeight: 200,
|
|
style: {}
|
|
};
|
|
});
|
|
|
|
teardown(() => {
|
|
app.scrollable = realScrollable;
|
|
});
|
|
|
|
suite('without icons', () => {
|
|
var realIcons;
|
|
setup(() => {
|
|
realIcons = app.icons;
|
|
app.icons = {
|
|
children: [],
|
|
style: {}
|
|
};
|
|
});
|
|
|
|
teardown(() => {
|
|
app.icons = realIcons;
|
|
});
|
|
|
|
test('should reset snap points when there are no icons', () => {
|
|
app.refreshGridSize();
|
|
assert.equal(app.scrollable.style.scrollSnapPointsY, 'repeat(200px)');
|
|
assert.equal(app.icons.style.backgroundSize, '100% 400px');
|
|
});
|
|
});
|
|
|
|
suite('with icons', () => {
|
|
var getBoundingClientRectStub;
|
|
setup(() => {
|
|
app.icons.appendChild(getIcon('abc'));
|
|
app.icons.appendChild(getIcon('def'));
|
|
app.icons.appendChild(getIcon('ghi'));
|
|
app.icons.appendChild(getIcon('jkl'));
|
|
getBoundingClientRectStub = sinon.stub(app.icons.firstElementChild,
|
|
'getBoundingClientRect', () => {
|
|
return { height: 100 };
|
|
});
|
|
});
|
|
|
|
teardown(() => {
|
|
getBoundingClientRectStub.restore();
|
|
});
|
|
|
|
test('should snap to 3 icon rows when the screen is 399px', () => {
|
|
app.scrollable.clientHeight = 399;
|
|
app.refreshGridSize();
|
|
assert.equal(app.scrollable.style.scrollSnapPointsY, 'repeat(300px)');
|
|
assert.equal(app.icons.style.backgroundSize, '100% 600px');
|
|
});
|
|
|
|
test('should snap to 4 icon rows when the screen is 400px', () => {
|
|
app.scrollable.clientHeight = 400;
|
|
app.refreshGridSize();
|
|
assert.equal(app.scrollable.style.scrollSnapPointsY, 'repeat(400px)');
|
|
assert.equal(app.icons.style.backgroundSize, '100% 800px');
|
|
});
|
|
|
|
test('should snap to 4 icon rows when the screen is 401px', () => {
|
|
app.scrollable.clientHeight = 401;
|
|
app.refreshGridSize();
|
|
assert.equal(app.scrollable.style.scrollSnapPointsY, 'repeat(400px)');
|
|
assert.equal(app.icons.style.backgroundSize, '100% 800px');
|
|
});
|
|
|
|
test('should size to always have a whole page visible', () => {
|
|
app.scrollable.clientHeight = 150;
|
|
app.settings.scrollSnapping = true;
|
|
app.refreshGridSize();
|
|
assert.equal(app.pendingGridHeight, 250);
|
|
});
|
|
|
|
test('should size to one extra row with snapping disabled', () => {
|
|
app.scrollable.clientHeight = 150;
|
|
app.settings.scrollSnapping = false;
|
|
app.refreshGridSize();
|
|
assert.equal(app.pendingGridHeight, 300);
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('App#snapScrollPosition()', () => {
|
|
var realScrollable, scrollToSpy;
|
|
setup(() => {
|
|
realScrollable = app.scrollable;
|
|
app.scrollable = {
|
|
clientHeight: 100,
|
|
scrollTop: 0,
|
|
style: {},
|
|
scrollTo: () => {}
|
|
};
|
|
scrollToSpy = sinon.spy(app.scrollable, 'scrollTo');
|
|
app.settings.scrollSnapping = true;
|
|
});
|
|
|
|
teardown(() => {
|
|
app.settings.scrollSnapping = false;
|
|
app.scrollable = realScrollable;
|
|
});
|
|
|
|
test('should do nothing if already aligned', () => {
|
|
app.pendingGridHeight = 500;
|
|
app.pageHeight = 100;
|
|
|
|
app.scrollable.scrollTop = 0;
|
|
app.snapScrollPosition();
|
|
assert.isFalse(scrollToSpy.called);
|
|
|
|
app.scrollable.scrollTop = 100;
|
|
app.snapScrollPosition();
|
|
assert.isFalse(scrollToSpy.called);
|
|
});
|
|
|
|
test('should do nothing if nearly aligned', () => {
|
|
app.pendingGridHeight = 500;
|
|
app.pageHeight = 100;
|
|
app.scrollable.scrollTop = 101;
|
|
|
|
app.snapScrollPosition();
|
|
assert.isFalse(scrollToSpy.called);
|
|
});
|
|
|
|
test('should remove overflow and scroll to nearest snap point', () => {
|
|
app.pendingGridHeight = 500;
|
|
app.pageHeight = 100;
|
|
app.scrollable.scrollTop = 10;
|
|
|
|
app.snapScrollPosition();
|
|
assert.equal(app.scrollable.style.overflow, '');
|
|
assert.isTrue(scrollToSpy.calledWith(
|
|
{ left: 0, top: 0, behavior: 'smooth' }));
|
|
});
|
|
|
|
test('should do nothing if snapping disabled', () => {
|
|
app.settings.scrollSnapping = false;
|
|
app.pendingGridHeight = 500;
|
|
app.pageHeight = 100;
|
|
app.scrollable.scrollTop = 10;
|
|
|
|
app.snapScrollPosition();
|
|
assert.isFalse(scrollToSpy.called);
|
|
});
|
|
});
|
|
|
|
suite('App#showActionDialog()', () => {
|
|
var clock;
|
|
|
|
setup(function() {
|
|
clock = sinon.useFakeTimers();
|
|
});
|
|
|
|
teardown(function() {
|
|
clock.restore();
|
|
});
|
|
|
|
test('should attach events on click', () => {
|
|
var dialog = app.dialogs[0];
|
|
dialog.open = () => {};
|
|
app.showActionDialog(dialog, null, [() => {}]);
|
|
assert.ok(dialog.querySelector('button').onclick);
|
|
});
|
|
|
|
test('should set the title of the dialog', () => {
|
|
var dialog = app.cancelDownload;
|
|
dialog.open = () => {};
|
|
assert.isFalse(dialog.querySelector('.body')
|
|
.hasAttribute('data-l10n-args'));
|
|
app.showActionDialog(dialog, 'abc', []);
|
|
assert.isTrue(dialog.querySelector('.body')
|
|
.hasAttribute('data-l10n-args'));
|
|
clock.tick(10000); // Using a long number to avoid exporting the const.
|
|
});
|
|
|
|
test('should show the dialog after a little while', done => {
|
|
var dialog = {
|
|
style: { display: 'none' },
|
|
getElementsByClassName: () => [],
|
|
querySelector: () => {},
|
|
open: () => {
|
|
done();
|
|
}
|
|
};
|
|
|
|
app.showActionDialog(dialog, null, []);
|
|
clock.tick(10000);
|
|
});
|
|
});
|
|
|
|
suite('App#removeSelectedIcon()', () => {
|
|
var uninstallStub, clock;
|
|
setup(() => {
|
|
MockMozActivity.mSetup();
|
|
uninstallStub = sinon.stub(navigator.mozApps.mgmt, 'uninstall');
|
|
clock = sinon.useFakeTimers();
|
|
});
|
|
|
|
teardown(() => {
|
|
MockMozActivity.mTeardown();
|
|
uninstallStub.restore();
|
|
clock.restore();
|
|
});
|
|
|
|
test('does nothing with no selected icon', () => {
|
|
app.selectedIcon = null;
|
|
app.removeSelectedIcon();
|
|
assert.isFalse(uninstallStub.called);
|
|
assert.equal(MockMozActivity.calls.length, 0);
|
|
});
|
|
|
|
test('does nothing for unremovable apps', () => {
|
|
app.selectedIcon = { app: { removable: false } };
|
|
app.removeSelectedIcon();
|
|
assert.isFalse(uninstallStub.called);
|
|
});
|
|
|
|
test('uninstalls app icons', () => {
|
|
app.selectedIcon = { app: { removable: true } };
|
|
app.removeSelectedIcon();
|
|
assert.isTrue(uninstallStub.calledWith(app.selectedIcon.app));
|
|
});
|
|
|
|
test('removes bookmark icons', () => {
|
|
app.selectedIcon = { bookmark: { id: 'abc' } };
|
|
app.removeSelectedIcon();
|
|
assert.equal(MockMozActivity.calls.length, 1);
|
|
assert.equal(MockMozActivity.calls[0].name, 'remove-bookmark');
|
|
assert.equal(MockMozActivity.calls[0].data.type, 'url');
|
|
assert.equal(MockMozActivity.calls[0].data.url,
|
|
app.selectedIcon.bookmark.id);
|
|
});
|
|
|
|
test('re-enters edit mode when removing bookmarks', () => {
|
|
var enterEditModeStub = sinon.stub(app, 'enterEditMode');
|
|
app.selectedIcon = { bookmark: { id: 'abc' } };
|
|
app.removeSelectedIcon();
|
|
|
|
MockMozActivity.mTriggerOnError();
|
|
assert.isTrue(enterEditModeStub.calledWith(app.selectedIcon));
|
|
|
|
clock.tick(50);
|
|
assert.isTrue(enterEditModeStub.calledWith(null));
|
|
});
|
|
});
|
|
|
|
suite('App#renameSelectedIcon()', () => {
|
|
var clock;
|
|
setup(() => {
|
|
MockMozActivity.mSetup();
|
|
clock = sinon.useFakeTimers();
|
|
});
|
|
|
|
teardown(() => {
|
|
MockMozActivity.mTeardown();
|
|
clock.restore();
|
|
});
|
|
|
|
test('does nothing with no selected icon', () => {
|
|
app.selectedIcon = null;
|
|
app.renameSelectedIcon();
|
|
assert.equal(MockMozActivity.calls.length, 0);
|
|
});
|
|
|
|
test('does nothing with no bookmark icon selected', () => {
|
|
app.selectedIcon = { app: {} };
|
|
app.renameSelectedIcon();
|
|
assert.equal(MockMozActivity.calls.length, 0);
|
|
});
|
|
|
|
test('edits bookmark icons', () => {
|
|
app.selectedIcon = { bookmark: { id: 'abc' } };
|
|
app.renameSelectedIcon();
|
|
assert.equal(MockMozActivity.calls.length, 1);
|
|
assert.equal(MockMozActivity.calls[0].name, 'save-bookmark');
|
|
assert.equal(MockMozActivity.calls[0].data.type, 'url');
|
|
assert.equal(MockMozActivity.calls[0].data.url,
|
|
app.selectedIcon.bookmark.id);
|
|
});
|
|
|
|
test('re-enters edit mode', () => {
|
|
var enterEditModeStub = sinon.stub(app, 'enterEditMode');
|
|
app.selectedIcon = { bookmark: { id: 'abc' } };
|
|
app.renameSelectedIcon();
|
|
|
|
MockMozActivity.mTriggerOnError();
|
|
assert.isTrue(enterEditModeStub.calledWith(app.selectedIcon));
|
|
|
|
clock.tick(50);
|
|
assert.isTrue(enterEditModeStub.calledWith(app.selectedIcon));
|
|
});
|
|
});
|
|
|
|
suite('App#iconIsEditable()', () => {
|
|
var icon;
|
|
test('bookmark icons are editable', () => {
|
|
icon = { bookmark: {} };
|
|
assert.isTrue(app.iconIsEditable(icon));
|
|
});
|
|
|
|
test('removable app icons are editable', () => {
|
|
icon = { app: { removable: true } };
|
|
assert.isTrue(app.iconIsEditable(icon));
|
|
});
|
|
|
|
test('unremovable app icons are not editable', () => {
|
|
icon = { app: { removable: false } };
|
|
assert.isFalse(app.iconIsEditable(icon));
|
|
});
|
|
});
|
|
|
|
suite('App#updateSelectedIcon()', () => {
|
|
var icon;
|
|
setup(() => {
|
|
icon = document.createElement('div');
|
|
});
|
|
|
|
test('selects bookmark icons', () => {
|
|
icon.bookmark = {};
|
|
app.selectedIcon = null;
|
|
app.updateSelectedIcon(icon);
|
|
assert.equal(app.selectedIcon, icon);
|
|
assert.isTrue(icon.classList.contains('selected'));
|
|
assert.isTrue(app.remove.classList.contains('active'));
|
|
assert.isTrue(app.rename.classList.contains('active'));
|
|
});
|
|
|
|
test('selects removable app icons', () => {
|
|
icon.app = { removable: true };
|
|
app.selectedIcon = null;
|
|
app.updateSelectedIcon(icon);
|
|
assert.equal(app.selectedIcon, icon);
|
|
assert.isTrue(icon.classList.contains('selected'));
|
|
assert.isTrue(app.remove.classList.contains('active'));
|
|
assert.isFalse(app.rename.classList.contains('active'));
|
|
});
|
|
|
|
test('deselects old selected icon', () => {
|
|
icon.bookmark = {};
|
|
var oldIcon = document.createElement('div');
|
|
app.selectedIcon = oldIcon;
|
|
app.selectedIcon.classList.add('selected');
|
|
app.updateSelectedIcon(icon);
|
|
assert.isFalse(oldIcon.classList.contains('selected'));
|
|
});
|
|
|
|
test('marks unremovable apps as un-editable', () => {
|
|
icon.app = { removable: false };
|
|
app.selectedIcon = null;
|
|
app.updateSelectedIcon(icon);
|
|
assert.notEqual(app.selectedIcon, icon);
|
|
assert.isFalse(icon.classList.contains('selected'));
|
|
assert.isTrue(icon.classList.contains('uneditable'));
|
|
});
|
|
});
|
|
|
|
suite('App#enterEditMode()', () => {
|
|
var updateSelectedIconSpy, mockIcon;
|
|
|
|
setup(() => {
|
|
mockIcon = document.createElement('div');
|
|
mockIcon.bookmark = {};
|
|
|
|
updateSelectedIconSpy = sinon.spy(app, 'updateSelectedIcon');
|
|
|
|
app.editMode = false;
|
|
app.selectedIcon = null;
|
|
document.body.classList.remove('edit-mode');
|
|
app.enterEditMode(mockIcon);
|
|
});
|
|
|
|
teardown(() => {
|
|
updateSelectedIconSpy.restore();
|
|
});
|
|
|
|
test('updates the selected icon', () => {
|
|
assert.isTrue(updateSelectedIconSpy.calledWith(mockIcon));
|
|
});
|
|
|
|
test('document is marked as in edit mode', () => {
|
|
assert.isTrue(app.editMode);
|
|
assert.isTrue(document.body.classList.contains('edit-mode'));
|
|
});
|
|
});
|
|
|
|
suite('App#exitEditMode()', () => {
|
|
var updateSelectedIconStub;
|
|
setup(() => {
|
|
updateSelectedIconStub = sinon.stub(app, 'updateSelectedIcon');
|
|
app.editMode = true;
|
|
app.exitEditMode();
|
|
});
|
|
|
|
teardown(() => {
|
|
updateSelectedIconStub.restore();
|
|
});
|
|
|
|
test('document is marked as not in edit mode', () => {
|
|
assert.isFalse(document.body.classList.contains('edit-mode'));
|
|
assert.isFalse(app.rename.classList.contains('active'));
|
|
assert.isFalse(app.remove.classList.contains('active'));
|
|
});
|
|
|
|
test('icon is deselected', () => {
|
|
assert.isTrue(updateSelectedIconStub.calledWith(null));
|
|
});
|
|
});
|
|
|
|
suite('App#handleEvent()', () => {
|
|
suite('activate', () => {
|
|
test('should be preventDefaulted', () => {
|
|
var icon = getIcon('abc');
|
|
icon.firstElementChild.launch = () => {};
|
|
var defaultPrevented = false;
|
|
app.handleEvent({
|
|
type: 'activate',
|
|
preventDefault: () => { defaultPrevented = true; },
|
|
detail: { target: icon }
|
|
});
|
|
assert.isTrue(defaultPrevented);
|
|
});
|
|
|
|
test('unrecoverable app should be removed', () => {
|
|
var uninstallStub = sinon.stub(MockNavigatormozApps.mgmt, 'uninstall');
|
|
var icon = getIcon('abc');
|
|
icon.firstElementChild.state = 'unrecoverable';
|
|
app.handleEvent(new CustomEvent('activate',
|
|
{ detail: { target: icon } }));
|
|
assert.isTrue(uninstallStub.called);
|
|
});
|
|
|
|
test('installing app should open a cancel download dialog', () => {
|
|
var showActionDialogStub = sinon.stub(app, 'showActionDialog');
|
|
var icon = getIcon('abc');
|
|
icon.firstElementChild.state = 'installing';
|
|
icon.firstElementChild.name = 'Name';
|
|
app.handleEvent(new CustomEvent('activate',
|
|
{ detail: { target: icon } }));
|
|
assert.isTrue(showActionDialogStub.called);
|
|
var stubCall = showActionDialogStub.getCall(0);
|
|
assert.equal(stubCall.args[0], app.cancelDownload);
|
|
showActionDialogStub.restore();
|
|
});
|
|
|
|
suite('error and paused', () => {
|
|
var showActionDialogStub;
|
|
|
|
var testApp = state => {
|
|
showActionDialogStub = sinon.stub(app, 'showActionDialog');
|
|
var icon = getIcon('abc');
|
|
icon.firstElementChild.state = state;
|
|
icon.firstElementChild.name = 'Name';
|
|
app.handleEvent(new CustomEvent('activate',
|
|
{ detail: { target: icon } }));
|
|
};
|
|
|
|
teardown(() => {
|
|
showActionDialogStub.restore();
|
|
});
|
|
|
|
test('error app should open a resume download dialog', () => {
|
|
testApp('error');
|
|
assert.isTrue(showActionDialogStub.called);
|
|
var stubCall = showActionDialogStub.getCall(0);
|
|
assert.equal(stubCall.args[0], app.resumeDownload);
|
|
});
|
|
|
|
test('paused app should open a resume download dialog', () => {
|
|
testApp('paused');
|
|
assert.isTrue(showActionDialogStub.called);
|
|
var stubCall = showActionDialogStub.getCall(0);
|
|
assert.equal(stubCall.args[0], app.resumeDownload);
|
|
});
|
|
});
|
|
|
|
suite('drag-move', () => {
|
|
var setIntervalStub, clearIntervalStub;
|
|
|
|
setup(() => {
|
|
app.lastWindowWidth = app.lastWindowHeight = 500;
|
|
setIntervalStub = sinon.stub(window, 'setInterval');
|
|
clearIntervalStub = sinon.stub(window, 'clearInterval');
|
|
|
|
app.draggingRemovable = app.draggingEditable = true;
|
|
});
|
|
|
|
teardown(() => {
|
|
setIntervalStub.restore();
|
|
clearIntervalStub.restore();
|
|
});
|
|
|
|
test('Auto-scroll is activated at the top of the screen', () => {
|
|
app.handleEvent(new CustomEvent('drag-move', { detail: {
|
|
clientX: 0,
|
|
clientY: 0
|
|
}}));
|
|
|
|
assert.isTrue(setIntervalStub.called);
|
|
});
|
|
|
|
test('Auto-scroll is activated at the bottom of the screen', () => {
|
|
app.handleEvent(new CustomEvent('drag-move', { detail: {
|
|
clientX: 0,
|
|
clientY: 500
|
|
}}));
|
|
|
|
assert.isTrue(setIntervalStub.called);
|
|
});
|
|
|
|
test('Auto-scroll is cancelled when not at the top or bottom', () => {
|
|
app.autoScrollInterval = 'abc';
|
|
app.icons.getChildFromPoint = () => { return null; };
|
|
|
|
app.handleEvent(new CustomEvent('drag-move', { detail: {
|
|
clientX: 0,
|
|
clientY: 250
|
|
}}));
|
|
|
|
assert.isTrue(clearIntervalStub.calledWith('abc'));
|
|
delete app.icons.getChildFromPoint;
|
|
});
|
|
});
|
|
|
|
suite('drag-finish', () => {
|
|
test('auto-scroll interval should be cancelled', () => {
|
|
var clearIntervalStub = sinon.stub(window, 'clearInterval');
|
|
app.autoScrollInterval = 'abc';
|
|
app.handleEvent(new CustomEvent('drag-finish'));
|
|
clearIntervalStub.restore();
|
|
|
|
assert.isTrue(clearIntervalStub.calledWith('abc'));
|
|
assert.equal(app.autoScrollTimeout, null);
|
|
});
|
|
});
|
|
|
|
suite('drag-end', () => {
|
|
var realInnerHeight, realInnerWidth, realIcons, reorderChildSpy;
|
|
var mockIconContainer = { firstElementChild: 'icon' };
|
|
|
|
setup(() => {
|
|
realInnerHeight =
|
|
Object.getOwnPropertyDescriptor(window, 'innerHeight');
|
|
realInnerWidth =
|
|
Object.getOwnPropertyDescriptor(window, 'innerWidth');
|
|
Object.defineProperty(window, 'innerHeight', {
|
|
value: 500,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(window, 'innerWidth', {
|
|
value: 500,
|
|
configurable: true
|
|
});
|
|
|
|
realIcons = app.icons;
|
|
app.icons = {
|
|
firstChild: 'abc',
|
|
getChildOffsetRect: () => {
|
|
return { left: 0, top: 0, right: 10, bottom: 10 };
|
|
},
|
|
reorderChild: () => {}
|
|
};
|
|
app.iconsLeft = 10;
|
|
app.iconsRight = 490;
|
|
|
|
reorderChildSpy = sinon.spy(app.icons, 'reorderChild');
|
|
});
|
|
|
|
teardown(() => {
|
|
app.icons = realIcons;
|
|
reorderChildSpy.restore();
|
|
Object.defineProperty(window, 'innerHeight', realInnerHeight);
|
|
Object.defineProperty(window, 'innerWidth', realInnerWidth);
|
|
});
|
|
|
|
test('icon can be dropped at the beginning of the container', () => {
|
|
app.handleEvent(new CustomEvent('drag-end', {
|
|
detail:
|
|
{ target: 'def', dropTarget: null, clientX: 250, clientY: -100 }
|
|
}));
|
|
assert.isTrue(reorderChildSpy.calledWith('def', 'abc'));
|
|
});
|
|
|
|
test('icon can be dropped at the end of the container', () => {
|
|
app.handleEvent(new CustomEvent('drag-end', {
|
|
detail:
|
|
{ target: 'def', dropTarget: null, clientX: 250, clientY: 600 }
|
|
}));
|
|
assert.isTrue(reorderChildSpy.calledWith('def', null));
|
|
});
|
|
|
|
test('dropping icon on itself does nothing', () => {
|
|
app.handleEvent(new CustomEvent('drag-end', {
|
|
detail: {
|
|
target: 'abc',
|
|
dropTarget: mockIconContainer,
|
|
clientX: 0,
|
|
clientY: 0
|
|
}
|
|
}));
|
|
assert.isFalse(reorderChildSpy.called);
|
|
});
|
|
|
|
test('dropping icon to the side does nothing', () => {
|
|
app.handleEvent(new CustomEvent('drag-end', {
|
|
detail:
|
|
{ target: 'def', dropTarget: null, clientX: 5, clientY: 0 }
|
|
}));
|
|
app.handleEvent(new CustomEvent('drag-end', {
|
|
detail:
|
|
{ target: 'def', dropTarget: null, clientX: 495, clientY: 0 }
|
|
}));
|
|
assert.isFalse(reorderChildSpy.called);
|
|
});
|
|
|
|
test('dropping icon without moving activates edit mode', () => {
|
|
var enterEditModeStub = sinon.stub(app, 'enterEditMode');
|
|
var iconEditableStub = sinon.stub(app, 'iconIsEditable', () => true);
|
|
|
|
app.shouldEnterEditMode = true;
|
|
app.handleEvent(new CustomEvent('drag-end', {
|
|
detail: { target: mockIconContainer,
|
|
dropTarget: mockIconContainer,
|
|
clientX: 0, clientY: 0 }
|
|
}));
|
|
assert.isTrue(
|
|
enterEditModeStub.calledWith(mockIconContainer.firstElementChild));
|
|
|
|
enterEditModeStub.restore();
|
|
iconEditableStub.restore();
|
|
});
|
|
});
|
|
|
|
test('app with default state should be launched', done => {
|
|
var icon = getIcon('abc');
|
|
icon.firstElementChild.state = 'unknownState';
|
|
icon.firstElementChild.name = 'Name';
|
|
icon.firstElementChild.launch = () => {
|
|
done();
|
|
};
|
|
app.handleEvent(new CustomEvent('activate',
|
|
{ detail: { target: icon } }));
|
|
});
|
|
});
|
|
|
|
suite('resize', () => {
|
|
var synchroniseStub, refreshGridSizeStub, snapScrollPositionStub;
|
|
|
|
setup(() => {
|
|
app.icons.synchronise = () => {};
|
|
synchroniseStub = sinon.stub(app.icons, 'synchronise');
|
|
refreshGridSizeStub = sinon.stub(app, 'refreshGridSize');
|
|
snapScrollPositionStub = sinon.stub(app, 'snapScrollPosition');
|
|
app.lastWindowWidth = app.lastWindowHeight = null;
|
|
app.handleEvent(new CustomEvent('resize'));
|
|
});
|
|
|
|
teardown(() => {
|
|
synchroniseStub.restore();
|
|
refreshGridSizeStub.restore();
|
|
snapScrollPositionStub.restore();
|
|
delete app.icons.synchronise;
|
|
});
|
|
|
|
test('should call icons.synchronise()', () => {
|
|
assert.isTrue(synchroniseStub.called);
|
|
});
|
|
|
|
test('should call refreshGridSize()', () => {
|
|
assert.isTrue(refreshGridSizeStub.called);
|
|
});
|
|
|
|
test('should call snapScrollPosition()', () => {
|
|
assert.isTrue(snapScrollPositionStub.called);
|
|
});
|
|
|
|
test('should reset icon size', () => {
|
|
assert.equal(app._iconSize, 0);
|
|
});
|
|
|
|
test('should refresh incorrectly sized icons', () => {
|
|
app._iconSize = 1000;
|
|
var refreshIconStub = sinon.stub(app, 'refreshIcon');
|
|
Object.defineProperty(app.icons, 'children', {
|
|
value: [{
|
|
firstElementChild: 'abc',
|
|
style: { display: 'block' }
|
|
}],
|
|
configurable: true
|
|
});
|
|
|
|
app.lastWindowWidth = app.lastWindowHeight = null;
|
|
app.handleEvent(new CustomEvent('resize'));
|
|
assert.isTrue(refreshIconStub.calledWith('abc'));
|
|
|
|
refreshIconStub.restore();
|
|
delete app.icons.children;
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('install', () => {
|
|
test('newly installed apps should be added to the homescreen', done => {
|
|
var addAppStub = sinon.stub(app, 'addApp', () => {
|
|
addAppStub.restore();
|
|
done();
|
|
});
|
|
app.handleEvent(new CustomEvent('install',
|
|
{ detail: { application: {} } }));
|
|
});
|
|
});
|
|
});
|