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

61
gaia/apps/search/build/build.js Executable file
View file

@ -0,0 +1,61 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
var SearchAppBuilder = function() {
};
function pickAsset(root, filename, ppx) {
if (ppx !== '1') {
var suffix = '@' + ppx + 'x';
var hdpiFile = filename.replace(/(\.[a-z]+$)/, suffix + '$1');
var file = utils.getFile(root.path, hdpiFile);
if (file.exists()) {
return file;
}
}
return utils.getFile(root.path, filename);
}
SearchAppBuilder.prototype.setOptions = function(options) {
this.stageDir = utils.getFile(options.STAGE_APP_DIR);
this.appDir = utils.getFile(options.APP_DIR);
this.buildDir = utils.getFile(this.appDir.path, '/build');
this.distDirPath = options.GAIA_DISTRIBUTION_DIR;
};
SearchAppBuilder.prototype.initTopsitesJSON = function() {
var defaultJSONpath =
utils.joinPath(this.appDir.path, 'build', 'topsites.json');
var defaultJson = utils.getJSON(utils.getFile(defaultJSONpath));
defaultJson.forEach(function(site) {
if (site.tilePath) {
var file = pickAsset(this.buildDir, site.tilePath,
this.options.GAIA_DEV_PIXELS_PER_PX);
var icon = utils.getFileAsDataURI(file);
site.tile = icon;
delete site.tilePath;
}
}.bind(this));
var file = utils.getFile(this.stageDir.path, 'js', 'inittopsites.json');
var fileContents = utils.getDistributionFileContent('topsites', defaultJson,
this.distDirPath);
utils.writeContent(file, fileContents);
};
SearchAppBuilder.prototype.execute = function(options) {
this.options = options;
this.setOptions(options);
if (options.PROFILE_FOLDER !== 'profile-test') {
this.initTopsitesJSON();
}
};
exports.execute = function(options) {
utils.copyToStage(options);
(new SearchAppBuilder()).execute(options);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,14 @@
[
{
"title": "Mozilla",
"url": "https://mozilla.org",
"frecency": 0,
"tilePath": "mozilla.org.jpg"
},
{
"title": "Mozilla Support",
"url": "https://support.mozilla.org",
"frecency": 0,
"tilePath": "mozillasupport.jpg"
}
]

115
gaia/apps/search/index.html Executable file
View file

@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0">
<link rel="stylesheet" href="style/search.css">
<link rel="stylesheet" href="shared/elements/gaia_grid/style/external.css">
<link rel="stylesheet" href="shared/style/progress_activity.css">
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/search.{locale}.properties">
<link rel="localization" href="/shared/elements/gaia_grid/locales/gaia_grid.{locale}.properties">
<script defer src="shared/js/l10n.js"></script>
<script defer src="shared/js/lazy_loader.js"></script>
<script defer src="shared/js/async_storage.js"></script>
<script defer src="shared/js/utilities.js"></script>
<script defer src="shared/js/settings_listener.js"></script>
<script defer src="shared/js/format.js"></script>
<script defer src="shared/js/search_provider.js"></script>
<!-- <script defer src="shared/js/search_providers.json"></script> -->
<!-- Confirm dialog component -->
<script defer src="/shared/js/component_utils.js"></script>
<script defer src="/shared/elements/gaia_buttons/script.js"></script>
<script defer src="/shared/elements/gaia_confirm/script.js"></script>
<!--- gaia_grid required scripts -->
<script defer src="shared/js/homescreens/homescreen_settings.js"></script>
<script defer src="shared/js/homescreens/confirm_dialog_helper.js"></script>
<script defer src="shared/elements/gaia_grid/js/grid_view.js"></script>
<script defer src="shared/elements/gaia_grid/js/grid_icon_renderer.js"></script>
<script defer src="shared/elements/gaia_grid/js/grid_layout.js"></script>
<script defer src="shared/elements/gaia_grid/js/grid_zoom.js"></script>
<script defer src="shared/elements/gaia_grid/script.js"></script>
<script defer src="shared/elements/gaia_grid/js/items/grid_item.js"></script>
<script defer src="shared/elements/gaia_grid/js/items/bookmark.js"></script>
<script defer src="shared/elements/gaia_grid/js/items/marketplace_app.js"></script>
<script defer src="shared/elements/gaia_grid/js/items/divider.js"></script>
<script defer src="shared/elements/gaia_grid/js/items/mozapp.js"></script>
<script defer src="shared/elements/gaia_grid/js/items/placeholder.js"></script>
<script defer src="shared/js/async_storage.js"></script>
<script defer src="shared/js/url_helper.js"></script>
<script defer src="shared/js/web_manifest_helper.js"></script>
<script defer src="shared/js/utilities.js"></script>
<script defer src="shared/js/dedupe.js"></script>
<script defer src="js/places_idb.js"></script>
<script defer src="shared/js/sync_datastore.js"></script>
<script defer src="shared/js/icons_helper.js"></script>
<script defer src="shared/js/metrics_helper.js"></script>
<script defer src="js/contextmenu.js"></script>
<script defer src="js/search.js"></script>
<script defer src="js/providers/provider.js"></script>
<script defer src="js/providers/grid_provider.js"></script>
<!-- Ordering of the following files will impact which queries we send first -->
<script defer src="js/providers/marketplace.js"></script>
<script defer src="js/providers/suggestions.js"></script>
<script defer src="js/providers/bookmarks.js"></script>
<script defer src="js/providers/places.js"></script>
<script defer src="js/providers/local_apps.js"></script>
<!-- Include the app icon in the application.zip -->
<link rel="resource" type="image/png" href="shared/resources/branding/browser_84.png"/>
<link rel="resource" type="image/png" href="shared/resources/branding/browser_126.png"/>
<link rel="resource" type="image/png" href="shared/resources/branding/browser_142.png"/>
<link rel="resource" type="image/png" href="shared/resources/branding/browser_189.png"/>
<link rel="resource" type="image/png" href="shared/resources/branding/browser_284.png"/>
</head>
<body>
<div id="search-results" class="hidden">
<div id="offline-message">
<span id="settings-connectivity"></span>
<span data-l10n-id="offline-message"></span>
</div>
<section id="contacts" class="local" data-l10n-id="contacts"
aria-label="Contacts"></section>
<section id="icons-wrapper" class="apps" data-l10n-id="apps" aria-label="Apps">
<gaia-grid cols="4" text-rows="3" id="icons"></gaia-grid>
</section>
<h2 id="places-header" class="hidden"><span data-l10n-id="history"></span></h2>
<section id="places" class="local" data-l10n-id="places"
aria-label="Places"></section>
<div id="suggestions-wrapper">
<h2>
<select id="suggestions-select"></select>
<label for="suggestions-select" id="suggestions-provider"></label>
<span id="suggestions-provider-dropdown"></span>
</h2>
<div id="suggestions-notice-wrapper" hidden>
<p data-l10n-id="suggestions-settings-notice">
<span id="settings-link"></span>
</p>
<button id="suggestions-notice-confirm" data-l10n-id="ok"></button>
</div>
<section id="suggestions"
class="suggestions" data-l10n-id="suggestions"
aria-label="Suggestions"></section>
<div class="loading"><progress></progress></div>
</div>
</div>
<menu id="contextmenu" hidden>
<menuitem id="add-to-homescreen" data-l10n-id="add-to-home-screen" type="button"></menuitem>
</menu>
</body>
</html>

View file

@ -0,0 +1,92 @@
'use strict';
/* global MozActivity */
/* global Bookmarks */
(function(exports) {
/**
* The search context menu handles bookmarking of search
* results when the users long presses them.
*/
function Contextmenu() {
this.menu = document.getElementById('contextmenu');
this.grid = document.getElementById('icons');
this.bookmarkButton = this.menu.querySelector('#add-to-homescreen');
this.grid.addEventListener('contextmenu', this);
this.bookmarkButton.addEventListener('click', this);
}
Contextmenu.prototype = {
/**
* The current icon target.
*/
icon: null,
handleEvent: function(e) {
switch(e.type) {
case 'contextmenu':
var identifier = e.target.dataset.identifier;
var icon = this.grid.getIcon(identifier);
// Only show for bookmark targets
if (!icon || icon.detail.type !== 'bookmark') {
return;
}
var url = icon.detail.url;
if (Bookmarks.get(url)) {
// Bookmark already installed in device
return;
}
// In order to benefit from the system contextmenu in such a way that
// it overlaps search bar, let's create a contextmenu attribute on
// the fly, and remove it once the event dispatching is done.
e.target.setAttribute('contextmenu', 'contextmenu');
// Stop the grid from listening to events to prevent it from
// receiving a touchend event and launching the icon. This is
// restored on next tick.
this.grid.stop();
setTimeout(() => {
e.target.removeAttribute('contextmenu');
this.grid.start();
});
this.icon = icon;
break;
case 'click':
/* jshint nonew: false */
/**
* We could have several cases for icons in the search grid:
* - Icons from e.me: this.icon.icon will be url
* - Icons from places: this.icon.icon can be:
* - a blob, cached icon
* - a icon url
* To solve this, added an extra feature attribute: iconUrl,
* so GaiaGridItems with this extra attribute in the detail
* will have a way to keep track of the original url.
*/
var iconUrl = this.icon.detail.iconUrl || this.icon.icon;
new MozActivity({
name: 'save-bookmark',
data: {
type: 'url',
url: this.icon.detail.url,
name: this.icon.name,
icon: iconUrl
}
});
break;
}
}
};
exports.Contextmenu = Contextmenu;
}(window));

197
gaia/apps/search/js/newtab.js Executable file
View file

@ -0,0 +1,197 @@
'use strict';
/* global asyncStorage, MozActivity, SettingsListener */
(function(exports) {
var isManuallyPrivate = window.location.search.includes('private=1');
var isManuallyRegular = window.location.search.includes('private=0');
var isPrivateByDefault = false;
SettingsListener.observe('browser.private.default', false, function (value) {
if (value) {
isPrivateByDefault = true;
} else {
isPrivateByDefault = false;
}
exports.newtab.togglePrivacyMode();
});
/**
* The main Newtab page object.
* Instantiates places to populate history and top sites.
*/
function Newtab() {
// Initialize the parent port connection
var self = this;
navigator.mozApps.getSelf().onsuccess = function() {
var app = this.result;
app.connect('search-results').then(function onConnAccepted(ports) {
ports.forEach(function(port) {
self._port = port;
});
self.init();
}, function onConnectionRejected(reason) {
console.log('Error connecting: ' + reason + '\n');
});
};
this.togglePrivacyMode();
}
Newtab.prototype = {
/**
* A reference to the Places provider.
*/
provider: null,
/**
* Initializes top sites and history.
*/
init: function() {
this.provider.init();
this.provider.searchObj = this;
},
/**
* Requests a screenshot of the page from the system app.
*/
requestScreenshot: function(url) {
this._port.postMessage({
'action': 'request-screenshot',
'url': url
});
},
/**
* Requests that the system app opens a new private window.
*/
requestPrivateWindow: function() {
this._port.postMessage({
'action': 'private-window'
});
},
/**
* Toggles the privacy mode.
*
* When private browsing _IS_ the default and we _ARE NOT_ manually being
* regular or when private browsing _IS NOT_ the default and we _ARE_
* manually bring private: Makes this window look like a private window by
* applying the necessary theme color and background. The window can't be
* truly private, otherwise we wouldn't have access to local history and
* bookmarks.
*
* When private browsing _IS_ the default and we _ARE_ manually regular:
* Makes this window look normal by removing private theme settings.
*/
togglePrivacyMode: function() {
var themeMeta = document.querySelector('meta[name="theme-color"]');
var themeColor = document.createElement('meta');
if ((isPrivateByDefault && !isManuallyRegular) ||
(!isPrivateByDefault && isManuallyPrivate)) {
if (themeMeta) {
document.head.removeChild(themeMeta);
}
themeColor.setAttribute('name', 'theme-color');
themeColor.setAttribute('content', '#392E54');
document.head.appendChild(themeColor);
document.body.classList.add('private');
// Show the private dialog if needed.
asyncStorage.getItem('shouldSuppressPrivateDialog', value => {
if (value) {
return;
}
this.privateBrowserDialog = document.getElementById(
'private-window-dialog');
this.privateBrowserDialog.removeAttribute('hidden');
this.privateBrowserDialogClose = document.getElementById(
'private-window-hide-dialog');
this.privateBrowserDialogClose.addEventListener('click', this);
this.privateBrowserLearnMore = document.getElementById(
'private-learn-more');
this.privateBrowserLearnMore.addEventListener('click', this);
});
}
if (isPrivateByDefault && isManuallyRegular) {
if (themeMeta) {
document.head.removeChild(themeMeta);
}
}
},
/**
* General event handler.
*/
handleEvent: function(e) {
switch(e.target) {
case this.privateBrowserDialogClose:
this.hidePrivateBrowserDialog();
break;
case this.privateBrowserLearnMore:
this.learnAboutPrivateBrowsing(e);
break;
}
},
/**
* Hides the private browser dialog.
*/
hidePrivateBrowserDialog: function() {
this.privateBrowserDialog.setAttribute('hidden', true);
// Set async storage value if we have checked the box so we don't
// attempt to show it again.
var checkbox = this.privateBrowserDialog.querySelector('gaia-checkbox');
if (checkbox.checked) {
asyncStorage.setItem('shouldSuppressPrivateDialog', true);
}
},
/**
* Opens the learn more dialog.
*/
learnAboutPrivateBrowsing: function(e) {
e.preventDefault();
/* jshint nonew: false */
new MozActivity({
name: 'view',
data: {
type: 'url',
url: e.target.href
}
});
}
};
exports.newtab = new Newtab();
/**
* Stub search object to populate providers.
* TODO: We should split up the places provider into some data layer where
* the search or newtab page could leverage it.
*/
exports.Search = {
provider: function(provider) {
exports.newtab.provider = provider;
},
/**
* Opens a browser to a URL.
* @param {String} url The url to navigate to
*/
navigate: function(url) {
new MozActivity({name:'view',data:{type:'url',url:url}});
}
};
})(window);

180
gaia/apps/search/js/places_idb.js Executable file
View file

@ -0,0 +1,180 @@
/* globals Promise, asyncStorage, IDBKeyRange */
(function(exports) {
'use strict';
var DB_NAME = 'places_idb_store';
var DB_VERSION = 3;
var PLACES_STORE = 'places';
var VISITS_STORE = 'visits';
function PlacesIdbStore() {}
PlacesIdbStore.prototype = {
db: null,
latestRevision: null,
init: function() {
var self = this;
return new Promise(function(resolve, reject) {
var req = window.indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = self.upgradeSchema;
req.onsuccess = function(e) {
self.db = e.target.result;
asyncStorage.getItem('latest-revision', function(value) {
self.latestRevision = value || 0;
resolve();
});
};
});
},
upgradeSchema: function(e) {
var db = e.target.result;
var fromVersion = e.oldVersion;
if (fromVersion < 1) {
var places = db.createObjectStore(PLACES_STORE, { keyPath: 'url' });
places.createIndex('frecency', 'frecency', { unique: false });
places.createIndex('visited', 'visited', { unique: false });
}
if (fromVersion < 2) {
asyncStorage.removeItem('latest-revision');
var visits = db.createObjectStore(VISITS_STORE, { keyPath: 'date' });
visits.createIndex('date', 'date', { unique: true });
}
if (fromVersion < 3) {
var oStore = e.target.transaction.objectStore(VISITS_STORE);
oStore.createIndex('url', 'url', { unique: false });
}
},
add: function(id, data, rev) {
return new Promise((resolve, reject) => {
var txn = this.db
.transaction([PLACES_STORE, VISITS_STORE], 'readwrite');
if (data.visits && data.visits.length === 0) {
// Removed via browsing history clearing, let's remove it.
this._removeUrl(id).then(this.saveAndResolve(rev, resolve));
} else {
txn.objectStore(PLACES_STORE).put(data);
if (!data.visits) {
data.visits = Number.isFinite(data.visited) ? [data.visited] : [];
}
var visitsStore = txn.objectStore(VISITS_STORE);
data.visits.filter(Number.isFinite).forEach(function(date) {
visitsStore.put({
date: date,
url: data.url,
title: data.title,
icons: data.icons
});
});
txn.oncomplete = this.saveAndResolve(rev, resolve);
}
});
},
addPlace: function(place) {
return new Promise((resolve, reject) => {
var txn = this.db.transaction([PLACES_STORE], 'readwrite');
txn.objectStore(PLACES_STORE).put(place);
txn.oncomplete = resolve;
});
},
remove: function(id, rev) {
return new Promise((resolve, reject) => {
this._removeUrl(id).then(this.saveAndResolve(rev, resolve));
});
},
_removeUrl: function(id) {
return Promise.all([
// Remove the url from places store.
new Promise((resolve, reject) => {
var txn = this.db.transaction([PLACES_STORE], 'readwrite');
txn.objectStore(PLACES_STORE).delete(id);
txn.oncomplete = resolve;
}),
// Clear visits from this url.
new Promise((resolve, reject) => {
var txn = this.db.transaction([VISITS_STORE], 'readwrite');
var visitsStore = txn.objectStore(VISITS_STORE);
var urlIndex = visitsStore.index('url');
var req = urlIndex.openCursor(IDBKeyRange.only(id));
req.onsuccess = function() {
var cursor = req.result;
if (cursor) {
visitsStore.delete(cursor.primaryKey);
cursor.continue();
} else {
resolve();
}
};
})
]);
},
clear: function(rev) {
return new Promise((resolve, reject) => {
var txn = this.db
.transaction([PLACES_STORE, VISITS_STORE], 'readwrite');
txn.objectStore(PLACES_STORE).clear();
txn.objectStore(VISITS_STORE).clear();
txn.oncomplete = this.saveAndResolve(rev, resolve);
});
},
saveAndResolve: function(rev, resolve) {
return function() {
asyncStorage.setItem('latest-revision', rev);
resolve();
};
},
readStore: function(store, index, limit, done, filter) {
var results = [];
var txn = this.db.transaction(store, 'readonly');
var oStore = txn.objectStore(store);
oStore.index(index).openCursor(null, 'prev').onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
if (!filter || filter(cursor.value)) {
results.push(cursor.value);
}
if (results.length < limit) {
cursor.continue();
}
}
};
txn.oncomplete = function() {
done(results);
};
},
read: function(index, limit, done, filter) {
this.readStore(PLACES_STORE, index, limit, done, filter);
},
readVisits: function(limit, done, filter) {
this.readStore(VISITS_STORE, 'date', limit, done, filter);
}
};
exports.PlacesIdbStore = PlacesIdbStore;
})(window);

View file

@ -0,0 +1,72 @@
/* globals DataGridProvider, GaiaGrid, Search,
Promise, SyncDataStore, InMemoryStore */
(function(exports) {
'use strict';
var STORE_NAME = 'bookmarks_store';
function formatBookmark(bookmark) {
return {
data: new GaiaGrid.Bookmark({
id: bookmark.url,
name: bookmark.name || bookmark.url,
url: bookmark.url,
icon: bookmark.icon
})
};
}
function Bookmarks() {}
Bookmarks.prototype = {
__proto__: DataGridProvider.prototype,
name: 'Bookmarks',
init: function() {
DataGridProvider.prototype.init.apply(this, arguments);
this.persistStore = new InMemoryStore();
this.syncStore = new SyncDataStore(STORE_NAME, this.persistStore, 'url');
return this.syncStore.sync();
},
get: function(url) {
var results = this.persistStore.results;
for (var idx in results) {
var item = results[idx];
if (item.url === url) {
return formatBookmark(item);
}
}
return null;
},
search: function(filter) {
var self = this;
return new Promise(function(resolve, reject) {
var renderResults = [];
filter = filter.toLowerCase();
for(var elem in self.persistStore.results) {
var bookmark = self.persistStore.results[elem];
var name = ('name' in bookmark) && bookmark.name.toLowerCase();
if ((!bookmark.url || bookmark.url.indexOf(filter) == -1) &&
(!name || name.indexOf(filter) == -1)) {
continue;
}
renderResults.push(formatBookmark(bookmark));
}
resolve(renderResults);
});
}
};
exports.Bookmarks = new Bookmarks();
Search.provider(exports.Bookmarks);
}(window));

View file

@ -0,0 +1,77 @@
(function() {
'use strict';
/* global MozActivity, Promise, Provider, Search */
function Contacts() {
}
Contacts.prototype = {
__proto__: Provider.prototype,
name: 'Contacts',
click: function(e) {
var target = e.target;
Search.close();
/* jshint nonew: false */
new MozActivity({
name: 'open',
data: {
type: 'webcontacts/contact',
params: {
'id': target.dataset.contactId
}
}
});
},
search: function(input) {
return new Promise((resolve, reject) => {
var options = {
filterValue: input,
filterBy: ['givenName'],
filterOp: 'startsWith'
};
this.clear();
var request = navigator.mozContacts.find(options);
request.onsuccess = (function() {
var results = request.result;
if (!results.length) {
return reject();
}
var renderResults = [];
for (var i = 0; i < results.length; i++) {
var result = results[i];
for (var j = 0; j < result.name.length; j++) {
var renderObj = {
title: result.name[j],
dataset: {
contactId: result.id
}
};
if (result.photo) {
renderObj.icon = result.photo[0];
}
renderResults.push(renderObj);
}
}
resolve(renderResults);
}).bind(this);
request.onerror = function() {
};
});
}
};
Search.provider(new Contacts());
}());

View file

@ -0,0 +1,33 @@
/* global Provider */
'use strict';
function DataGridProvider() {
}
DataGridProvider.prototype = {
__proto__: Provider.prototype,
name: 'DataGridProvider',
isGridProvider: true,
init: function() {
this.grid = document.getElementById('icons');
},
clear: function() {
this.grid.clear();
},
render: function(results) {
results.forEach(function(config, index) {
this.grid.add(config.data);
}, this);
this.grid.render({
skipDivider: true
});
}
};

View file

@ -0,0 +1,170 @@
/* global Search, DataGridProvider, GaiaGrid, Promise */
(function() {
'use strict';
function LocalApps() {
this.apps = {};
this.appListing = [];
this.blacklist;
var mozApps = navigator.mozApps.mgmt;
mozApps.oninstall = e => {
var app = e.application;
var processApp = () => {
this.apps[app.manifestURL] = app;
this.createAppListing();
};
if (app.downloading) {
app.ondownloadapplied = processApp;
} else {
processApp();
}
};
mozApps.onuninstall = e => {
delete this.apps[e.application.manifestURL];
this.createAppListing();
};
mozApps.getAll().onsuccess = e => {
e.target.result.forEach(app => {
this.apps[app.manifestURL] = app;
});
this.createBlacklist().then(() => {
this.createAppListing();
});
};
}
LocalApps.prototype = {
__proto__: DataGridProvider.prototype,
name: 'LocalApps',
dedupes: true,
dedupeStrategy: 'exact',
createBlacklist: function() {
return new Promise((resolve, reject) => {
var self = this;
var key = 'app.launch_path.blacklist';
var req = navigator.mozSettings.createLock().get(key);
req.onsuccess = function onsuccess() {
self.blacklist = req.result[key] || [];
resolve();
};
});
},
createAppListing: function() {
var appListing = [];
var blacklist = this.blacklist;
var manifestURLs = Object.keys(this.apps);
manifestURLs.forEach(function eachManifest(manifestURL) {
var app = this.apps[manifestURL];
var manifest = app.manifest || app.updateManifest;
var HIDDEN_ROLES = [
'system', 'input', 'homescreen', 'search', 'theme', 'addon',
'langpack'
];
if (HIDDEN_ROLES.indexOf(manifest.role) !== -1) {
return;
}
var entryPoints = manifest.entry_points;
if (entryPoints) {
for (var i in entryPoints) {
var entry = entryPoints[i];
entry.entryPoint = i;
entry.manifestURL = app.manifestURL;
appListing.push(entry);
}
} else {
manifest.manifestURL = app.manifestURL;
appListing.push(manifest);
}
if (blacklist) {
blacklist.forEach(function(blackentry) {
for (var i in appListing) {
var app = appListing[i];
if (app.launch_path === blackentry) {
appListing.splice(i, 1);
}
}
});
}
}, this);
this.appListing = appListing;
},
removeAppListing: function(app) {
for (var i in this.appListing) {
var manifest = this.appListing[i];
if (manifest.manifestURL === app.manifestURL) {
this.appListing.splice(i, 1);
}
}
},
search: function(input) {
return new Promise((resolve, reject) => {
var results = this.find(input);
var formatted = [];
results.forEach(function eachResult(result) {
formatted.push({
dedupeId: result.app.manifestURL,
data: new GaiaGrid.Mozapp(result.app, result.entryPoint)
});
}, this);
resolve(formatted);
});
},
/**
* Checks if an app manifest matches a query.
*/
matches: function(manifest, query) {
query = query.toLowerCase();
// Get the localized name from the query.
var shortName = manifest.short_name || '';
var userLang = document.documentElement.lang;
var locales = manifest.locales;
var localized = locales && locales[userLang] && locales[userLang].name;
localized = localized || '';
return shortName.toLowerCase().indexOf(query) != -1 ||
manifest.name.toLowerCase().indexOf(query) != -1 ||
localized.toLowerCase().indexOf(query) != -1;
},
find: function(query) {
var results = [];
this.appListing.forEach(function(manifest) {
if (this.matches(manifest, query)) {
results.push({
app: this.apps[manifest.manifestURL],
entryPoint: manifest.entryPoint
});
}
}, this);
return results;
}
};
Search.provider(new LocalApps());
}());

View file

@ -0,0 +1,85 @@
/* global Search, DataGridProvider, GaiaGrid, Promise */
(function() {
'use strict';
const NUM_DISPLAY = 4;
var apiUrl = null;
function Marketplace() {}
Marketplace.prototype = {
__proto__: DataGridProvider.prototype,
name: 'Marketplace',
dedupes: true,
dedupeStrategy: 'exact',
remote: true,
init: function() {
DataGridProvider.prototype.init.apply(this, arguments);
var urlKey = 'search.marketplace.url';
var req = navigator.mozSettings.createLock().get(urlKey);
req.onsuccess = function () {
if (req.result[urlKey]) {
apiUrl = req.result[urlKey]
.replace('{limit}', NUM_DISPLAY)
.replace('{lang}', document.documentElement.lang);
}
};
},
search: function(input) {
return new Promise((resolve, reject) => {
this.abort();
if (!input || !apiUrl) {
return resolve([]);
}
var req = new XMLHttpRequest();
req.open('GET', apiUrl.replace('{q}', encodeURIComponent(input)), true);
req.onload = (function onload() {
var results = JSON.parse(req.responseText);
if (!results.length) {
resolve([]); return;
}
var length = Math.min(NUM_DISPLAY, results.length);
var formatted = [];
for (var i = 0; i < length; i++) {
var app = results[i];
formatted.push({
dedupeId: app.manifest_url,
data: new GaiaGrid.MarketPlaceApp({
id: app.manifest_url,
name: navigator.mozL10n.get('install-marketplace-title', {
title: app.name
}),
icons: app.icons,
slug: app.slug
})
});
}
resolve(formatted);
}).bind(this);
req.onerror = function onerror() {
console.log('Marketplace error.');
reject();
};
req.ontimeout = function ontimeout() {
console.log('Marketplace timeout.');
reject();
};
req.send();
this.request = req;
});
}
};
Search.provider(new Marketplace());
}());

View file

@ -0,0 +1,400 @@
/* globals Provider, SyncDataStore, Promise, IconsHelper */
/* globals Search, PlacesIdbStore */
/* globals DateHelper */
/* globals asyncStorage */
/* globals LazyLoader */
(function(exports) {
'use strict';
var _ = navigator.mozL10n.get;
// Maximum number of results to show show for a single query
var MAX_AWESOME_RESULTS = 4;
var MAX_HISTORY_RESULTS = 20;
var MAX_TOPSITES_RESULTS = 6;
// Name of the datastore we pick up places from
var STORE_NAME = 'places';
var topSitesWrapper = document.getElementById('top-sites');
var historyWrapper = document.getElementById('history');
// These elements are only included in the newtab page.
if (topSitesWrapper && historyWrapper) {
topSitesWrapper.addEventListener('click', itemClicked);
historyWrapper.addEventListener('click', itemClicked);
}
var cachedLink = document.createElement('a');
function parseUrl(url) {
cachedLink.href = url;
return cachedLink;
}
// Storage for locally fetched icons
var icons = {};
var iconUrls = {};
function getIcon(place) {
if (place.url in icons && icons[place.url]) {
return icons[place.url];
}
IconsHelper.getIcon(place.url, null, place).then(icon => {
saveIcon(place.url, icon);
});
return false;
}
function saveIcon(key, url) {
if (key in icons) {
return;
}
icons[key] = null;
iconUrls[key] = url;
fetchIcon(url, function(err, icon) {
if (!err) {
icons[key] = icon;
}
});
}
function fetchIcon(uri, callback) {
var xhr = new XMLHttpRequest({mozSystem: true});
xhr.open('GET', uri, true);
xhr.responseType = 'blob';
xhr.addEventListener('load', function() {
// Check icon was successfully downloded
// 0 is due to https://bugzilla.mozilla.org/show_bug.cgi?id=716491
if (!(xhr.status === 200 || xhr.status === 0)) {
return callback(new Error('error_downloading'));
}
var blob = xhr.response;
if (blob.size > this.MAX_ICON_SIZE) {
return callback(new Error('image_to_large'));
}
// Only save the icon if it can be loaded as an image bigger than 0px
var img = document.createElement('img');
img.src = window.URL.createObjectURL(blob);
img.onload = function() {
window.URL.revokeObjectURL(img.src);
if (img.naturalWidth <= 0) {
return callback(new Error('Cannot load image'));
}
callback(null, blob);
};
img.onerror = function() {
window.URL.revokeObjectURL(img.src);
return callback(new Error('Cannot load image'));
};
});
xhr.onerror = function(err) {
return callback(new Error('Cannot load uri'));
};
xhr.send();
}
function itemClicked(e) {
if (e.target.dataset.url) {
Search.navigate(e.target.dataset.url);
}
}
var listTemplate = createList();
function createList() {
var list = document.createElement('ul');
list.setAttribute('role', 'listbox');
return list;
}
function incrementHistoryThreshold(timestamp, currentThreshold, thresholds) {
var newThreshold = currentThreshold += 1;
if (timestamp < thresholds[newThreshold]) {
return incrementHistoryThreshold(timestamp, newThreshold, thresholds);
}
return newThreshold;
}
function drawHistoryHeading(parent, threshold, timestamp) {
var LABELS = [
'future',
'today',
'yesterday',
'last-7-days',
'this-month',
'last-6-months',
'older-than-6-months'
];
var text = '';
// Special case for month headings
if (threshold == 5 && timestamp) {
var date = new Date(timestamp);
var now = new Date();
text = _('month-' + date.getMonth());
if (date.getFullYear() != now.getFullYear()) {
text += ' ' + date.getFullYear();
}
} else {
text = _(LABELS[threshold]);
}
var h3 = document.createElement('h3');
var spanNode = document.createElement('span');
spanNode.textContent = text;
var ul = listTemplate.cloneNode(true);
h3.appendChild(spanNode);
parent.appendChild(h3);
parent.appendChild(ul);
}
function updateIcon(visit, iconWrapper) {
var iconDom = iconWrapper.querySelector('img');
IconsHelper.getIcon(visit.url, null, visit).then((icon) => {
if (icon && iconDom) {
iconDom.onload = function () {
iconWrapper.classList.remove('empty');
iconDom.style.display = 'block';
};
iconDom.src = icon;
}
});
}
function buildHistory(visits) {
var thresholds = [
Date.now(), // 0. Now
DateHelper.todayStarted(), // 1. Today
DateHelper.yesterdayStarted(), // 2. Yesterday
DateHelper.thisWeekStarted(), // 3. This week
DateHelper.thisMonthStarted(), // 4. This month
DateHelper.lastSixMonthsStarted(), // 5. Six months
0 // 6. Epoch!
];
var threshold = 0;
var month = null;
var year = null;
var fragment = document.createDocumentFragment();
visits.forEach(function(visit) {
// Draw new heading if new threshold reached
if (visit.date > 0 && visit.date < thresholds[threshold]) {
threshold = incrementHistoryThreshold(visit.date,
threshold, thresholds);
// Special case for month headings
if (threshold != 5) {
drawHistoryHeading(fragment, threshold);
}
}
if (threshold === 5) {
var timestampDate = new Date(visit.date);
if (timestampDate.getMonth() != month ||
timestampDate.getFullYear() != year) {
month = timestampDate.getMonth();
year = timestampDate.getFullYear();
drawHistoryHeading(fragment, threshold, visit.date);
}
}
visit.meta = visit.url;
visit.dataset = { url: visit.url };
var dom = exports.Places.buildResultsDom([visit]);
var iconDom = dom.querySelector('.icon');
fragment.appendChild(dom);
updateIcon(visit, iconDom);
});
return fragment;
}
function showStartPage() {
if (!topSitesWrapper || !historyWrapper) {
return;
}
var store = exports.Places.persistStore;
var urls = [];
store.readVisits(MAX_HISTORY_RESULTS, function(results) {
var docFragment = buildHistory(results);
historyWrapper.innerHTML = '';
historyWrapper.appendChild(docFragment);
}, function filter(visit) {
var isStored = visit.url in urls;
urls[visit.url] = true;
return !isStored;
});
store.read('frecency', MAX_TOPSITES_RESULTS, function(results) {
var docFragment = document.createDocumentFragment();
results.forEach(function(x) {
docFragment.appendChild(formatTopResult(x));
});
topSitesWrapper.innerHTML = '';
topSitesWrapper.appendChild(docFragment);
});
}
function formatTopResult(result) {
var div = document.createElement('div');
var span = document.createElement('span');
if (result.title) {
span.setAttribute('dir', 'auto');
span.textContent = result.title;
} else {
span.setAttribute('dir', 'ltr');
span.textContent = result.url;
}
div.dataset.url = result.url;
div.classList.add('top-site');
div.appendChild(span);
div.setAttribute('role', 'link');
if (result.screenshot || result.tile) {
var img = result.screenshot || result.tile;
var imgUrl = (typeof img === 'string') ? img : URL.createObjectURL(img);
div.style.backgroundImage = 'url(' + imgUrl + ')';
}
return div;
}
function matchesFilter(value, filter) {
return !filter || (value && value.match(new RegExp(filter, 'i')) !== null);
}
function formatPlace(placeObj, filter) {
var result = {
'title': placeObj.title,
'meta': placeObj.url,
'icon': getIcon(placeObj),
'dataset': {
'url': placeObj.url
},
'label': placeObj.title
};
return result;
}
function Places() {}
Places.prototype = {
__proto__: Provider.prototype,
name: 'Places',
click: itemClicked,
init: function() {
// If not on new tab page, set places div as container
if (!topSitesWrapper || !historyWrapper) {
this.header = document.getElementById(this.name.toLowerCase() +
'-header');
this.container = document.getElementById(this.name.toLowerCase());
this.container.addEventListener('click', this.click.bind(this));
}
this.persistStore = new PlacesIdbStore();
this.persistStore.init().then(() => {
this.syncStore =
new SyncDataStore(STORE_NAME, this.persistStore, 'url');
this.syncStore.filter = function(place) {
return place.url.startsWith('app://') ||
place.url === 'about:blank';
};
this.syncStore.onChange = function() {
showStartPage();
};
// Make init return a promise, so we know when
// we did the sync. Used right now for testing
// porpoises.
var rev = this.persistStore.latestRevision || 0;
return this.syncStore.sync(rev).then(() => {
return new Promise(resolve => {
function done() {
showStartPage();
resolve();
}
asyncStorage.getItem('have-preloaded-sites', (havePreloaded) => {
if (!havePreloaded) {
this.preloadTopSites().then(() => {
asyncStorage.setItem('have-preloaded-sites', true);
done();
});
} else {
done();
}
});
});
});
});
},
saveSites: function(sites) {
return Promise.all(sites.map(site => {
site.frecency = -2;
return this.persistStore.addPlace(site);
}));
},
preloadTopSites: function() {
// load default build top sites
return LazyLoader.getJSON('/js/inittopsites.json').then(sites => {
return this.saveSites(sites);
});
},
search: function(filter) {
return new Promise((resolve, reject) => {
var matchedOrigins = {};
this.persistStore.read('frecency', MAX_AWESOME_RESULTS, (results) => {
resolve(results.map(function(result) {
return formatPlace(result, filter);
}));
}, function filterFun(result) {
if (result.frecency <= 0) {
return false;
}
var url = parseUrl(result.url);
var matches = !(url.hostname in matchedOrigins) &&
(matchesFilter(result.title, filter) ||
matchesFilter(result.url, filter));
if (matches) {
matchedOrigins[url.hostname] = true;
}
return matches;
});
});
}
};
exports.Places = new Places();
Search.provider(exports.Places);
}(window));

View file

@ -0,0 +1,168 @@
/* globals IconsHelper */
'use strict';
/**
* Base Provider class
*/
function Provider() {
}
Provider.prototype = {
/**
* Name of the provider
* Overridden at the child provider level
*/
name: 'Provider',
/**
* Whether or not this provider dedupes results.
*/
dedupes: false,
/**
* Initializes the provider container and adds listeners
*/
init: function(searchObj) {
this.searchObj = searchObj;
this.container = document.getElementById(this.name.toLowerCase());
this.container.addEventListener('click', this.click.bind(this));
},
/**
* Clears the rendered results of this provider from the app grid
*/
clear: function() {
this.container.innerHTML = '';
},
/**
* Handler when a result is clicked
*/
click: function() {},
/**
* Aborts any in-progress request.
*/
abort: function() {
if (this.request && this.request.abort) {
this.request.abort();
}
},
/**
* Update an icon once it has been loaded.
*
* @param {Object} config A configuration object.
* @param {Object} iconWrapper An iconWrapper DOM object.
*/
updateIcon: function(config, iconWrapper) {
var iconDom = iconWrapper.querySelector('img');
IconsHelper.getIcon(config.dataset.url, null, config).then((icon) => {
if (icon && iconDom) {
iconDom.onload = function () {
iconWrapper.classList.remove('empty');
iconDom.style.display = 'block';
};
iconDom.src = icon;
}
});
},
/**
* Renders a set of results.
* Each result may contain the following attributes:
* - title: The title of the app.
* - meta: Secondary content to show for the result.
* - icon: The icon of the result.
* - dataset: Data attributes to apply to the result.
* - label: Aria-label for the result.
* - description: Additional description for the result.
*/
buildResultsDom: function(results) {
//<div class="result" data-url="mozilla.com" role="link"
// aria-label="My Urlasljd alskdja lsdjka sldjk"
// aria-describedby="description-0">
// <div class="icon">
// <img role="presentation" src="..." />
// </div>
// <div class="urlwrapper">
// <span class="title">My Urlasljd <span class="highlight">alskd</span>
// ja lsdjka sldjk</span>
// <small id="description-0" class="url"
// aria-label="http://url.com">http://url.com</small>
// </div>
//</div>
var frag = document.createDocumentFragment();
results.forEach(function(config, index) {
var result = document.createElement('div');
var iconWrapper = document.createElement('div');
var icon = document.createElement('img');
var description = document.createElement('div');
var title = document.createElement('span');
var meta = document.createElement('small');
result.classList.add('result');
iconWrapper.classList.add('icon');
description.classList.add('description');
title.classList.add('title');
meta.classList.add('meta');
for (var i in config.dataset) {
result.dataset[i] = config.dataset[i];
}
if (config.title) {
title.setAttribute('dir', 'auto');
title.textContent = config.title;
} else {
title.setAttribute('dir', 'ltr');
title.textContent = config.url;
}
if (config.meta) {
meta.textContent = config.meta;
// Expose meta infrormation as a helpful description for each result.
if (config.description) {
meta.id = this.name + '-description-' + index;
meta.setAttribute('aria-label', config.description);
result.setAttribute('aria-describedby', meta.id);
}
}
icon.setAttribute('role', 'presentation');
if (config.icon) {
icon.src = window.URL.createObjectURL(config.icon);
} else {
iconWrapper.classList.add('empty');
}
result.setAttribute('role', 'link');
// Either use an explicit label or, if not present, title.
result.setAttribute('aria-label', config.label || config.title);
description.appendChild(title);
description.appendChild(meta);
iconWrapper.appendChild(icon);
result.appendChild(iconWrapper);
result.appendChild(description);
frag.appendChild(result);
if (!config.icon) {
this.updateIcon(config, iconWrapper);
}
}, this);
return frag;
},
render: function(results) {
var dom = this.buildResultsDom(results);
this.container.appendChild(dom);
if (this.header) {
results.length ? this.header.classList.remove('hidden') :
this.header.classList.add('hidden');
}
}
};

View file

@ -0,0 +1,140 @@
(function() {
'use strict';
/* globals Promise, Provider, Search, MetricsHelper */
/* globals LazyLoader */
/* globals SearchProvider */
var suggestionsWrapper = document.getElementById('suggestions-wrapper');
var suggestionsProvider = document.getElementById('suggestions-provider');
var suggestionsSelect = document.getElementById('suggestions-select');
function providerUpdated() {
navigator.mozL10n.setAttributes(suggestionsProvider, 'search-header', {
provider: SearchProvider('title').toUpperCase()
});
var providers = SearchProvider.providers();
var selectFragment = document.createDocumentFragment();
var optionNode = document.createElement('option');
Object.keys(providers).forEach(provider => {
var option = optionNode.cloneNode();
option.value = provider;
option.text = providers[provider].title;
if (provider === SearchProvider.selected()) {
option.selected = true;
}
selectFragment.appendChild(option);
});
suggestionsSelect.innerHTML = '';
suggestionsSelect.appendChild(selectFragment);
}
function encodeTerms(str, search) {
return str.replace('{searchTerms}', encodeURIComponent(search));
}
function Suggestions() {}
Suggestions.prototype = {
__proto__: Provider.prototype,
name: 'Suggestions',
currentSearch: null,
init: function() {
this.metrics = new MetricsHelper();
this.metrics.init();
Provider.prototype.init.apply(this, arguments);
suggestionsSelect.addEventListener('change', function(e) {
SearchProvider.setProvider(e.target.value);
});
SearchProvider.ready().then(() => {
providerUpdated();
SearchProvider.providerUpdated(() => {
providerUpdated();
this.redoSearch();
});
});
},
redoSearch: function() {
if (!this.currentSearch) {
return;
}
this.search(this.currentSearch).then(results => {
this.render(results);
});
},
click: function(e) {
this.metrics.report('websearch', SearchProvider('title'));
var suggestion = e.target.dataset.suggestion;
var url = encodeTerms(SearchProvider('searchUrl'), suggestion);
Search.navigate(url);
},
search: function(input, preventRemote) {
this.currentSearch = preventRemote ? null : input;
this.render([input]);
if (!navigator.onLine || preventRemote) {
return Promise.resolve([input]);
}
suggestionsWrapper.dataset.loading = true;
return SearchProvider.ready().then(() => {
return new Promise((resolve, reject) => {
var url = encodeTerms(SearchProvider('suggestUrl'), input);
LazyLoader.getJSON(url, true).then(result => {
var results = result[1];
// We add an item to search the entered term as well
results.unshift(input);
resolve(results);
}).catch(function() {
suggestionsWrapper.dataset.loading = false;
reject();
});
});
});
},
render: function(items) {
var ul = document.createElement('ul');
ul.setAttribute('role', 'listbox');
items.forEach(function each(item) {
var li = document.createElement('li');
li.dataset.suggestion = li.textContent = item;
li.setAttribute('role', 'option');
ul.appendChild(li);
});
suggestionsWrapper.dataset.loading = false;
this.container.innerHTML = '';
if (ul.childNodes.length) {
this.container.appendChild(ul);
}
},
clear: function() {
Provider.prototype.clear.apply(this, arguments);
this.currentSearch = null;
}
};
Search.provider(new Suggestions());
}());

398
gaia/apps/search/js/search.js Executable file
View file

@ -0,0 +1,398 @@
(function() {
'use strict';
/* global asyncStorage */
/* global Contextmenu */
/* global Search */
/* global SearchDedupe */
/* global SettingsListener */
/* global UrlHelper */
/* global SearchProvider */
/* global MetricsHelper */
/* global MozActivity */
// timeout before notifying providers
var SEARCH_DELAY = 500;
var MAX_GRID_SIZE = 4;
window.Search = {
_port: null,
providers: {},
gridCount: 0,
searchResults: document.getElementById('search-results'),
offlineMessage: document.getElementById('offline-message'),
suggestionsWrapper: document.getElementById('suggestions-wrapper'),
grid: document.getElementById('icons'),
gridWrapper: document.getElementById('icons-wrapper'),
suggestionsEnabled: false,
/**
* Used to display a notice on how to configure the search provider
* on first use
*/
suggestionNotice: document.getElementById('suggestions-notice-wrapper'),
get settingsLink() {
return document.getElementById('settings-link');
},
toShowNotice: null,
NOTICE_KEY: 'notice-shown',
init: function() {
this.dedupe = new SearchDedupe();
this.metrics = new MetricsHelper();
this.metrics.init();
// Initialize the parent port connection
var self = this;
navigator.mozApps.getSelf().onsuccess = function() {
var app = this.result;
app.connect('search-results').then(function onConnAccepted(ports) {
ports.forEach(function(port) {
self._port = port;
});
setConnectionHandler();
}, function onConnectionRejected(reason) {
console.log('Error connecting: ' + reason + '\n');
});
};
function setConnectionHandler() {
navigator.mozSetMessageHandler('connection',
function(connectionRequest) {
var keyword = connectionRequest.keyword;
var port = connectionRequest.port;
if (keyword === 'search') {
port.onmessage = self.dispatchMessage.bind(self);
port.start();
}
});
initializeProviders();
}
function initializeProviders() {
for (var i in self.providers) {
self.providers[i].init(self);
}
}
var enabledKey = 'search.suggestions.enabled';
SettingsListener.observe(enabledKey, true, function(enabled) {
this.suggestionsEnabled = enabled;
}.bind(this));
this.initNotice();
this.initConnectivityCheck();
this.contextmenu = new Contextmenu();
window.addEventListener('resize', this.resize);
window.addEventListener('scroll', this.onScroll);
navigator.mozSetMessageHandler('activity',
this.handleActivityEvents.bind(this));
},
resize: function() {
if (this.grid && this.grid.render) {
this.grid.render({
rerender: true,
skipDivider: true
});
}
},
// Typically an input keeps focus when the user scrolls, here we
// want to grab focus and manually dismiss the keyboard.
onScroll: function() {
window.focus();
},
/**
* Adds a search provider
*/
provider: function(provider) {
if (!(provider.name in this.providers)) {
this.providers[provider.name] = provider;
}
},
/**
* Dispatches messages to handlers in the Search class
*/
dispatchMessage: function(msg) {
if (typeof this[msg.data.action] === 'function') {
this[msg.data.action](msg);
}
},
/**
* Called when the user changes the search query
*/
change: function(msg) {
clearTimeout(this.changeTimeout);
this.showSearchResults();
var input = msg.data.input;
var providers = this.providers;
this.changeTimeout = setTimeout(() => {
this.clear();
this.dedupe.reset();
Object.keys(providers).forEach((providerKey) => {
var provider = providers[providerKey];
var preventRemoteFetch =
UrlHelper.isURL(input) ||
msg.data.isPrivateBrowser ||
!this.suggestionsEnabled;
if (provider.remote && preventRemoteFetch) {
return;
}
if (provider.name === 'Suggestions') {
var toShow = input.length > 2 &&
this.toShowNotice &&
this.suggestionsEnabled &&
this.suggestionNotice.hidden &&
navigator.onLine;
if (toShow) {
this.suggestionNotice.hidden = false;
}
}
provider.search(input, preventRemoteFetch).then((results) => {
this.collect(provider, results);
}, error => { if (error) console.warn('Search provider ' + provider.name + ': ' + error); });
});
}, SEARCH_DELAY);
},
/**
* Show a notice to the user informaing them of how to configure
* search providers, should only be shown once.
*/
initNotice: function() {
var confirm = document.getElementById('suggestions-notice-confirm');
confirm.addEventListener('click', this.discardNotice.bind(this, true));
var settingsLink = this.settingsLink;
if (settingsLink) {
settingsLink
.addEventListener('click', this.openSettings.bind(this));
}
asyncStorage.getItem(this.NOTICE_KEY, function(value) {
if (this.toShowNotice === null) {
this.toShowNotice = !value;
}
}.bind(this));
},
openSettings: function() {
this.discardNotice();
/* jshint nonew: false */
new MozActivity({
name: 'configure',
data: {
target: 'device',
section: 'search'
}
});
},
discardNotice: function(focus) {
this.suggestionNotice.hidden = true;
this.toShowNotice = false;
asyncStorage.setItem(this.NOTICE_KEY, true);
if (focus) {
this._port.postMessage({'action': 'focus'});
}
},
/**
* Expands the search experience when the user taps on a suggestion
* or submits a query.
*/
expandSearch: function(query) {
this.clear();
this.providers.WebResults.fullscreen(query);
},
/**
* Collects results from a search provider.
* If the provider de-duplicates results, filter through them
* and dedupe them. Otherwise, render.
* @param {Array} results The results of the provider search.
*/
collect: function(provider, results) {
if (provider.dedupes) {
results = this.dedupe.reduce(results, provider.dedupeStrategy);
}
if (provider.isGridProvider &&
(results.length + this.gridCount) > MAX_GRID_SIZE) {
var spaces = MAX_GRID_SIZE - this.gridCount;
if (spaces < 1) {
this.abort();
return;
}
results.splice(spaces, (results.length - spaces));
}
if (provider.isGridProvider) {
this.gridCount += results.length;
}
this.gridWrapper.classList.toggle('hidden', !this.gridCount);
provider.render(results);
},
/**
* Called when the user trigger a search activity
*/
handleActivityEvents: function(activity) {
var activityName = activity.source.name;
if (activityName === 'search') {
this.submit({data: {
input: activity.source.data.keyword
}});
}
},
/**
* Called when the user submits the search form
*/
submit: function(msg) {
this.discardNotice();
var input = msg.data.input;
// Not a valid URL, could be a search term
if (UrlHelper.isNotURL(input)) {
this.metrics.report('websearch', SearchProvider('title'));
var url = SearchProvider('searchUrl')
.replace('{searchTerms}', encodeURIComponent(input));
this.navigate(url);
return;
}
var hasScheme = UrlHelper.hasScheme(input);
// No scheme, prepend basic protocol and return
if (!hasScheme) {
input = 'http://' + input;
}
this.navigate(input);
},
/**
* Clear results from each provider.
*/
clear: function(msg) {
this.abort();
for (var i in this.providers) {
this.providers[i].clear();
}
this.gridCount = 0;
this.suggestionNotice.hidden = true;
},
showSearchResults: function() {
if (this.searchResults) {
this.searchResults.classList.remove('hidden');
}
},
/**
* Aborts all in-progress provider requests.
*/
abort: function() {
clearTimeout(this.changeTimeout);
for (var i in this.providers) {
this.providers[i].abort();
}
},
/**
* Messages the parent container to close
*/
close: function() {
this.abort();
this._port.postMessage({'action': 'hide'});
},
/**
* Opens a browser to a URL
* @param {String} url The url to navigate to
*/
navigate: function(url) {
new MozActivity({name:'view',data:{type:'url',url:url}});
},
/**
* Sends a message to the system app to update the input value
*/
setInput: function(input) {
this._port.postMessage({
'action': 'input',
'input': input
});
this.expandSearch(input);
},
initConnectivityCheck: function() {
var self = this;
function onConnectivityChange() {
if (navigator.onLine) {
self.searchResults.classList.remove('offline');
} else {
self.searchResults.classList.add('offline');
}
}
this.offlineMessage.addEventListener(
'click', function() {
var activity = new window.MozActivity({
name: 'configure',
data: {
target: 'device',
section: 'root',
filterBy: 'connectivity'
}
});
activity.onsuccess = function() {
/*
XXX: Since this activity inmediately returns
success, we cannot go back to the search bar.
Keeping a reference of the activity once this
is fixed.
*/
};
}
);
window.addEventListener('offline', onConnectivityChange);
window.addEventListener('online', onConnectivityChange);
onConnectivityChange();
}
};
window.addEventListener('load', Search.init.bind(Search));
})();

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

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

View file

@ -0,0 +1,48 @@
# LOCALIZATION NOTE {{title}} is name of a Marketplace Application to be installed
install-marketplace-title = Install {{ title }}
history = HISTORY
top-sites = TOP SITES
suggestions.ariaLabel = Suggestions
contacts.ariaLabel = Contacts
places.ariaLabel = Places
apps.ariaLabel = Apps
# LOCALIZATION NOTE Search and Settings are names of applications. In this case, there is a section titled 'Search' within the Settings application.
suggestions-settings-notice = Search suggestions are enabled. The characters you type when entering a search are being sent to the search engine. You can disable search suggestions in the device <span>Settings</span>.
ok = OK
add-to-home-screen.label = Add to Home Screen
offline-message = No internet connection
# History headings
future=FUTURE
today=TODAY
yesterday=YESTERDAY
last-7-days=LAST 7 DAYS
this-month=THIS MONTH
last-6-months=LAST 6 MONTHS
older-than-6-months=OLDER THAN 6 MONTHS
# Months
month-0=JANUARY
month-1=FEBRUARY
month-2=MARCH
month-3=APRIL
month-4=MAY
month-5=JUNE
month-6=JULY
month-7=AUGUST
month-8=SEPTEMBER
month-9=OCTOBER
month-10=NOVEMBER
month-11=DECEMBER
search-header={{ provider }} SEARCH
# Private Browsing
private-window-header=You’re browsing privately
private-window-info1=In private windows, {{brandShortName}} won’t keep any browser history, cookies, or temporary Internet files. However, files you download and bookmarks you make will be kept.
private-window-info2=While this device won’t have a record of your browsing history, your Internet service provider can still track the pages you visit.
private-window-dont-tell=Don’t tell me again
private-learn-more=Learn more

View file

@ -0,0 +1,84 @@
{
"name": "Browser",
"description": "Gaia System Web Browser",
"type": "certified",
"role": "search",
"launch_path": "/newtab.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"ar": {
"name": "Browser",
"description": "Gaia System Web Browser"
},
"en-US": {
"name": "Browser",
"description": "Gaia System Web Browser"
},
"fr": {
"name": "Navigateur",
"description": "Navigateur web système de Gaia"
},
"zh-TW": {
"name": "Browser",
"description": "Gaia System Web Browser"
}
},
"default_locale": "en-US",
"permissions": {
"themeable": {},
"mobileconnection": {},
"webapps-manage": {},
"open-remote-window": {},
"settings": {
"access": "readwrite"
},
"systemXHR": {},
"contacts": {
"access": "readonly"
},
"moz-extremely-unstable-and-will-change-webcomponents": {}
},
"connections": {
"search": {
"handler_path": "index.html",
"description": "Notifies the search app on query change.",
"rules": {}
}
},
"datastores-access": {
"places": {
"access": "readonly",
"description": "Stores data about browsing history."
},
"bookmarks_store": {
"access": "readonly",
"description": "Stores data about bookmarks"
}
},
"icons": {
"84": "/shared/resources/branding/firefox-browser.png",
"126": "/shared/resources/branding/firefox-browser.png",
"142": "/shared/resources/branding/firefox-browser.png",
"189": "/shared/resources/branding/firefox-browser.png",
"256": "/shared/resources/branding/firefox-browser.png",
"284": "/shared/resources/branding/firefox-browser.png"
},
"chrome": {
"navigation": true
},
"activities": {
"search": {
"filters": {
"type": "text/plain",
"keyword": {
"required": true
}
},
"disposition": "window",
"href": "/index.html"
}
}
}

51
gaia/apps/search/newtab.html Executable file
View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="theme-color" content="#fff">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0">
<link rel="stylesheet" href="style/newtab.css">
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="/shared/locales/branding/branding.{locale}.properties">
<link rel="localization" href="locales/search.{locale}.properties">
<script defer src="shared/js/l10n.js"></script>
<script defer src="shared/js/lazy_loader.js"></script>
<script defer src="shared/js/async_storage.js"></script>
<script defer src="js/places_idb.js"></script>
<script defer src="shared/js/sync_datastore.js"></script>
<script defer src="shared/js/url_helper.js"></script>
<script defer src="shared/js/utilities.js"></script>
<script defer src="shared/js/settings_listener.js"></script>
<script src="shared/js/component_utils.js"></script>
<script src="shared/elements/gaia_buttons/script.js"></script>
<script src="shared/elements/gaia_checkbox/script.js"></script>
<script defer src="js/newtab.js"></script>
<script defer src="shared/js/icons_helper.js"></script>
<script defer src="js/providers/provider.js"></script>
<script defer src="js/providers/grid_provider.js"></script>
<script defer src="js/providers/places.js"></script>
</head>
<body>
<div id="private-window-dialog" hidden>
<h1 data-l10n-id="private-window-header"></h1>
<p data-l10n-id="private-window-info1"></p>
<p data-l10n-id="private-window-info2"></p>
<p>
<a id="private-learn-more" target="_blank" href="https://support.mozilla.org/kb/private-browsing-firefox-os" data-l10n-id="private-learn-more"></a>
</p>
<gaia-checkbox class="inline">
<label data-l10n-id="private-window-dont-tell"></label>
</gaia-checkbox>
<gaia-buttons class="inline">
<button type="submit" data-l10n-id="ok" id="private-window-hide-dialog" class="full"></button>
</gaia-buttons>
</div>
<h2 id="top-sites-header"><span data-l10n-id="top-sites"></span></h2>
<section id="top-sites" aria-labelledby="top-sites-header"></section>
<section id="history" class="local"></section>
</body>
</html>

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

218
gaia/apps/search/style/newtab.css Executable file
View file

@ -0,0 +1,218 @@
/*csslint empty-rules: false*/
/* See bug 1053721 for csslint empty rule requirement. */
:root {
--primary-background-color: #fff;
--results-header-color: #a6a6a6;
--divider-color: #e5e5e5;
--history-result-title-color: #4d4d4d;
}
body.private {
--primary-background-color: #392E54;
--results-header-color: #858585;
--divider-color: #454545;
--history-result-title-color: #9e9e9e;
}
* {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
font-size: 10px;
box-sizing: border-box;
background: #fff;
overflow-x: hidden;
}
body {
padding: 0.5rem 1rem;
}
html[dir="ltr"] { text-align: left; }
html[dir="rtl"] { text-align: right; }
h2,
#history h3:first-child {
text-align: center;
border-top: 0.2rem solid var(--divider-color);
font-size: 1.4rem;
margin-top: 0.9rem;
margin-bottom: -0.9rem;
color: var(--results-header-color);
font-style: normal;
-moz-user-select: none;
}
h2 span,
#history h3:first-child span {
position: relative;
top: -0.9rem;
background: var(--primary-background-color);
padding: 0.5rem;
margin-bottom: 0;
font-weight: 500;
}
#top-sites {
overflow: auto;
margin-top: 1rem;
-moz-user-select: none;
}
#top-sites .top-site:nth-child(2n) {
margin-inline-start: 1rem;
}
.top-site {
display: block;
width: calc(50% - 0.5rem);
height: 9rem;
background-color: rgba(255, 255, 255, 0.3);
background-size: cover;
position: relative;
margin-bottom: 1rem;
border: 0.1rem solid rgba(133,133,133, 0.8);
box-sizing: border-box;
}
html[dir="ltr"] .top-site { float: left; }
html[dir="rtl"] .top-site { float: right; }
.top-site span {
box-sizing: border-box;
font-size: 1.3rem;
font-style: italic;
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-color: rgba(0, 0, 0, 0.7);
position: absolute;
bottom: 0;
padding: 0.2rem;
pointer-events: none;
color: #fff;
}
#history {
margin-top: 2.1rem;
}
#history .result {
font-size: 1.6rem;
display: flex;
flex-direction: row;
border-bottom: 0.1rem solid rgba(0, 0, 0, 0.1);
padding: 1rem 0.7rem;
overflow: hidden;
color: var(--history-result-title-color);
}
#history .result * {
pointer-events: none;
}
#history .result .description {
width: calc(100% - 1.8rem);
}
#history .icon {
margin-top: 0.3rem;
margin-bottom: 0;
margin-inline-start: 0;
margin-inline-end: 0.8rem;
display: block;
flex: 0 0 1.6rem;
width: 1.6rem;
height: 1.6rem;
}
#history .icon img {
display: none;
width: 100%;
height: 100%;
}
#history .icon.empty {
border: 0.1rem dashed #5A5A5A;
border-radius: 0.2rem;
}
#history .meta {
font-size: 1.2rem;
color: #5f5f5f;
direction: ltr;
}
#history .title, #history .meta {
display: block;
white-space:nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
section h3 {
text-align: center;
font-size: 1.4rem;
font-style: italic;
color: #858585;
font-weight: normal;
margin-top: 1rem;
-moz-user-select: none;
}
/* Pseudo-private view */
body.private {
background: var(--primary-background-color);
}
/* Private browser dialog */
#private-window-dialog {
min-height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 2;
padding: 15rem 2rem 0;
-moz-box-sizing: border-box;
background: var(--primary-background-color) url(images/pb_dialog.png) no-repeat center 2rem / 10.2rem 11.6rem;
}
#private-window-dialog[hidden] {
display: none;
}
#private-window-dialog h1 {
font-size: 2rem;
color: #fff;
text-align: center;
font-weight: 300;
font-style: italic;
margin-bottom: 1.5rem;
}
#private-window-dialog a,
#private-window-dialog p {
color: #fff;
font-size: 1.3rem;
line-height: 1.8rem;
margin-bottom: 1.2rem;
}
#private-window-dialog a {
color: #00caf2;
}
#private-window-dialog gaia-checkbox {
margin: 10px 0;
}
#private-window-dialog gaia-checkbox label {
color: #fff;
font-size: 1.3rem;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

321
gaia/apps/search/style/search.css Executable file
View file

@ -0,0 +1,321 @@
/* Only include gaia-theme to get CSS variables, the transition-duration one
in particular. Other parts of gaia-theme are not used. */
@import url('../shared/elements/gaia-theme/gaia-theme.css');
/**
* Disable selection by default except for elements with
* "contenteditable".
*/
* {
margin: 0;
padding: 0;
-moz-user-select: none;
}
html[dir="ltr"] { text-align: left; }
html[dir="rtl"] { text-align: right; }
[contenteditable="true"] {
-moz-user-select: text;
}
html, body {
height: 100%;
font-size: 10px;
}
body, a {
color: rgba(255, 255, 255, 0.9);
}
#offline-message {
font-size: 1.4rem;
font-style: italic;
margin: 0;
margin-inline-end: 1rem;
height: 4.2rem;
padding-bottom: 1.3rem;
text-align: center;
display: none;
justify-content: center;
align-items: center;
}
#settings-connectivity {
background: url(/style/icons/settings.png) no-repeat 50% 50% / 1.4rem;
width: 1.4rem;
height: 1.4rem;
margin-top: -0.1rem;
padding-inline-end: 0.8rem;
}
#search-results {
padding-top: 1.3rem;
}
#search-results.offline #suggestions {
display: none;
}
#search-results.offline #offline-message {
display: flex;
}
#suggestions-provider {
color: #00aac5;
position: relative;
padding: 0;
padding-inline-end: 2rem;
padding-inline-start: 1rem;
}
#suggestions-provider-dropdown {
content: ' ';
position: absolute;
top: -.4rem;
margin-inline-start: -1.4rem;
width: 0;
height: 0;
background: none;
padding: 0;
border-style: solid;
border-color: transparent transparent #00aac5 transparent;
}
html[dir="ltr"] #suggestions-provider-dropdown {
border-width: 0 0 .8rem .8rem;
}
html[dir="rtl"] #suggestions-provider-dropdown {
border-width: 0 .8rem .8rem 0;
}
#suggestions {
font-size: 1.6rem;
overflow-x: hidden;
margin-inline-end: 1.6rem;
}
#suggestions ul {
list-style: none;
white-space: nowrap;
padding-inline-start: 0;
}
#suggestions li {
height: 6rem;
line-height: 6rem;
margin: 0;
border-bottom: 0.1rem solid rgba(255, 255, 255, 0.05);
background: url(/style/icons/search.png) no-repeat;
background-size: 3rem;
padding-inline-start: 4.5rem;
width: 100%;
unicode-bidi: -moz-plaintext;
}
html[dir="ltr"] #suggestions li { background-position: left 0.8rem center; }
html[dir="rtl"] #suggestions li { background-position: right 0.8rem center; }
section .result * {
pointer-events: none;
}
h2 {
position: relative;
text-align: center;
border-top: 0.3rem solid #5f5f5f;
font-size: 1.4rem;
color: #858585;
margin-top: 0.9rem;
margin-bottom: -0.9rem;
margin-inline-start: 0.5rem;
margin-inline-end: 0.6rem;
}
/* The select box is hidden behind search provider label since we
only want to see the dialog prompt, not the actual select box */
#suggestions-select {
position: absolute;
top: 0;
opacity: 0;
}
h2 span, h2 label {
position: relative;
top: -0.9rem;
background: #333;
padding: 0 1rem;
margin-bottom: 0;
font-weight: 500;
}
#places-header {
margin-top: 0.9rem;
}
#places-header.hidden {
display: none;
}
.local .result {
display: flex;
flex-direction: row;
padding: 0.8rem 1.5rem;
position: relative;
}
.local .result:after {
position: absolute;
bottom: 0;
content: " ";
width: calc(100% - 1rem);
height: 0.1rem;
background: #535353;
left: 0.5rem;
right: 0.5rem;
}
.local .result:last-child:after {
display: none;
}
.local .result:active {
background-color: #3e666e;
}
.local .icon {
margin: 0;
margin-top: 0.4rem;
margin-inline-end: 1.6rem;
display: block;
flex: 0 0 32px;
width: 32px;
height: 32px;
}
.local .icon img {
width: 32px;
height: 32px;
}
.local .icon.empty {
border: 1px dashed #5A5A5A;
border-radius: 2px;
}
.local .icon.empty img {
opacity: 0;
}
.local .title, .local .meta {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.local .description {
overflow: hidden;
}
.local .title {
color: #e7e7e7;
font-size: 1.5rem;
}
.local .meta {
color: #858585;
font-size: 1.4rem;
}
.local .highlight {
font-weight: bold;
text-decoration: underline;
}
/* web */
section.apps {
/* Hide the whitespace between inline elements */
font-size: 0;
height: 10rem;
overflow: hidden;
padding-bottom: 1.5rem;
}
gaia-grid {
display: block;
height: 100%;
width: 100%;
text-align: left; /* workaround the fact gaia-grid has a direction: 'ltr' */
}
.hidden {
display: none;
}
#suggestions-notice-wrapper {
background: rgba(45, 45, 45, 0.94);
z-index: 100;
border-radius: 2px;
position: absolute;
margin: 1rem 1.5rem 0;
}
#suggestions-notice-wrapper p {
padding: 1.5rem;
font-size: 1.6rem;
font-style: italic;
}
#suggestions-notice-confirm {
display: block;
width: 100%;
color: #00AACC;
background: none;
border: none;
border-top: 0.1rem solid rgba(255, 255, 255, 0.05);
border-radius: 0;
padding: 1rem;
font-size: 16px;
font-style: italic;
}
#settings-link {
color: #00AACC;
}
#suggestions-notice-wrapper::before {
content: ' ';
height: 0;
width: 0;
position: absolute;
border-style: solid;
border-width: 0 1rem 1rem;
border-color: transparent transparent rgba(45, 45, 45, 0.94) transparent;
top: -1rem;
margin-inline-start: calc(50% - 1rem);
z-index: -1;
}
/* show up to 2-line labels in result icons */
#search-results .icon .title {
text-shadow: none !important;
height: calc(3.88rem + 0.2rem);
white-space: normal !important;
}
.loading {
display: none;
padding: 2.5rem 0;
text-align: center;
width: 100%;
}
[data-loading=true] .loading {
display: block;
}
#places .meta {
direction: ltr;
}
html[dir="ltr"] #places .meta { text-align: left; }
html[dir="rtl"] #places .meta { text-align: right; }

View file

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

View file

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

View file

@ -0,0 +1,74 @@
'use strict';
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var Rocketbar =
require('../../../system/test/marionette/lib/rocketbar.js');
var createAppServer =
require('../../../homescreen/test/marionette/server/parent');
marionette('Search - Installed Apps Test', function() {
var clientOptions = require(__dirname + '/client_options.js');
var client = marionette.client({
profile: clientOptions,
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var appInstall, home, rocketbar, search, server, system;
setup(function(done) {
appInstall = new AppInstall(client);
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
var app = __dirname + '/fixtures/installed_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
test('app result after install/uninstall', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
rocketbar.enterText('Calendar');
search.goToResults();
// Should only have the 'stock' calendar app.
client.waitFor(function() {
return search.countGridResults() === 1;
});
// Exit rocketbar and install the app.
client.switchToFrame();
system.tapHome();
appInstall.installPackage(server.packageManifestURL);
// Search again for the same term.
rocketbar.appTitleFocus();
rocketbar.enterText('Calendar');
search.goToResults();
// Should now contain the newly installed app as well.
client.waitFor(function() {
return search.countGridResults() === 2;
});
// Remove the installed app.
client.switchToFrame();
system.tapHome();
appInstall.uninstall(server.packageManifestURL);
home.confirmDialog('remove');
// Should now contain only a single app.
rocketbar.appTitleFocus();
rocketbar.enterText('Calendar');
search.goToResults();
client.waitFor(function() {
return search.countGridResults() === 1;
});
});
});

View file

@ -0,0 +1,48 @@
'use strict';
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
marionette('Search - App search', function() {
var profile = require(__dirname + '/client_options.js');
// Causing a crash in this test, so disable apz.
profile.prefs = {
'layers.async-pan-zoom.enabled': false
};
var client = marionette.client({
profile: profile,
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, search, rocketbar, system;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
});
test('Search apps from Rocketbar', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
rocketbar.enterText('calendar');
search.goToResults();
var calendarIdentifier = 'app://calendar.gaiamobile.org/manifest.webapp';
var result = search.checkResult(calendarIdentifier, 'Calendar');
result.tap();
search.goToApp('app://calendar.gaiamobile.org');
});
test('Search for app with entry point', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
rocketbar.enterText('Phone');
search.goToResults();
var phoneIdentifier =
'app://communications.gaiamobile.org/manifest.webapp-dialer';
var result = search.checkResult(phoneIdentifier, 'Phone');
result.tap();
search.goToApp('app://communications.gaiamobile.org', 'dialer');
});
});

View file

@ -0,0 +1,95 @@
'use strict';
var Server = require('../../../../shared/test/integration/server');
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
var assert = require('chai').assert;
marionette('Browser test', function() {
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var search, system, server, home, rocketbar;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
setup(function() {
home = client.loader.getAppClass('verticalhome');
search = client.loader.getAppClass('search');
system = client.loader.getAppClass('system');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
});
test('Test title injecting html', function() {
var url = server.url('xsstitle.html');
rocketbar.goToURL(url);
client.helper.wait(1500);
system.closeBrowserByUrl(url);
// Open browser app.
client.apps.launch(search.URL);
client.apps.switchToApp(search.URL);
client.waitFor(function() {
return search.getHistoryResults().length === 1;
});
var title = client.executeScript(function() {
return document.querySelector('#history .title').innerHTML;
});
assert.equal(title, '&lt;em&gt;test&lt;/em&gt;');
});
test('Large Icon', function() {
var url = server.url('largeicon.html');
rocketbar.goToURL(url);
client.helper.wait(500);
system.closeBrowserByUrl(url);
// Open browser app.
client.apps.launch(search.URL);
client.apps.switchToApp(search.URL);
client.waitFor(function() {
return search.getHistoryResults().length === 1;
});
var width = client.executeScript(function() {
var icon = document.querySelector('#history .icon');
return icon.clientWidth;
});
assert.equal(width, 16);
});
test('Ensure fallback to url when no place title', function() {
var url = server.url('notitle.html');
rocketbar.goToURL(url);
client.helper.wait(500);
system.closeBrowserByUrl(url);
// Open browser app.
client.apps.launch(search.URL);
client.apps.switchToApp(search.URL);
client.waitFor(function() {
return search.getTopSites().length === 1;
});
var topSite = search.getTopSites()[0];
assert.equal(topSite.text(), url);
});
});

View file

@ -0,0 +1,74 @@
'use strict';
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
var Server = require('../../../../shared/test/integration/server');
marionette('Search - Switch Providers Test', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, search, rocketbar, system, server;
var providers;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
providers = {
version: search.searchDataVersion(),
providers: {
'first': {
title: 'first',
searchUrl: server.url('sample.html'),
suggestUrl: server.url('suggestions_one.json')
},
'second': {
title: 'second',
searchUrl: server.url('sample.html'),
suggestUrl: server.url('suggestions_two.json')
}
}
};
client.settings.set('search.suggestions.enabled', true);
client.settings.set('search.cache', providers);
client.settings.set('search.provider', 'first');
});
test('Test switching suggestion provider', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
search.triggerFirstRun(rocketbar);
rocketbar.enterText('sometext');
search.goToResults();
client.waitFor(function() {
return client.findElements(search.Selectors.suggestions).length === 2;
});
var select = search.switchProvidersSelect;
client.helper.tapSelectOption(select, 'second');
client.switchToFrame();
search.goToResults();
client.waitFor(function() {
return client.findElements(search.Selectors.suggestions).length === 3;
});
});
});

View file

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

View file

@ -0,0 +1,43 @@
'use strict';
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
var Contacts = require(
'../../../communications/contacts/test/marionette/lib/contacts');
var assert = require('assert');
marionette('Search - Contact search', function() {
var client = marionette.client();
var contacts, search, rocketbar;
setup(function() {
rocketbar = new Rocketbar(client);
contacts = new Contacts(client);
search = client.loader.getAppClass('search');
});
test.skip('able to search contact from rocketbar', function() {
var contactTel = 1231231234;
contacts.launch();
contacts.addContact({
givenName: 'HelloWorld',
tel: contactTel
});
client.apps.close(Contacts.URL, 'contacts');
client.switchToFrame();
search.doSearch('HelloWorld');
search.goToResults();
search.checkResult('firstContact', 'HelloWorld');
search.goToApp(Contacts.URL, 'contacts');
var firstTel = client.helper
.waitForElement(Contacts.Selectors.detailsTelButtonFirst);
assert.equal(firstTel.text(), contactTel);
});
});

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="shortcut icon"
href="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
<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,15 @@
{
"name": "Calendar2",
"description": "Just a dummy app for testing what happens when we install something.",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {},
"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 lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<header><h1>Mozilla</h1></header>
<section>We are global community with a common purpose</section>
</body>
</html>

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 @@
["foo", ["bar"]]

View file

@ -0,0 +1 @@
["foo", ["bar", "baz"]]

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><em>test</em></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,39 @@
'use strict';
var assert = require('assert');
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
marionette('Search - Home Button Press', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, search, rocketbar, system;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
});
test('Home button returns to home screen', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
rocketbar.enterText('calendar');
// Ensure search results are displayed.
search.goToResults();
var calendarIdentifier = 'app://calendar.gaiamobile.org/manifest.webapp';
search.checkResult(calendarIdentifier, 'Calendar');
// Emulate the home button and ensure the home screen is displayed.
client.switchToFrame();
assert.ok(client.findElement(search.Selectors.iframe).displayed());
system.tapHome();
client.waitFor(function(){
return !client.findElement(search.Selectors.iframe).displayed();
});
});
});

View file

@ -0,0 +1,164 @@
'use strict';
/* global module */
var assert = require('assert');
var PROVIDERS_VERSION = 3;
/**
* Abstraction around search app.
* @constructor
*/
function Search(client) {
this.client = client;
}
/**
* @type String Origin of search app
*/
Search.URL = 'app://search.gaiamobile.org';
Search.Selectors = {
allGridResults: 'gaia-grid .icon',
iframe: 'iframe[mozapptype="search"]',
firstAppContainer: 'gaia-grid',
firstApp: 'gaia-grid .icon',
firstContact: '#contacts div',
firstContactContainer: '#contacts',
firstPlace: '#places div .title',
firstPlaceContainer: '#places',
firstRunConfirm: '#suggestions-notice-confirm',
privateWindow: '#private-window',
topSites: '.top-site',
topSitesHeader: '#top-sites-header',
historyResults: '#history .result',
suggestions: '#suggestions li',
switchProviders: '#suggestions-select',
privateDialog: '#private-window-dialog',
privateDialogClose: '#private-window-hide-dialog',
privateDialogCheckbox: '#private-window-dialog gaia-checkbox'
};
Search.prototype = {
URL: Search.URL,
NEW_TAB_URL: Search.URL + '/newtab.html',
Selectors: Search.Selectors,
privateBrowserUrl: 'app://search.gaiamobile.org/newtab.html?private=1',
nonPrivateBrowserUrl: 'app://search.gaiamobile.org/newtab.html?private=0',
/**
* Navigates to the search results frame.
*/
goToResults: function() {
var frameSelector = Search.Selectors.iframe;
var iframe = this.client.helper.waitForElement(frameSelector);
this.client.helper.waitForElement(iframe);
this.client.switchToFrame(iframe);
this.client.helper.waitForElement('body');
},
/**
* Return selector for the grid item with a n identifier
*/
getResultSelector: function(identifier) {
return '.icon[data-identifier="' + identifier + '"]';
},
/**
* Return selector for the history list item by URL
*/
getHistoryResultSelector: function(url) {
return '.result[data-url="' + url + '"]';
},
/**
* Return grid results for a particular identifier
*/
getResult: function(identifier) {
var selector = '.icon[data-identifier="' + identifier + '"]';
return this.client.findElements(selector);
},
getHistoryResults: function() {
return this.client.findElements(Search.Selectors.historyResults);
},
getTopSites: function() {
return this.client.findElements(Search.Selectors.topSites);
},
/**
* Checks that we have a result for a given app in the results list.
*/
checkResult: function(identifier, expected) {
var selectors = Search.Selectors;
var selector = '.icon[data-identifier*="' + identifier + '"]';
this.client.helper.waitForElement(selectors.firstAppContainer);
var result = this.client.helper.waitForElement(selector);
assert.equal(expected, result.text());
return result;
},
/**
* Counts all grid search results.
*/
countGridResults: function() {
return this.client.findElements(Search.Selectors.allGridResults).length;
},
/**
* On first run a warning is shown to users on Search app configuration
* trigger this notice and confirm it.
*/
triggerFirstRun: function(rocketbar) {
rocketbar.enterText('a');
this.goToResults();
this.client.executeScript(function() {
window.wrappedJSObject.Search.toShowNotice = false;
});
this.client.switchToFrame();
rocketbar.enterText('');
},
/**
* Navigates to a specific app/entry point
* Waits for the body to be loaded
*/
goToApp: function() {
this.client.switchToFrame();
this.client.apps.switchToApp.apply(this.client.apps, arguments);
this.client.helper.waitForElement('body');
},
searchDataVersion: function() {
return PROVIDERS_VERSION;
},
/**
* Gets a reference to the provider select using findElement.
* This waits for the element to be available, but not visible on the apge.
*/
get switchProvidersSelect() {
// Fail finding elements quickly.
var quickly = this.client.scope({
searchTimeout: 20
});
var element;
try {
element = quickly.findElement(Search.Selectors.switchProviders);
} catch(e) {
return this.switchProvidersSelect;
}
return element;
},
};
module.exports = Search;

View file

@ -0,0 +1,86 @@
'use strict';
var assert = require('assert');
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
var Server = require('../../../../shared/test/integration/server');
marionette('Search - Notice Test', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, search, rocketbar, system, server;
var providers;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
providers = {
version: search.searchDataVersion(),
providers: {
'first': {
title: 'first',
searchUrl: server.url('sample.html'),
suggestUrl: server.url('suggestions_one.json')
}
}
};
client.settings.set('search.suggestions.enabled', true);
client.settings.set('search.cache', providers);
client.settings.set('search.provider', 'first');
});
test('Search Notice', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
var confirmSelector = search.Selectors.firstRunConfirm;
// Notice should not be displayed if we type < 3 characters
rocketbar.enterText('ab');
rocketbar.enterText('cd');
search.goToResults();
assert.ok(!client.findElement(confirmSelector).displayed());
// Should not show notice if suggestions are disabled
client.settings.set('search.suggestions.enabled', false);
client.switchToFrame();
rocketbar.enterText('abc');
search.goToResults();
client.helper.waitForElementToDisappear(confirmSelector);
// Notice should be displayed if we show > 3 characters
client.settings.set('search.suggestions.enabled', true);
client.switchToFrame();
rocketbar.enterText('abc');
search.goToResults();
client.helper.waitForElement(confirmSelector);
// Notice should be dismissed if we press enter
client.switchToFrame();
var searchText = 'abc';
rocketbar.enterText(searchText, true);
rocketbar.switchToSearchFrame(server.url('sample.html'), searchText);
client.switchToFrame();
home.pressHomeButton();
rocketbar.homescreenFocus();
search.goToResults();
assert.ok(!client.findElement(confirmSelector).displayed());
});
});

View file

@ -0,0 +1,130 @@
'use strict';
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
var Server = require('../../../../shared/test/integration/server');
var assert = require('assert');
marionette('Places tests', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, search, server, rocketbar, system;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
suiteTeardown(function() {
server.stop();
});
setup(function() {
home = client.loader.getAppClass('verticalhome');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
});
test('Test url searching', function() {
var url = server.url('sample.html');
var url2 = server.url('favicon.html');
// Lauch the rocketbar and trigger its first run notice
home.waitForLaunch();
home.focusRocketBar();
search.triggerFirstRun(rocketbar);
// Input a url and press enter to visit
rocketbar.enterText(url + '\uE006');
rocketbar.switchToBrowserFrame(url);
// Go home
client.switchToFrame();
home.pressHomeButton();
client.apps.switchToApp(home.URL);
// Redo search for url
home.focusRocketBar();
rocketbar.enterText(url);
search.goToResults();
var id = search.getHistoryResultSelector(url);
var result = client.helper.waitForElement(id);
// Click result and check app loads
result.click();
client.switchToFrame();
rocketbar.switchToBrowserFrame(url);
// Go home
client.switchToFrame();
home.pressHomeButton();
client.apps.switchToApp(home.URL);
// Input a different url and press enter to visit
home.focusRocketBar();
rocketbar.enterText(url2 + '\uE006');
rocketbar.switchToBrowserFrame(url2);
});
test.skip('Search for a string that doesnt match visited url', function() {
var url = server.url('sample.html');
search.triggerFirstRun(rocketbar);
rocketbar.focus();
rocketbar.enterText(url + '\uE006');
rocketbar.waitForBrowserFrame();
client.switchToFrame();
rocketbar.focus();
rocketbar.enterText('non_matching_string');
search.goToResults();
assert.equal(client.findElements(search.Selectors.firstPlace).length, 0);
});
test.skip('Ensures urls visited twice only show in results once', function() {
var url = server.url('sample.html');
search.triggerFirstRun(rocketbar);
rocketbar.focus();
rocketbar.enterText(url + '\uE006');
rocketbar.waitForBrowserFrame();
rocketbar.focus();
rocketbar.enterText(url + '\uE006');
rocketbar.waitForBrowserFrame();
client.switchToFrame();
rocketbar.focus();
rocketbar.enterText(url);
search.goToResults();
// Wait to get the correct amount of results
client.waitFor(function() {
return client.findElements(search.Selectors.firstPlace).length === 1;
}.bind(this));
// Wait for a second and check we dont get extra results
client.helper.wait(1000);
assert.equal(client.findElements(search.Selectors.firstPlace).length, 1);
});
test.skip('Ensure favicon is loaded', function() {
var url = server.url('favicon.html');
search.triggerFirstRun(rocketbar);
rocketbar.focus();
rocketbar.enterText(url + '\uE006');
rocketbar.waitForBrowserFrame();
client.waitFor(function() {
client.switchToFrame();
rocketbar.focus();
rocketbar.enterText(url);
search.goToResults();
var result = client.helper.waitForElement('#places div .icon');
return !result.getAttribute('class').match('empty');
});
});
});

View file

@ -0,0 +1,64 @@
'use strict';
var assert = require('assert');
marionette('Private Browser Trigger', function() {
var client = marionette.client({
profile: {
settings: {
'browser.private.default': true
}
}
});
var search, system;
setup(function() {
search = client.loader.getAppClass('search');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
});
function launchPrivateWindow() {
var frame = system.waitForLaunch(search.URL);
client.switchToFrame(frame);
}
test('launches private window', function() {
// Assert that the dialog is visible and is dismissable.
launchPrivateWindow();
var dialog = client.helper.waitForElement(search.Selectors.privateDialog);
client.helper.waitForElement(search.Selectors.privateDialogClose)
.scriptWith(function(el) {
el.wrappedJSObject.dispatchEvent(new CustomEvent('click'));
});
client.helper.waitForElementToDisappear(dialog);
client.executeScript(function() {
window.wrappedJSObject.close();
});
client.switchToFrame();
// Assert that the dialog is visible if we relaunch the search app.
launchPrivateWindow();
dialog = client.helper.waitForElement(search.Selectors.privateDialog);
// Tap the checkbox and close the dialog.
client.helper.waitForElement(search.Selectors.privateDialogCheckbox)
.scriptWith(function(el) {
el.wrappedJSObject.checked = true;
});
client.helper.waitForElement(search.Selectors.privateDialogClose)
.scriptWith(function(el) {
el.wrappedJSObject.dispatchEvent(new CustomEvent('click'));
});
client.helper.waitForElementToDisappear(dialog);
client.executeScript(function() {
window.wrappedJSObject.close();
});
client.switchToFrame();
// The next time we re-launch the browser the dialog should not appear.
launchPrivateWindow();
assert.ok(client.helper.waitForElement(
search.Selectors.topSitesHeader).displayed());
});
});

View file

@ -0,0 +1,48 @@
'use strict';
marionette('Private Browsing', function() {
var client = marionette.client({
profile: {
settings: {
'browser.private.default': true
}
}
});
var search, system;
setup(function() {
search = client.loader.getAppClass('search');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
});
test('launches private window', function() {
client.executeScript(function () {
window.dispatchEvent(new CustomEvent('new-private-window'));
});
client.helper.waitForElement('body');
var iframe = system.getAppIframe(search.privateBrowserUrl);
client.waitFor(function() {
return iframe.getAttribute('src') === search.privateBrowserUrl;
});
});
test('launches non-private window', function() {
client.executeScript(function () {
window.dispatchEvent(new CustomEvent('new-non-private-window'));
});
client.helper.waitForElement('body');
var iframe = system.getAppIframe(search.nonPrivateBrowserUrl);
client.waitFor(function() {
return iframe.getAttribute('src') === search.nonPrivateBrowserUrl;
});
});
});

View file

@ -0,0 +1,126 @@
'use strict';
var assert = require('assert');
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
var Server = require('../../../../shared/test/integration/server');
marionette('Search - Rocketbar Test', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, search, rocketbar, system, server;
var providers, searchTemplate;
var phoneIdentifier =
'app://communications.gaiamobile.org/manifest.webapp-dialer';
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
searchTemplate = server.url('sample.html') + '?q=';
providers = {
version: search.searchDataVersion(),
providers: {
'first': {
title: 'first',
searchUrl: searchTemplate + '{searchTerms}',
suggestUrl: server.url('suggestions_one.json')
}
}
};
mockSearch();
home.waitForLaunch();
rocketbar.homescreenFocus();
});
function mockSearch() {
client.settings.set('search.suggestions.enabled', true);
client.settings.set('search.cache', providers);
client.settings.set('search.provider', 'first');
}
function assertThatUrlContains(text) {
rocketbar.appTitleFocus();
client.waitFor(function() {
var value = rocketbar.input.getAttribute('value');
var searchText = text.replace(' ', '%20');
return value === (searchTemplate + searchText);
});
}
test('General walkthrough', function() {
search.triggerFirstRun(rocketbar);
var textToSearch = 'a test';
// Clear button shouldnt be visible when no text entered
assert.ok(!rocketbar.clear.displayed());
// Search for an app ane make sure it exists
rocketbar.enterText('Phone');
search.goToResults();
search.checkResult(phoneIdentifier, 'Phone');
// Press rocketbar close button, ensure the homescreen is
// now displayed
client.switchToFrame();
rocketbar.cancel.click();
client.apps.switchToApp(home.URL);
client.waitFor(function() {
return home.visibleIcons.length && home.visibleIcons[0].displayed();
});
// When we previously pressed close, when rocketbar reopens value
// should be empty
rocketbar.homescreenFocus();
assert.equal(rocketbar.input.getAttribute('value'), '');
// Search for an app again, this time press close after searching
rocketbar.enterText('Phone');
home.pressHomeButton();
client.waitFor(function() {
return home.visibleIcons.length && home.visibleIcons[0].displayed();
});
// If we press home button during a search, next time we focus the rocketbar
// previous result should be displayed
rocketbar.homescreenFocus();
search.goToResults();
search.checkResult(phoneIdentifier, 'Phone');
// Clear button should be visible when text entered
client.switchToFrame();
assert.ok(rocketbar.clear.displayed());
// Press clear button, input
rocketbar.clear.click();
assert.equal(rocketbar.input.getAttribute('value'), '');
assert.ok(!rocketbar.clear.displayed());
// Perform a search
rocketbar.enterText(textToSearch, true);
rocketbar.switchToBrowserFrame(searchTemplate);
});
test('Browser search', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
var textToSearch = 'a test';
rocketbar.performSearchInBrowser(textToSearch);
assertThatUrlContains(textToSearch);
});
});

View file

@ -0,0 +1,74 @@
'use strict';
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
var Server = require('../../../../shared/test/integration/server');
marionette('Search - Suggestions disabled', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, search, rocketbar, system, server;
var providers;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
providers = {
version: search.searchDataVersion(),
providers: {
'first': {
title: 'first',
searchUrl: server.url('sample.html'),
suggestUrl: server.url('suggestions_one.json')
},
'second': {
title: 'second',
searchUrl: server.url('sample.html'),
suggestUrl: server.url('suggestions_two.json')
}
}
};
client.settings.set('search.suggestions.enabled', false);
client.settings.set('search.cache', providers);
client.settings.set('search.provider', 'first');
});
test('Test switching suggestion provider', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
search.triggerFirstRun(rocketbar);
rocketbar.enterText('sometext');
search.goToResults();
client.waitFor(function() {
return client.findElements(search.Selectors.suggestions).length === 1;
});
var select = search.switchProvidersSelect;
client.helper.tapSelectOption(select, 'second');
client.switchToFrame();
search.goToResults();
client.waitFor(function() {
return client.findElements(search.Selectors.suggestions).length === 1;
});
});
});

View file

@ -0,0 +1,68 @@
'use strict';
var Rocketbar = require('../../../system/test/marionette/lib/rocketbar.js');
var Server = require('../../../../shared/test/integration/server');
marionette('Search - Suggestions Test', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var home, search, rocketbar, system, server;
var providers;
suiteSetup(function(done) {
Server.create(__dirname + '/fixtures/', function(err, _server) {
server = _server;
done();
});
});
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
search = client.loader.getAppClass('search');
rocketbar = new Rocketbar(client);
system.waitForFullyLoaded();
providers = {
version: search.searchDataVersion(),
providers: {
'first': {
title: 'first',
searchUrl: server.url('sample.html'),
suggestUrl: server.url('suggestions_one.json')
}
}
};
client.settings.set('search.suggestions.enabled', true);
client.settings.set('search.cache', providers);
client.settings.set('search.provider', 'first');
});
test('Test suggestions', function() {
home.waitForLaunch();
rocketbar.homescreenFocus();
search.triggerFirstRun(rocketbar);
rocketbar.enterText('sometext');
search.goToResults();
// Ensure we get 2 results (hardcoded in the provider results)
client.waitFor(function() {
return client.findElements(search.Selectors.suggestions).length === 2;
});
// Ensure clicking on a result opens the browser correctly
var first = client.helper.waitForElement('#suggestions li');
first.click();
client.switchToFrame();
rocketbar.switchToBrowserFrame(providers.providers.first.searchUrl);
});
});

View file

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

View file

@ -0,0 +1,80 @@
'use strict';
require('/js/contextmenu.js');
suite('search/contextmenu', function() {
var subject = null,
realBookmarks = null,
isBookmarked = false;
suiteSetup(function() {
realBookmarks = window.Bookmarks;
window.Bookmarks = {
get: function() {
return isBookmarked;
}
};
loadBodyHTML('/index.html');
});
suiteTeardown(function() {
window.Bookmarks = realBookmarks;
});
setup(function() {
this.sinon.useFakeTimers();
subject = new window.Contextmenu();
});
suite('handleEvent', function() {
test('contextmenu icon not bookmarked', function() {
var target = document.createElement('div');
target.getIcon = function() {
return {
detail: {
type: 'bookmark'
}
};
};
target.start = function() {};
target.stop = function() {};
subject.grid = target;
var fakeContextMenuEvent = {
type: 'contextmenu',
target: target
};
var startStub = this.sinon.stub(subject.grid, 'start');
var stopStub = this.sinon.stub(subject.grid, 'stop');
subject.handleEvent(fakeContextMenuEvent);
assert.ok(stopStub.calledOnce);
this.sinon.clock.tick();
assert.ok(startStub.calledOnce);
});
test('contextmenu icon already bookmarked', function() {
var target = document.createElement('div');
target.getIcon = function() {
return {
detail: {
type: 'bookmark'
}
};
};
target.stop = function() {};
subject.grid = target;
var fakeContextMenuEvent = {
type: 'contextmenu',
target: target
};
isBookmarked = true;
var stopStub = this.sinon.stub(subject.grid, 'stop');
subject.handleEvent(fakeContextMenuEvent);
assert.isFalse(stopStub.called);
});
});
});

View file

@ -0,0 +1,13 @@
'use strict';
/* global Promise */
var MockProvider = function(name) {
this.name = name;
};
MockProvider.prototype.init = function() {};
MockProvider.prototype.clear = function() {};
MockProvider.prototype.search = function() {
return new Promise(() => {});
};
MockProvider.prototype.abort = function() {};

View file

@ -0,0 +1,17 @@
'use strict';
window.Search = {
_port: null,
providers: {},
init: function() {},
provider: function(provider) {
this.providers[provider.name] = provider;
},
onSearchInput: function() {},
close: function() {},
navigate: function() {},
setInput: function() {},
collect: function(provider, results) {
provider.render(results);
}
};

View file

@ -0,0 +1,169 @@
/* globals MocksHelper */
/* globals asyncStorage */
/* globals PlacesIdbStore */
'use strict';
require('/shared/test/unit/mocks/mock_async_storage.js');
requireApp('search/js/places_idb.js');
var mocksHelper = new MocksHelper([
'asyncStorage'
]).init();
suite('search/places_idb', function() {
var subject;
var mozilla = 'http://mozilla.org';
var google = 'http://google.com';
var yahoo = 'http://yahoo.com';
function place(url, frecency, visited, visits) {
frecency = frecency || 1;
visited = visited || 0;
visits = visits || [visited];
return {
url: url,
frecency: frecency,
visited: visited,
visits: visits
};
}
mocksHelper.attachTestHelpers();
setup(done => {
asyncStorage.getItem = function(key, callback) {
callback(null);
};
subject = new PlacesIdbStore();
subject.init().then(function() {
subject.clear().then(function() {
done();
});
});
});
test('Test frecency', function(done) {
Promise.all([
subject.add('url', place(mozilla, 1, 0)),
subject.add('url', place(google, 3, 1)),
subject.add('url', place(yahoo, 2, 2)),
subject.add('url', place(mozilla, 4, 3))
]).then(function() {
subject.read('frecency', 5, function(results) {
assert.equal(results.length, 3);
assert.equal(results[0].url, mozilla);
assert.equal(results[1].url, google);
assert.equal(results[2].url, yahoo);
done();
});
});
});
test('Test history', function(done) {
Promise.all([
subject.add('url', place(mozilla, 1, 0)),
subject.add('url', place(google, 3, 1)),
subject.add('url', place(yahoo, 2, 2)),
subject.add('url', place(mozilla, 4, 3))
]).then(function() {
subject.read('visited', 5, function(results) {
assert.equal(results.length, 3);
assert.equal(results[0].url, mozilla);
assert.equal(results[1].url, yahoo);
assert.equal(results[2].url, google);
done();
});
});
});
test('Test frecency filter', function(done) {
Promise.all([
subject.add('url', place(google, -1, 0)),
subject.add('url', place(yahoo, -2, 2)),
subject.add('url', place(mozilla, 1, 3))
]).then(function() {
subject.read('frecency', 5, function(results) {
assert.equal(results.length, 1);
assert.equal(results[0].url, mozilla);
done();
}, function(place) {
return place.frecency > 0;
});
});
});
test('Test filter', function(done) {
Promise.all([
subject.add('url', place(mozilla, 1, 0)),
subject.add('url', place(google, 3, 1)),
subject.add('url', place(yahoo, 2, 2)),
subject.add('url', place(mozilla, 4, 3))
]).then(function() {
subject.read('visited', 5, function(results) {
assert.equal(results.length, 1);
assert.equal(results[0].url, mozilla);
done();
}, function(place) {
return place.url === mozilla;
});
});
});
test('Test visits', function(done) {
Promise.all([
subject.add('url', place(mozilla, 1, 0, [0])),
subject.add('url', place(google, 3, 0, [4, 2])),
subject.add('url', place(yahoo, 2, 0, [7, 3])),
subject.add('url', place(mozilla, 4, 0, [6, 5, 1, 0]))
]).then(function() {
subject.readVisits(7, function(results) {
assert.equal(results.length, 7);
assert.equal(results[0].url, yahoo);
assert.equal(results[1].url, mozilla);
assert.equal(results[2].url, mozilla);
assert.equal(results[3].url, google);
assert.equal(results[4].url, yahoo);
assert.equal(results[5].url, google);
assert.equal(results[6].url, mozilla);
done();
});
});
});
test('Adding empty visits should delete the record', function(done) {
Promise.all([
subject.add(mozilla, place(mozilla, 1, 1, [1])),
subject.add(mozilla, place(mozilla, 1, 2, []))
]).then(() => {
subject.read('visited', 5, results => {
assert.equal(results.length, 0);
subject.readVisits(5, results => {
assert.equal(results.length, 0);
done();
});
});
});
});
test('Adding empty visits should delete the record', function(done) {
Promise.all([
subject.add(mozilla, place(mozilla, 1, 1, [1])),
subject.remove(mozilla)
]).then(() => {
subject.read('visited', 5, results => {
assert.equal(results.length, 0);
subject.readVisits(5, results => {
assert.equal(results.length, 0);
done();
});
});
});
});
});

View file

@ -0,0 +1,99 @@
/* global MockNavigatorDatastore, MockDatastore, Promise */
'use strict';
require('/shared/test/unit/mocks/mock_navigator_datastore.js');
require('/shared/js/utilities.js');
requireApp('search/test/unit/mock_search.js');
requireApp('search/js/providers/provider.js');
requireApp('search/js/providers/grid_provider.js');
// Required files for the grid and a mozapp result
require('/shared/js/l10n.js');
require('/shared/elements/gaia_grid/js/grid_icon_renderer.js');
require('/shared/elements/gaia_grid/js/grid_layout.js');
require('/shared/elements/gaia_grid/js/grid_view.js');
require('/shared/elements/gaia_grid/script.js');
require('/shared/elements/gaia_grid/js/items/grid_item.js');
require('/shared/elements/gaia_grid/js/items/bookmark.js');
require('/shared/js/sync_datastore.js');
suite('search/providers/providers', function() {
var subject;
var realDatastore;
var promiseDone = Promise.resolve({ operation: 'done' });
var ICON = 'https://mozorg.cdn.mozilla.net/media/img/favicon.ico';
suiteSetup(function() {
realDatastore = navigator.getDataStores;
navigator.getDataStores = MockNavigatorDatastore.getDataStores;
MockNavigatorDatastore._records = {};
MockDatastore.sync = function() {
var cursor = {
next: function() {
cursor.next = () => promiseDone;
return Promise.resolve({
operation: 'add',
data: {
url: 'http://mozilla.org',
name: 'Homepage',
icon: ICON
}
});
}
};
return cursor;
};
});
suiteTeardown(function() {
navigator.getDataStores = realDatastore;
});
setup(function(done) {
requireApp('search/js/providers/bookmarks.js', function() {
subject = window.Bookmarks;
subject.grid = document.createElement('gaia-grid');
promiseDone.then(function() {
subject.init().then(done);
});
});
});
suite('search', function() {
test('renders data url', function(done) {
subject.search('mozilla').then((results) => {
assert.equal(results[0].data.detail.url, 'http://mozilla.org');
assert.equal(results[0].data.detail.name, 'Homepage');
assert.equal(results[0].data.detail.id, 'http://mozilla.org');
assert.equal(results[0].data.detail.icon, ICON);
done();
});
});
test('search name', function(done) {
subject.search('homepage').then((results) => {
assert.equal(results[0].data.detail.name, 'Homepage');
done();
});
});
});
suite('get', function() {
test('ok', function() {
var bookmark = subject.get('http://mozilla.org');
assert.equal(bookmark.data.detail.url, 'http://mozilla.org');
assert.equal(bookmark.data.detail.name, 'Homepage');
assert.equal(bookmark.data.detail.id, 'http://mozilla.org');
assert.equal(bookmark.data.detail.icon, ICON);
});
test('fail', function() {
var bookmark = subject.get('http://catanddog.org');
assert.isNull(bookmark);
});
});
});

View file

@ -0,0 +1,94 @@
'use strict';
/* global MocksHelper, MockMozContacts, MockContactsList, MockMozActivity,
Search */
require('/shared/test/unit/mocks/mock_moz_activity.js');
require('/shared/js/url_helper.js');
requireApp('search/test/unit/mock_search.js');
requireApp('search/js/providers/provider.js');
require('/shared/test/unit/mocks/mock_mozContacts.js');
require('/apps/communications/contacts/test/unit/mock_contacts_list.js');
var mocksForMarketplaceProvider = new MocksHelper([
'MozActivity'
]).init();
suite('search/providers/contacts', function() {
mocksForMarketplaceProvider.attachTestHelpers();
var fakeElement, stubById, subject;
suiteSetup(function() {
navigator.mozContacts = MockMozContacts;
sinon.stub(navigator.mozContacts, 'find', function() {
return {
set onsuccess(cb) {
cb(MockContactsList());
},
set onerror(cb) {
},
result: MockContactsList()
};
});
});
setup(function(done) {
fakeElement = document.createElement('div');
fakeElement.style.cssText = 'height: 100px; display: block;';
stubById = this.sinon.stub(document, 'getElementById')
.returns(fakeElement.cloneNode(true));
requireApp('search/js/providers/contacts.js', function() {
subject = Search.providers.Contacts;
subject.init();
done();
});
});
teardown(function() {
stubById.restore();
});
suite('click', function() {
test('launches the web activity', function() {
var searchCloseStub = this.sinon.stub(Search, 'close');
subject.click({
target: {
dataset: {
contactId: 'mozilla'
}
}
});
var activityInfo = MockMozActivity.calls[0];
assert.equal(activityInfo.name, 'open');
assert.equal(activityInfo.data.params.id, 'mozilla');
assert.ok(searchCloseStub.calledOnce);
});
});
suite('search', function() {
setup(function() {
this.sinon.stub(URL, 'createObjectURL');
});
test('clears results', function() {
var stub = this.sinon.stub(subject, 'clear');
subject.search(null, function() {});
assert.ok(stub.calledOnce);
});
test('contact is rendered', function(done) {
subject.search('stub content').then((results) => {
Search.collect(subject, results);
var contact = subject.container.querySelector('.result');
assert.equal(contact.querySelector('.title').innerHTML, 'Pepito A');
assert.equal(contact.getAttribute('aria-label'), 'Pepito A');
assert.equal(contact.getAttribute('role'), 'link');
assert.equal(contact.querySelector('.icon img').getAttribute('role'),
'presentation');
done();
});
});
});
});

View file

@ -0,0 +1,156 @@
'use strict';
/* global Search, MockNavigatormozApps, MockNavigatorSettings */
require('/shared/test/unit/mocks/mock_navigator_moz_apps.js');
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
requireApp('search/test/unit/mock_search.js');
requireApp('search/js/providers/provider.js');
requireApp('search/js/providers/grid_provider.js');
suite('search/providers/local_apps', function() {
var realMozApps;
var realMozSettings;
var _apps, _blValue;
suiteSetup(function() {
_apps = [{
manifest: {
entry_points: {
'fakeapp1-1': {
launch_path: '/fakeapp1-1/index.html',
name: 'Mozilla Fake App 1 - 1'
},
'fakeapp1-2': {
launch_path: '/fakeapp1-2/index.html',
name: 'Mozilla Fake App 1 - 2'
}
},
name: 'Mozilla Fake App 1'
},
manifestURL: 'http://fakeapp1/manifest.webapp'
},{
manifest: {
launch_path: '/fakeapp2/index.html',
name: 'Mozilla Fake App 2'
},
manifestURL: 'http://fakeapp2/manifest.webapp'
},{
manifest: {
launch_path: '/fakeapp3/index.html',
name: 'Mooilla Fake App 3',
short_name: 'shorty'
}
},{
manifest: {
launch_path: '/fakeapp4/index.html',
name: 'Blah blah',
locales: {
foobar: {
name: 'localized app'
}
}
}
}];
_blValue = ['/fakeapp1-1/index.html'];
realMozApps = navigator.mozApps;
navigator.mozApps = MockNavigatormozApps;
realMozSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
navigator.mozApps.mApps = _apps;
navigator.mozSettings.createLock().set({
'app.launch_path.blacklist': _blValue
});
});
suiteTeardown(function() {
navigator.mozApps = realMozApps;
navigator.mozSettings = realMozSettings;
});
suite('Blacklist', function() {
var subject;
setup(function(done) {
requireApp('search/js/providers/local_apps.js', function() {
subject = Search.providers.LocalApps;
done();
});
});
teardown(function() {
MockNavigatormozApps.mTeardown();
});
test('CreateBlacklist returns correct blacklist', function(done) {
subject.createBlacklist().then(() => {
assert.equal(subject.blacklist, _blValue);
done();
});
});
});
suite('Search', function() {
var subject;
setup(function(done) {
requireApp('search/js/providers/local_apps.js', function() {
subject = Search.providers.LocalApps;
subject.apps = _apps;
subject.blacklist = _blValue;
subject.createAppListing();
done();
});
});
teardown(function() {
MockNavigatormozApps.mTeardown();
});
test('Search returns correct applications', function() {
var results = subject.find('moz');
assert.equal(results.length, 2);
});
test('Search returns correct applications', function() {
var results = subject.find('mo');
assert.equal(results.length, 3);
});
test('Search for localized apps', function() {
document.documentElement.lang = 'foobar';
var results = subject.find('localized');
assert.equal(results.length, 1);
});
test('Search returns correct applications', function() {
var results = subject.find('shorty');
assert.equal(results.length, 1);
});
test('hidden role which downloads is not listed', function() {
var resultLength = subject.appListing.length;
var mockApp = {
manifestURL: 'marketplace/langpack',
downloading: true,
manifest: {
role: 'langpack'
}
};
navigator.mozApps.mgmt.oninstall({
application: mockApp
});
mockApp.ondownloadapplied();
assert.equal(resultLength, subject.appListing.length);
});
});
});

View file

@ -0,0 +1,108 @@
'use strict';
/* global MocksHelper, Search */
/* global MockNavigatorSettings */
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/shared/test/unit/mocks/mock_moz_activity.js');
require('/shared/test/unit/mocks/mock_l10n.js');
requireApp('search/test/unit/mock_search.js');
requireApp('search/js/providers/provider.js');
requireApp('search/js/providers/grid_provider.js');
// Required files for the grid and a marketplaceapp result
require('/shared/js/l10n.js');
require('/shared/elements/gaia_grid/js/grid_icon_renderer.js');
require('/shared/elements/gaia_grid/js/grid_layout.js');
require('/shared/elements/gaia_grid/js/grid_view.js');
require('/shared/elements/gaia_grid/script.js');
require('/shared/elements/gaia_grid/js/items/grid_item.js');
require('/shared/elements/gaia_grid/js/items/bookmark.js');
require('/shared/elements/gaia_grid/js/items/marketplace_app.js');
var mocksForMarketplaceProvider = new MocksHelper([
'MozActivity'
]).init();
suite('search/providers/marketplace', function() {
mocksForMarketplaceProvider.attachTestHelpers();
var fakeElement, stubById, subject, realMozSettings;
setup(function(done) {
realMozSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
navigator.mozSettings.createLock().set({
'search.marketplace.url': 'http://localhost?{q}'
});
fakeElement = document.createElement('div');
fakeElement.style.cssText = 'height: 100px; display: block;';
stubById = this.sinon.stub(document, 'getElementById')
.returns(fakeElement.cloneNode(true));
requireApp('search/js/providers/marketplace.js', function() {
subject = Search.providers.Marketplace;
subject.init();
done();
});
});
teardown(function() {
stubById.restore();
});
suite('mock response', function() {
var marketplaceContent = [{
'manifest_url': 'http://fakeapp1.mozilla.org/manifest.webapp',
'name': 'FIRST Marketplace App',
'icon': ''
}, {
'manifest_url': 'http://fakeapp2.mozilla.org/manifest.webapp',
'name': 'SECOND Marketplace App',
'icon': ''
}];
var requests = [];
var xhr;
setup(function() {
xhr = sinon.useFakeXMLHttpRequest();
requests = [];
xhr.onCreate = function(req) {
setTimeout(function() {
requests.push(req);
req.responseText = JSON.stringify(marketplaceContent);
req.status = 200;
req.onload();
});
};
});
teardown(function() {
subject.request = null;
xhr.restore();
});
test('escapes search terms', function(done) {
subject.search('foo#').then(function(results) {
assert.equal(requests[0].url, 'http://localhost?foo%23');
done();
});
});
test('renders all results', function(done) {
subject.search('fake').then(results => {
assert.equal(results.length, 2);
done();
});
// setTimeout to ensure that the search microtask fires.
setTimeout(function() {
var req = requests[0];
req.responseText = JSON.stringify(marketplaceContent);
req.onload();
});
});
});
});

View file

@ -0,0 +1,68 @@
'use strict';
/* global Provider, MocksHelper */
requireApp('search/shared/js/everythingme/eme.js');
requireApp('search/js/providers/provider.js');
require('/shared/test/unit/mocks/mock_icons_helper.js');
var mocks = new MocksHelper([
'IconsHelper'
]).init();
suite('search/providers/provider', function() {
var fakeElement, stubById, subject;
mocks.attachTestHelpers();
setup(function() {
fakeElement = document.createElement('div');
fakeElement.style.cssText = 'height: 100px; display: block;';
stubById = this.sinon.stub(document, 'getElementById')
.returns(fakeElement.cloneNode(true));
subject = new Provider();
});
teardown(function() {
stubById.restore();
});
suite('constructor', function() {
test('everything looks ok', function() {
assert.ok(subject.name);
});
});
suite('init', function() {
test('sets up container', function() {
subject.init();
assert.ok(subject.container);
});
});
suite('render', function() {
test('escapes HTML', function() {
subject.init();
subject.render([{
title: '<hello>',
meta: '<world>',
dataset: {}
}]);
var title = subject.container.querySelector('.title');
assert.equal(title.innerHTML, '&lt;hello&gt;');
var meta = subject.container.querySelector('.meta');
assert.equal(meta.innerHTML, '&lt;world&gt;');
});
});
suite('clear', function() {
test('clears container', function() {
subject.init();
subject.container.innerHTML = 'not empty';
subject.clear();
assert.equal(subject.container.innerHTML, '');
});
});
});

View file

@ -0,0 +1,37 @@
'use strict';
/* global MocksHelper, Search */
requireApp('search/test/unit/mock_search.js');
requireApp('search/js/providers/provider.js');
require('/shared/js/metrics_helper.js');
requireApp('search/test/unit/mock_provider.js');
require('/shared/test/unit/mocks/mock_search_provider.js');
var mocksForSuggestionsProvider = new MocksHelper([
'SearchProvider'
]).init();
suite('search/providers/suggestions', function() {
mocksForSuggestionsProvider.attachTestHelpers();
var subject;
requireApp('search/js/providers/suggestions.js', function(done) {
subject = Search.providers.Suggestions;
subject.init();
done();
});
suite('click', function() {
test('should report to metrics', function() {
var stub = this.sinon.stub(window.MetricsHelper.prototype, 'report');
subject.click({
target: {
dataset: {
suggestions: 'mozilla'
}
}
});
assert.ok(stub.calledWith('websearch', 'testProvider'));
});
});
});

View file

@ -0,0 +1,734 @@
'use strict';
/* global MockNavigatormozApps, MockNavigatormozSetMessageHandler,
Search, MockProvider, MockasyncStorage, Promise, MocksHelper */
require('/shared/test/unit/mocks/mock_navigator_moz_apps.js');
require('/shared/test/unit/mocks/mock_navigator_moz_set_message_handler.js');
require('/shared/test/unit/mocks/mock_moz_activity.js');
require('/shared/test/unit/mocks/mock_search_provider.js');
require('/shared/js/url_helper.js');
require('/shared/js/dedupe.js');
require('/js/contextmenu.js');
require('/shared/js/metrics_helper.js');
require('/shared/test/unit/mocks/mock_async_storage.js');
requireApp('search/test/unit/mock_provider.js');
var mocksForSearch = new MocksHelper([
'SearchProvider'
]).init();
suite('search/search', function() {
var realAsyncStorage;
var realMozApps;
var realSetMessageHandler;
var realOnLine;
var clock;
mocksForSearch.attachTestHelpers();
function removeProvider(provider) {
delete Search.providers[provider.name];
}
suiteSetup(function(done) {
loadBodyHTML('/index.html');
realSetMessageHandler = navigator.mozSetMessageHandler;
navigator.mozSetMessageHandler = MockNavigatormozSetMessageHandler;
realMozApps = navigator.mozApps;
navigator.mozApps = MockNavigatormozApps;
realAsyncStorage = window.asyncStorage;
window.asyncStorage = MockasyncStorage;
window.SettingsListener = {
observe: function() {}
};
clock = sinon.useFakeTimers();
requireApp('search/js/search.js', function() {
// assert.equal(Search.toShowNotice, null);
// Search.toShowNotice = false;
Search._port = { postMessage: function() {} };
done();
});
});
suiteTeardown(function() {
navigator.mozSetMessageHandler = realSetMessageHandler;
navigator.mozApps = realMozApps;
window.asyncStorage = realAsyncStorage;
clock.restore();
delete window.SettingsListener;
});
setup(function() {
MockNavigatormozSetMessageHandler.mSetup();
});
teardown(function() {
MockNavigatormozSetMessageHandler.mTeardown();
MockNavigatormozApps.mTeardown();
});
suite('init', function() {
test('will call provider init method', function() {
var initCalled;
this.sinon.spy(window.MetricsHelper.prototype, 'init');
this.sinon.spy(navigator, 'mozSetMessageHandler');
Search.providers = [{
init: function() {
initCalled = true;
},
search: function() {},
abort: function() {}
}];
Search.init();
MockNavigatormozApps.mTriggerLastRequestSuccess();
assert.equal(MockNavigatormozApps.mLastConnectionKeyword,
'search-results');
MockNavigatormozApps.mLastConnectionCallback([]);
assert.ok(initCalled);
Search.providers = [];
assert.isFalse(Search.suggestionsWrapper.classList.contains('offline'));
assert.ok(window.MetricsHelper.prototype.init.calledOnce);
assert.ok(navigator.mozSetMessageHandler.called);
});
});
suite('offline', function() {
suiteSetup(function() {
realOnLine = Object.getOwnPropertyDescriptor(navigator, 'onLine');
Object.defineProperty(navigator, 'onLine', {
configurable: true,
get: function() { return false; }
});
});
suiteTeardown(function() {
if (realOnLine) {
Object.defineProperty(navigator, 'onLine', realOnLine);
}
});
test('init while offline', function() {
this.sinon.spy(Search, 'initConnectivityCheck');
Search.init();
assert.ok(Search.initConnectivityCheck.calledOnce);
assert.isTrue(Search.searchResults.classList.contains('offline'));
});
});
suite('provider', function() {
test('increments number of providers', function() {
function numProviders() {
return Object.keys(Search.providers).length;
}
var count = numProviders();
Search.provider({
name: 'Foo'
});
assert.equal(count + 1, numProviders());
removeProvider({
name: 'Foo'
});
assert.equal(count, numProviders());
});
});
suite('initNotice', function() {
test('gets the settingsLink on every call', function() {
var stub = this.sinon.stub(document, 'getElementById').returns({
addEventListener: this.sinon.spy()
});
Search.initNotice();
Search.initNotice();
assert.isTrue(stub.withArgs('settings-link').callCount === 2);
});
});
suite('scrolling focus', function() {
test('we grab focus when scrolling results', function() {
var stub = this.sinon.stub(window, 'focus');
window.dispatchEvent(new window.Event('scroll'));
assert.ok(stub.calledOnce);
});
});
suite('dispatchMessage', function() {
test('dispatches messages based on action', function() {
var stub = this.sinon.stub(Search, 'change');
Search.dispatchMessage({
data: {
action: 'change',
input: 'http://mozilla.org'
}
});
assert.ok(stub.calledOnce);
});
});
suite('change', function() {
setup(function() {
var fakeProvider = {
name: 'Foo',
search: function() {
return Promise.resolve();
},
abort: function() {},
clear: function() {}
};
Search.provider(fakeProvider);
});
test('only searches once if called twice rapidly', function() {
var stub = this.sinon.stub(Search.providers.Foo, 'search').returns(
new Promise(() => {}));
Search.change({
data: {
input: 'a'
}
});
Search.change({
data: {
input: 'a'
}
});
assert.ok(stub.notCalled);
clock.tick(1000); // For typing timeout
assert.ok(stub.calledOnce);
});
test('full URLs do not get sent', function() {
var remoteProvider = {
name: 'remoteguy',
remote: true,
search: function() {},
abort: function() {},
clear: function() {}
};
Search.provider(remoteProvider);
Search.suggestionsEnabled = true;
var stub = this.sinon.stub(Search.providers.remoteguy, 'search');
Search.change({
data: {
input: 'http://mozilla.org'
}
});
clock.tick(1000); // For typing timeout
assert.ok(stub.notCalled);
});
test('aborting will cancel search timeout', function() {
var stub = this.sinon.stub(Search.providers.Foo, 'search');
Search.change({
data: {
input: 'a'
}
});
Search.change({
data: {
input: 'a'
}
});
assert.ok(stub.notCalled);
Search.abort();
clock.tick(1000); // For typing timeout
assert.ok(stub.notCalled);
});
test('when a private browser', function() {
var remoteProvider = {
name: 'remoteguy',
remote: true,
search: function() {},
abort: function() {},
clear: function() {}
};
Search.provider(remoteProvider);
Search.suggestionsEnabled = true;
var stub = this.sinon.stub(Search.providers.remoteguy, 'search');
Search.change({
data: {
input: 'search me',
isPrivateBrowser: true
}
});
clock.tick(1000); // For typing timeout
assert.ok(stub.notCalled);
});
});
suite('submit', function() {
test('Navigates to a URL', function() {
var stub = this.sinon.stub(Search, 'navigate');
Search.dispatchMessage({
data: {
action: 'submit',
input: 'http://mozilla.org'
}
});
clock.tick(1000); // For typing timeout
assert.ok(stub.calledOnce);
});
test('Fires correct Event to metrics search for search term', function() {
var stub = this.sinon.stub(window.MetricsHelper.prototype, 'report');
Search.dispatchMessage({
data: {
action: 'submit',
input: 'searchterm'
}
});
clock.tick(1000); // For typing timeout
assert.ok(stub.calledWith('websearch', 'testProvider'));
});
});
suite('clear', function() {
var called = 0;
suiteSetup(function() {
Search.providers = {
Fake: {
clear: function() {
called++;
},
abort: function() {}
}
};
});
test('calls the provider.clear method', function() {
Search.clear();
assert.equal(called, 1);
});
});
suite('abort', function() {
test('calls abort method of provider', function() {
Search.providers = {
Fake: {
clear: function() {},
abort: function() {}
}
};
var abortStub = this.sinon.stub(Search.providers.Fake, 'abort');
Search.abort();
assert.ok(abortStub.calledOnce);
});
});
suite('close', function() {
test('posts a message to the port', function() {
var stub = this.sinon.stub(Search._port, 'postMessage');
Search.close();
assert.ok(stub.calledWith({action: 'hide'}));
});
});
suite('navigate', function() {
test('Open activity is fired', function() {
var url = 'http://mozilla.org';
var stub = this.sinon.stub(window, 'open');
Search.navigate(url);
assert.ok(stub.calledOnce);
});
});
suite('expandSearch', function() {
setup(function() {
Search.providers = {
WebResults: {
clear: function() {},
abort: function() {},
search: function() {
return new Promise(() => {});
},
fullscreen: function() {}
}
};
});
test('calls search for WebResults', function() {
var stub = this.sinon.stub(Search.providers.WebResults,'fullscreen');
Search.expandSearch();
assert.ok(stub.calledOnce);
});
});
suite('setInput', function() {
test('posts a message to the port', function() {
var stub = this.sinon.stub(Search._port, 'postMessage');
this.sinon.stub(Search, 'expandSearch');
Search.setInput('foo');
assert.ok(stub.calledWith({action: 'input', input: 'foo'}));
});
});
suite('handleActivityEvents', function() {
test('handle search activity', function() {
this.sinon.stub(Search, 'submit');
Search.handleActivityEvents({
source: {
name: 'search',
data: {keyword: 'mozilla'}
}
});
assert.ok(Search.submit.calledWith({data: {
input: 'mozilla'
}}));
});
});
suite('collect', function() {
setup(function() {
Search.dedupe.exactResults = {};
Search.dedupe.fuzzyResults = {};
});
// Suppport functions
function exactProvider() {
return {
dedupes: true,
dedupeStrategy: 'exact',
render: function() {}
};
}
function fuzzyProvider() {
return {
dedupes: true,
dedupeStrategy: 'fuzzy',
render: function() {}
};
}
test('provider does not de-dupe', function() {
var results = [
{dedupeId: 'a'},
{dedupeId: 'b'}
];
var provider = {
dedupes: false,
render: function() {}
};
var renderStub = this.sinon.stub(provider, 'render');
Search.collect(provider, results);
assert.equal(renderStub.getCall(0).args[0].length, 2);
});
test('when no de-duplication necessary', function() {
var provider1 = exactProvider();
var provider2 = exactProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, [{dedupeId: 'a'}]);
Search.collect(provider2, [{dedupeId: 'b'}]);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 1);
});
test('de-duplicates an exact manifestURL', function() {
var results1 = [
{dedupeId: 'http://mozilla.org/manifest.webapp'}
];
var results2 = [
{dedupeId: 'http://mozilla.org/manifest.webapp'}
];
var provider1 = exactProvider();
var provider2 = exactProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 0);
});
test('de-duplicates a fuzzy match /w same domain', function() {
var results1 = [
{dedupeId: 'http://mozilla.org/manifest.webapp'}
];
var results2 = [
{dedupeId: 'http://mozilla.org/awesome/app'}
];
var provider1 = fuzzyProvider();
var provider2 = fuzzyProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 0);
});
test('de-duplicates a fuzzy match /w subdomain', function() {
var results1 = [
{dedupeId: 'http://mozilla.org/manifest.webapp'}
];
var results2 = [
{dedupeId: 'http://omggame.mozilla.org/awesome/app'}
];
var provider1 = fuzzyProvider();
var provider2 = fuzzyProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 0);
});
test('attempt to avoid de-duplicating second level domains', function() {
var results1 = [
{dedupeId: 'http://mozilla.co.org/manifest.webapp'}
];
var results2 = [
{dedupeId: 'http://bob.co.org/awesome/app'}
];
var provider1 = exactProvider();
var provider2 = fuzzyProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 1);
});
test('installed hosted app /w web result', function() {
var results1 = [
{dedupeId:
'https://m.facebook.com/openwebapp/manifest.webapp'
}
];
var results2 = [
{dedupeId: 'https://www.facebook.com/'}
];
var provider1 = exactProvider();
var provider2 = fuzzyProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 0);
});
test('mis-matching domain against manifest url', function() {
var results1 = [
{dedupeId:
'https://bits.wikimedia.org/wikipediamobilefirefoxos/manifest.webapp'
}
];
var results2 = [
{dedupeId: 'http://en.m.wikipedia.org'}
];
var provider1 = fuzzyProvider();
var provider2 = fuzzyProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 0);
});
test('common domain parts filtered out', function() {
Search.dedupe.fuzzyResults = {
'touch': true,
'mobile': true
};
var results1 = [
{dedupeId: 'https://touch.www.linkedin.com/login.html'}
];
var results2 = [
{dedupeId: 'https://touch.mozilla.org/fake.html'}
];
var provider1 = fuzzyProvider();
var provider2 = fuzzyProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 1);
});
test('bug 1030713 - subdomain dededupe matching', function() {
var results1 = [
{dedupeId: 'http://translate.google.com'}
];
var results2 = [
{dedupeId: 'http://mail.google.com/tasks'},
{dedupeId: 'http://drive.google.com/keep'},
];
var provider1 = fuzzyProvider();
var provider2 = fuzzyProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 2);
});
test('bug 1030713 - subdomain dededupe matching with path', function() {
var results1 = [
{dedupeId: 'http://drive.google.com/keep'}
];
var results2 = [
{dedupeId: 'http://mail.google.com/tasks'},
{dedupeId: 'http://translate.google.com'},
];
var provider1 = fuzzyProvider();
var provider2 = fuzzyProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 2);
});
test('exact provider does not de-dupe against itself', function() {
var results = [
{dedupeId: 'https://mozilla.org/index.html'},
{dedupeId: 'https://mozilla.org/sauce.html'}
];
var provider = exactProvider();
var renderStub = this.sinon.stub(provider, 'render');
Search.collect(provider, results);
assert.equal(renderStub.getCall(0).args[0].length, 2);
});
test('fuzzy provider does not de-dupe against itself', function() {
var results = [
{dedupeId: 'https://mozilla.org/index.html'},
{dedupeId: 'https://mozilla.org/sauce.html'}
];
var provider = fuzzyProvider();
var renderStub = this.sinon.stub(provider, 'render');
Search.collect(provider, results);
assert.equal(renderStub.getCall(0).args[0].length, 2);
});
test('exact provider ignores querystring', function() {
var results1 = [
{dedupeId: 'https://mozilla.org/index.html?ignoreme=true'}
];
var results2 = [
{dedupeId: 'https://mozilla.org/index.html'}
];
var provider1 = exactProvider();
var provider2 = exactProvider();
var renderStub1 = this.sinon.stub(provider1, 'render');
var renderStub2 = this.sinon.stub(provider2, 'render');
Search.collect(provider1, results1);
Search.collect(provider2, results2);
assert.equal(renderStub1.getCall(0).args[0].length, 1);
assert.equal(renderStub2.getCall(0).args[0].length, 0);
});
test('Dont search remote providers when suggestions disabled', function() {
var localProvider = new MockProvider('local');
var remoteProvider = new MockProvider('remote');
remoteProvider.remote = true;
var remoteStub = this.sinon.stub(remoteProvider, 'search')
.returns(new Promise(() => {}));
var localStub = this.sinon.stub(localProvider, 'search')
.returns(new Promise(() => {}));
Search.provider(localProvider);
Search.provider(remoteProvider);
Search.suggestionsEnabled = false;
Search.change({data: {input: 'test'}});
clock.tick(1000);
assert.ok(remoteStub.notCalled);
assert.ok(localStub.calledOnce);
removeProvider(localProvider);
removeProvider(remoteProvider);
});
test('Search all providers when suggestions enabled', function() {
var localProvider = new MockProvider('local');
var remoteProvider = new MockProvider('remote');
remoteProvider.remote = true;
var remoteStub = this.sinon.stub(remoteProvider, 'search')
.returns(new Promise(() => {}));
var localStub = this.sinon.stub(localProvider, 'search')
.returns(new Promise(() => {}));
Search.provider(localProvider);
Search.provider(remoteProvider);
Search.suggestionsEnabled = true;
Search.change({data: {input: 'test'}});
clock.tick(1000);
assert.ok(remoteStub.calledOnce);
assert.ok(localStub.calledOnce);
removeProvider(localProvider);
removeProvider(remoteProvider);
});
});
});

View file

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

View file

@ -0,0 +1,78 @@
/* globals DateHelper */
'use strict';
requireApp('search/shared/js/utilities.js');
suite('Date Helper', function() {
function assertMidnight(date) {
assert.equal(date.getHours(), 0);
assert.equal(date.getMinutes(), 0);
assert.equal(date.getSeconds(), 0);
assert.equal(date.getMilliseconds(), 0);
}
suite('started', function() {
test('todayStarted', function() {
var now = new Date();
var todayStartedTimestamp = DateHelper.todayStarted();
var todayStarted = new Date(todayStartedTimestamp);
assert.equal(todayStarted.getFullYear(), now.getFullYear());
assert.equal(todayStarted.getMonth(), now.getMonth());
assert.equal(todayStarted.getDate(), now.getDate());
assertMidnight(todayStarted);
});
test('yesterdayStarted', function() {
var now = new Date();
var yesterdayStartedTimestamp = DateHelper.yesterdayStarted();
var yesterdayStarted = new Date(yesterdayStartedTimestamp);
assert.equal(true, yesterdayStartedTimestamp <= now.valueOf());
assertMidnight(yesterdayStarted);
});
test('thisWeekStarted', function() {
var now = new Date();
var weekStartedTimestamp = DateHelper.thisWeekStarted();
var weekStarted = new Date(weekStartedTimestamp);
assert.equal(true, weekStartedTimestamp <= now.valueOf());
assert.equal(weekStarted.getDay(), 0);
assertMidnight(weekStarted);
});
test('thisMonthStarted', function() {
var now = new Date();
var monthStartedTimestamp = DateHelper.thisMonthStarted();
var monthStarted = new Date(monthStartedTimestamp);
assert.equal(true, monthStartedTimestamp <= now.valueOf());
assert.equal(monthStarted.getFullYear(), now.getFullYear());
assert.equal(monthStarted.getMonth(), now.getMonth());
assert.equal(monthStarted.getDate(), 1);
assertMidnight(monthStarted);
});
test('lastSixMonthsStarted', function() {
var sixMonthsStarted = DateHelper.lastSixMonthsStarted();
var now = new Date().valueOf();
var delta = now - sixMonthsStarted;
assert.equal(true, delta >= 2629743830 * 6);
assert.equal(true, delta < 2629743830 * 7);
});
test('thisYearStarted', function() {
var now = new Date();
var yearStartedTimestamp = DateHelper.thisYearStarted();
var yearStarted = new Date(yearStartedTimestamp);
assert.equal(true, yearStartedTimestamp <= now.valueOf());
assert.equal(yearStarted.getFullYear(), now.getFullYear());
assert.equal(yearStarted.getMonth(), 0);
assert.equal(yearStarted.getDate(), 1);
assertMidnight(yearStarted);
});
});
});