'use strict'; var getIconId = require('./icon_id'); /** * Abstraction around homescreen. * @constructor */ function VerticalHome(client) { this.client = client; this.system = client.loader.getAppClass('system'); } /** * @type String Origin of VerticalHome app */ VerticalHome.URL = 'app://verticalhome.gaiamobile.org'; VerticalHome.Selectors = { grid: '#icons', editHeaderText: '#edit-header h1', editHeaderDone: '#exit-edit-mode', editGroup: '#edit-group', editGroupTitle: '#edit-group-title', editGroupSave: '#edit-group-save', editGroupTitleClear: '#edit-group-title-clear', search: '#search', firstIcon: '#icons div.icon:not(.placeholder)', groupHeader: '#icons .group .header', groupTitle: '#icons .group .header .title', groupBackground: '#icons .group .background', groupToggle: '#icons .group .toggle', dividers: '#icons section.divider', contextmenu: '#contextmenu-dialog', themeColor: 'head meta[name="theme-color"]', placeholders: '#icons .placeholder' }; /** * Launches our new homescreen and focuses on it. */ VerticalHome.prototype = { URL: VerticalHome.URL, Selectors: VerticalHome.Selectors, get numIcons() { return this.client.findElements(VerticalHome.Selectors.firstIcon).length; }, get numDividers() { return this.dividers.length; }, get dividers() { return this.client.findElements(VerticalHome.Selectors.dividers); }, get contextMenu() { return this.client.findElement(VerticalHome.Selectors.contextmenu); }, /** Fetch a particular type of a gaia-confirm dialog. @param {String} type of dialog. */ getConfirmDialog: function(type) { var selector = 'gaia-confirm[data-type="' + type + '"]'; return this.client.helper.waitForElement(selector); }, /** Click confirm on a particular type of confirmation dialog. @param {String} type of dialog. @param {String} selector of the button. Defaults to .confirm. */ confirmDialog: function(type, button) { var dialog = this.getConfirmDialog(type); var confirm; this.client.waitFor(function() { confirm = dialog.findElement(button || '.confirm'); return confirm && confirm.displayed(); }); // XXX: Hack to use faster polling var quickly = this.client.scope({ searchTimeout: 50 }); confirm.client = quickly; // tricky logic to ensure the dialog has been removed and clicked this.client.waitFor(function() { try { // click the dialog to dismiss it confirm.click(); // ensure it is either hidden or hits the stale element ref return !confirm.displayed(); } catch (e) { if (e.type === 'StaleElementReference') { // element was successfully removed return true; } throw e; } }); }, /** * Enter edit mode by long pressing a given icon, or the first icon on the * grid if not specified. */ enterEditMode: function(icon) { var actions = this.client.loader.getActions(); var firstIcon = icon || this.client.helper.waitForElement(VerticalHome.Selectors.firstIcon); actions.longPress(firstIcon, 1).perform(); this.client.helper.waitForElement(VerticalHome.Selectors.editHeaderText); }, /** * Exits the edit mode by long-pressing the done button and waiting for it * to disappear. */ exitEditMode: function() { var done = this.client.findElement(VerticalHome.Selectors.editHeaderDone); done.click(); this.client.helper.waitForElementToDisappear(done); }, /** * Emulates pressing of the hardware home button. */ pressHomeButton: function() { this.client.executeScript(function() { var home = new CustomEvent('home'); window.dispatchEvent(home); }); }, focusRocketBar: function() { this.client.helper.waitForElement(VerticalHome.Selectors.search).tap(); this.client.switchToFrame(); }, /** Tap an app icon in the homescreen @param {String} manifestURL full manifest url path. */ launchApp: function(manifestURL) { // switch back to the homescreen this.client.switchToFrame(); this.client.switchToFrame(this.system.getHomescreenIframe()); // tap the app in the homescreen var newApp = this.getIcon(manifestURL); newApp.tap(); // go to the system app this.client.switchToFrame(); }, /** Tap an app icon and switch to it's application iframe. @param {String} manifestURL full manifest url path. */ launchAndSwitchToApp: function(manifestURL) { var client = this.client.scope({ searchTimeout: 100 }); var frame; client.waitFor(function() { this.launchApp(manifestURL); // wait for the app to show up try { frame = client.findElement( 'iframe[mozapp="' + manifestURL + '"]' ); } catch (e) { // try again... return false; } client.switchToFrame(frame); return true; }.bind(this)); }, /** Restart the homescreen then refocus on it. */ restart: function() { this.client.executeScript(function() { window.close(); }); // initialize our frames again since we killed the iframe this.client.switchToFrame(); this.client.switchToFrame(this.system.getHomescreenIframe()); }, /** Find and return every id for all the items on the grid... Each element can be used with `.getIcon` to find the element for a given id. @return {Array[String]} */ getIconIdentifiers: function() { return this.client.findElements('[data-identifier]').map(function(el) { return getIconId(el); }); }, /** Fetch an icon element on the homescreen. @param {String} manifestURL must be a fully qualified manifest url. @return {Marionette.Element} */ getIcon: function(manifestUrl, entryPoint) { return this.client.helper.waitForElement( '[data-identifier*="' + manifestUrl + (entryPoint ? '-' + entryPoint : '') + '"]' ); }, /** Fetch the nth icon on the homescreen. @param {number} the icon number we want @return {Marionette.Element} */ getNthIcon: function(number) { return this.client.helper.waitForElement( '.icon:nth-of-type(' + number + ')' ); }, /** Get the the current meta=theme-color of the homescreen @return {String} */ getThemeColor: function() { var meta = this.client.findElement(VerticalHome.Selectors.themeColor); return meta.getAttribute('content'); }, /** * Waits for the homescreen to launch and switches to the frame. */ waitForLaunch: function() { this.client.helper.waitForElement('body'); this.client.apps.switchToApp(VerticalHome.URL); }, /** * Gets a localized application name from a manifest. * @param {String} app to open * @param {String} entryPoint to open * @param {String} locale */ localizedAppName: function(app, entryPoint, locale) { if (!locale) { locale = entryPoint; entryPoint = null; } var file = 'app://' + app + '.gaiamobile.org/manifest.webapp'; // use a chrome-scoped Marionette client for the cross-domain XHR var chromeClient = this.client.scope({context: 'chrome'}); var manifest = chromeClient.executeAsyncScript(function(file) { var xhr = new XMLHttpRequest(); xhr.open('GET', file, true); xhr.onload = function(o) { var data = JSON.parse(xhr.response); marionetteScriptFinished(data); }; xhr.send(null); }, [file]); var locales; if (entryPoint) { locales = manifest.entry_points[entryPoint].locales; } else { locales = manifest.locales; } if (!locales) { return false; } if (locale.indexOf('-x-ps') > -1) { return this.client.executeScript(function(locale, name) { var mozL10n = window.wrappedJSObject.navigator.mozL10n; return mozL10n.qps[locale].translate(name); }, [locale, locales['en-US'].name]); } return locales[locale].name; }, /** * Returns a localized string from a properties file. * @param {String} key of the string to lookup. */ l10n: function(key) { var string = this.client.executeScript(function(key) { return window.wrappedJSObject.navigator.mozL10n.get(key); }, [key]); return string; }, containsClass: function(selector, clazz) { return this.client.executeScript(function(selector, clazz) { return document.querySelector(selector).classList.contains(clazz); }, [selector, clazz]); }, /** * Helper function to move an icon to a specified index. Currently uses * executeScript() and manually fiddles with the homescreen grid logic, * this is because scripted drag/drop does not work too well within the * vertically scrolling homescreen on b2g desktop. * @param {Element} icon The grid icon object. * @param {Integer} index The position to insert the icon into. */ moveIconToIndex: function(icon, index) { this.client.executeScript(function(identifier, newPos) { var app = window.wrappedJSObject.app; var icon = app.grid.getIcon(identifier); app.grid.moveTo(icon.detail.index, newPos); app.grid.render(); }, [icon.getAttribute('data-identifier'), index]); // Wait for the icon to animate into place var actions = this.client.loader.getActions(); actions.wait(1).perform(); }, /** * Opens the context menu by long-pressing on the first available placeholder. * If there is no placeholder, the first icon will be dragged to the top of * the grid to create a new group, and thus new placeholders. */ openContextMenu: function() { var actions = this.client.loader.getActions(); var body = this.client.findElement('body'); actions.longPress(body, 1).perform(); } }; module.exports = VerticalHome;