From 896f3929a2b6bdeebd2114120396f3d797278774 Mon Sep 17 00:00:00 2001 From: Fernando Campo Date: Thu, 15 Jan 2015 10:43:02 +0000 Subject: [PATCH] Bug 1073495 - Add 'Remember selection' checkbox to activity selection menu ------ All work made by David , just Marionette tests fixed --- .../test/unit/default_activity_helper_test.js | 61 ++++ apps/system/index.html | 2 + apps/system/js/action_menu.js | 63 +++- apps/system/js/activities.js | 113 ++++++- apps/system/locales/system.en-US.properties | 1 + .../action_menu/action_menu_extended.css | 23 ++ .../test/apps/activitycallee/defaulttest.html | 27 ++ .../test/apps/activitycallee/manifest.webapp | 10 + .../test/apps/activitycaller/index.html | 1 + .../test/apps/activitycaller/js/caller.js | 11 + .../test/apps/fakeinstalledapp/index.html | 10 + .../apps/fakeinstalledapp/manifest.webapp | 22 ++ .../test/marionette/default_activity_test.js | 192 +++++++++++ apps/system/test/unit/action_menu_test.js | 41 ++- apps/system/test/unit/activities_test.js | 315 ++++++++++++++---- shared/js/default_activity_helper.js | 169 ++++++++++ .../activities/activities.en-US.properties | 31 ++ .../mocks/mock_default_activity_helper.js | 30 ++ .../unit/mocks/mock_navigator_moz_apps.js | 5 +- 19 files changed, 1034 insertions(+), 93 deletions(-) create mode 100644 apps/sharedtest/test/unit/default_activity_helper_test.js create mode 100644 apps/system/test/apps/activitycallee/defaulttest.html create mode 100644 apps/system/test/apps/fakeinstalledapp/index.html create mode 100644 apps/system/test/apps/fakeinstalledapp/manifest.webapp create mode 100644 apps/system/test/marionette/default_activity_test.js create mode 100644 shared/js/default_activity_helper.js create mode 100644 shared/locales/activities/activities.en-US.properties create mode 100644 shared/test/unit/mocks/mock_default_activity_helper.js diff --git a/apps/sharedtest/test/unit/default_activity_helper_test.js b/apps/sharedtest/test/unit/default_activity_helper_test.js new file mode 100644 index 000000000000..6e6d5769b787 --- /dev/null +++ b/apps/sharedtest/test/unit/default_activity_helper_test.js @@ -0,0 +1,61 @@ +'use strict'; + +/* global DefaultActivityHelper, MockSettingsHelper, SettingsHelper */ + +require('/shared/js/default_activity_helper.js'); +require('/shared/test/unit/mocks/mock_settings_helper.js'); + +suite('Default Activity Helper', function() { + var realSettingsHelper; + + suiteSetup(function() { + realSettingsHelper = window.SettingsHelper; + window.SettingsHelper = MockSettingsHelper; + }); + + suiteTeardown(function() { + window.SettingsHelper = realSettingsHelper; + }); + + suite('getDefaultConfig', function() { + test('returns config if supported', function() { + var config = DefaultActivityHelper.getDefaultConfig('view', 'url'); + assert.ok(config); + assert.equal(config.settingsId, 'activity.default.openurl'); + }); + + test('returns undefined if not supported', function() { + var config = DefaultActivityHelper.getDefaultConfig('aaa', 'bbbb'); + assert.equal(config, undefined); + }); + }); + + suite('getDefaultAction', function(done) { + test('setting is recovered for supported activity', function(done) { + var settingsHelper = SettingsHelper('activity.default.openurl', null); + settingsHelper.set('manifest'); + DefaultActivityHelper.getDefaultAction('view', 'url').then((action) => { + assert.equal(action, 'manifest'); + done(); + }); + }); + + test('cb is called with null if not supported', function(done) { + DefaultActivityHelper.getDefaultAction('aaaa', 'bbb').then((action) => { + assert.equal(action, null); + done(); + }); + }); + }); + + suite('setDefaultAction', function(done) { + test('setting is set for supported action', function(done) { + var settingsHelper = SettingsHelper('activity.default.openurl', null); + DefaultActivityHelper.setDefaultAction('view', 'url', 'testmanifest'); + settingsHelper.get(function(value) { + assert.equal(value, 'testmanifest'); + done(); + }); + }); + }); +}); diff --git a/apps/system/index.html b/apps/system/index.html index 26b2040da9a2..4711fd0b096a 100644 --- a/apps/system/index.html +++ b/apps/system/index.html @@ -25,6 +25,7 @@ + @@ -329,6 +330,7 @@ + diff --git a/apps/system/js/action_menu.js b/apps/system/js/action_menu.js index b9bcb546ab74..6d7489861099 100644 --- a/apps/system/js/action_menu.js +++ b/apps/system/js/action_menu.js @@ -15,11 +15,12 @@ * @param {Boolean} preventFocusChange Set to true to prevent focus changing. */ function ActionMenu(listItems, titleL10nId, successCb, cancelCb, - preventFocusChange) { + preventFocusChange, askForDefaultChoice) { this.onselected = successCb || function() {}; this.oncancel = cancelCb || function() {}; this.listItems = listItems; this.titleL10nId = titleL10nId; + this.askForDefaultChoice = askForDefaultChoice; } ActionMenu.prototype = { @@ -52,6 +53,23 @@ this.container.appendChild(this.header); + // And a default choice button if asked + if (this.askForDefaultChoice) { + this.defaultChoice = document.createElement('label'); + this.defaultChoice.setAttribute('class', 'pack-checkbox'); + this.defaultChoice.setAttribute('data-action', 'set-default-action'); + + this.defaultChoiceInput = document.createElement('input'); + this.defaultChoiceInput.setAttribute('type', 'checkbox'); + + this.defaultChoiceSpan = document.createElement('span'); + this.defaultChoiceSpan.setAttribute('data-l10n-id', + 'set-default-action'); + + this.defaultChoice.appendChild(this.defaultChoiceInput); + this.defaultChoice.appendChild(this.defaultChoiceSpan); + } + // Following our paradigm we need a cancel this.cancel = document.createElement('button'); this.cancel.setAttribute('data-l10n-id', 'cancel'); @@ -104,6 +122,19 @@ } }, + /** + * This changes the input to be checked or unchecked + * @memberof ActionMenu.prototype + */ + toggleSetDefaultAction: function() { + var checked = this.defaultChoiceInput.checked; + if (checked) { + this.defaultChoiceInput.removeAttribute('checked'); + } else { + this.defaultChoiceInput.setAttribute('checked', true); + } + }, + /** * Builds the dom for the menu. * @memberof ActionMenu.prototype @@ -113,6 +144,7 @@ items.forEach(function traveseItems(item) { var action = document.createElement('button'); action.dataset.value = item.value; + action.dataset.manifest = item.manifest; action.textContent = item.label; if (item.icon) { @@ -121,6 +153,13 @@ } this.menu.appendChild(action); }, this); + + if (this.askForDefaultChoice) { + this.defaultChoiceSpan.setAttribute('data-l10n-id', + 'set-default-action'); + this.menu.appendChild(this.defaultChoice); + } + this.menu.appendChild(this.cancel); }, @@ -173,10 +212,22 @@ case 'click': evt.preventDefault(); var action = target.dataset.action; - if (action && action === 'cancel') { - this.hide(); - this.oncancel(); - return; + if (action) { + switch (action) { + case 'cancel': + this.hide(); + this.oncancel(); + break; + case 'set-default-action': + this.toggleSetDefaultAction(); + break; + } + return; + } + + var defaultSelected = false; + if (this.askForDefaultChoice) { + defaultSelected = !!this.defaultChoiceInput.getAttribute('checked'); } var value = target.dataset.value; @@ -184,7 +235,7 @@ return; } value = parseInt(value); - this.hide(this.onselected.bind(this, value)); + this.hide(this.onselected.bind(this, value, defaultSelected)); break; case 'home': diff --git a/apps/system/js/activities.js b/apps/system/js/activities.js index fa2c05550a2f..c336d6a38985 100644 --- a/apps/system/js/activities.js +++ b/apps/system/js/activities.js @@ -1,5 +1,5 @@ 'use strict'; -/* global ActionMenu, applications, ManifestHelper */ +/* global ActionMenu, applications, ManifestHelper, DefaultActivityHelper */ (function(exports) { @@ -12,6 +12,7 @@ function Activities() { window.addEventListener('mozChromeEvent', this); window.addEventListener('appopened', this); + window.addEventListener('applicationinstall', this); this.actionMenu = null; } @@ -39,9 +40,64 @@ this.actionMenu.hide(); } break; + case 'applicationinstall': + this._onNewAppInstalled(evt.detail.application); + break; } }, + _onNewAppInstalled: function(app) { + var activities = app && app.manifest && app.manifest.activities; + + Object.keys(activities).forEach(function(activity) { + var filters = activities[activity].filters; + + // Type can be single value, array, or a definition object (with value) + var type = filters && filters.type && filters.type.value || + filters && filters.type; + + if (!type) { + return; + } + + if (typeof type === 'string') { + // single value, change to Array + type = type.split(','); + } + + // Now that it's an array, we check all the elements + type.forEach((diff) => { + DefaultActivityHelper.getDefaultAction(activity, diff) + .then((defApp) => { + // If default launch app is set for the type + if (defApp) { + // Delete the current default app + DefaultActivityHelper.setDefaultAction(activity, diff, null); + } + }); + }); + }); + }, + + /** + * This gets the index from the defaultChoice in the choices + * list. + * @param {Object} defaultChoice The default choice + * @return {Integer} The index where the default + * choice is located. -1 if it + * is not found + */ + _choiceFromDefaultAction: function(defaultChoiceManifest, detail) { + var index = -1; + if (defaultChoiceManifest) { + index = detail.choices.findIndex(function(choice) { + return choice.manifest.indexOf(defaultChoiceManifest) !== -1; + }); + } + + return index; + }, + /** * Displays the activity menu if needed. * If there is only one option, the activity is automatically launched. @@ -49,10 +105,21 @@ * @param {Object} detail The activity choose event detail. */ chooseActivity: function(detail) { - this._id = detail.id; - var choices = detail.choices; + var name = detail && detail.name; + var type = detail && detail.activityType; + this._detail = detail; this.publish('activityrequesting'); - if (choices.length === 1) { + + DefaultActivityHelper.getDefaultAction(name, type).then( + this._gotDefaultAction.bind(this)); + }, + + _gotDefaultAction: function(defaultChoice) { + var choices = this._detail.choices; + var index = this._choiceFromDefaultAction(defaultChoice, this._detail); + if (index > -1) { + this.choose(index); + } else if (choices.length === 1) { this.choose('0'); } else { // @@ -75,7 +142,7 @@ // activity, but it is much simpler to restrict to the FL app // only. // - if (detail.name === 'view') { + if (this._detail.name === 'view') { var flAppIndex = choices.findIndex(function(choice) { var matchingRegex = /^(http|https|app)\:\/\/fl\.gaiamobile\.org\//; @@ -95,11 +162,21 @@ // shows window.dispatchEvent(new CustomEvent('activitymenuwillopen')); - var activityNameL10nId = 'activity-' + detail.name; + var name = this._detail.name; + var type = this._detail.activityType; + var config = DefaultActivityHelper.getDefaultConfig(name, type); + + var activityNameL10nId; + if (config) { + activityNameL10nId = config.l10nId; + } else { + activityNameL10nId = 'activity-' + this._detail.name; + } + if (!this.actionMenu) { this.actionMenu = new ActionMenu(this._listItems(choices), activityNameL10nId, this.choose.bind(this), - this.cancel.bind(this)); + this.cancel.bind(this), null, config !== undefined); this.actionMenu.start(); } }).bind(this)); @@ -110,18 +187,27 @@ * The user chooses an activity from the activity menu. * @memberof Activities.prototype * @param {Number} choice The activity choice. + * @param {Boolean} setAsDefault Should this be set as the default activity. */ - choose: function(choice) { + choose: function(choice, setAsDefault) { this.actionMenu = null; var returnedChoice = { - id: this._id, + id: this._detail.id, type: 'activity-choice', - value: choice + value: choice, + setAsDefault: setAsDefault }; + var name = this._detail.name; + var type = this._detail.activityType; + + if (setAsDefault) { + DefaultActivityHelper.setDefaultAction(name, type, + this._detail.choices[choice].manifest); + } this._sendEvent(returnedChoice); - delete this._id; + delete this._detail; }, /** @@ -132,13 +218,13 @@ this.actionMenu = null; var returnedChoice = { - id: this._id, + id: this._detail.id, type: 'activity-choice', value: -1 }; this._sendEvent(returnedChoice); - delete this._id; + delete this._detail; }, publish: function(eventName) { @@ -176,6 +262,7 @@ items.push({ label: new ManifestHelper(app.manifest).name, icon: choice.icon, + manifest: choice.manifest, value: index }); }); diff --git a/apps/system/locales/system.en-US.properties b/apps/system/locales/system.en-US.properties index bd749b11ef97..4e37c689b938 100644 --- a/apps/system/locales/system.en-US.properties +++ b/apps/system/locales/system.en-US.properties @@ -467,6 +467,7 @@ activity-record=Record from: activity-browse=Browse with: activity-configure=Configure with: activity-dial=Dial from: +set-default-action=Use from now on # Persona dialog and Identity persona-signin=Sign In diff --git a/apps/system/style/action_menu/action_menu_extended.css b/apps/system/style/action_menu/action_menu_extended.css index fa19fe677f9a..848f09aa4cec 100644 --- a/apps/system/style/action_menu/action_menu_extended.css +++ b/apps/system/style/action_menu/action_menu_extended.css @@ -57,6 +57,29 @@ form[role="dialog"][data-type="object"].visible { transform: translateY(0); } +/* Extending action menu to display checkbox */ +form[role="dialog"][data-type="action"] > menu > .pack-checkbox { + margin: -1rem 0 0 1.5rem; + padding: 0 1.2rem; + width: auto; +} + +form[role="dialog"][data-type="action"] > menu > .pack-checkbox span { + font-size: 1.6rem; + font-weight: 300; + font-style: italic; + vertical-align: middle; + padding-top: 5rem; + padding-left: 3rem; + pointer-events: none; + +} + +form[role="dialog"][data-type="action"] > menu > .pack-checkbox span:after { + left: 0; + margin-left: 0.9rem; +} + @media (min-width: 768px) { form[role="dialog"][data-type="action"], form[role="dialog"][data-type="object"] { diff --git a/apps/system/test/apps/activitycallee/defaulttest.html b/apps/system/test/apps/activitycallee/defaulttest.html new file mode 100644 index 000000000000..003f41d20768 --- /dev/null +++ b/apps/system/test/apps/activitycallee/defaulttest.html @@ -0,0 +1,27 @@ + + + + + + Fake Camera + + + + + + +

Default activity app

+ + + diff --git a/apps/system/test/apps/activitycallee/manifest.webapp b/apps/system/test/apps/activitycallee/manifest.webapp index e71b4cdea699..62b4a34b7ef5 100644 --- a/apps/system/test/apps/activitycallee/manifest.webapp +++ b/apps/system/test/apps/activitycallee/manifest.webapp @@ -18,6 +18,16 @@ "disposition": "inline", "href": "chain.html", "returnValue": true + }, + "view": { + "disposition": "inline", + "href": "defaulttest.html", + "filters": { + "type": { + "required": true, + "value": ["url"] + } + } } } } diff --git a/apps/system/test/apps/activitycaller/index.html b/apps/system/test/apps/activitycaller/index.html index 59e235893c34..963c1cec6a57 100644 --- a/apps/system/test/apps/activitycaller/index.html +++ b/apps/system/test/apps/activitycaller/index.html @@ -25,6 +25,7 @@

I am a fake activity caller.

+ diff --git a/apps/system/test/apps/activitycaller/js/caller.js b/apps/system/test/apps/activitycaller/js/caller.js index f2bf13ab1dd0..3eb00894b9fc 100644 --- a/apps/system/test/apps/activitycaller/js/caller.js +++ b/apps/system/test/apps/activitycaller/js/caller.js @@ -16,6 +16,17 @@ document.getElementById('testchainactivity') }); }); +document.getElementById('testdefaultactivity') + .addEventListener('click', function() { + new MozActivity({ + name: 'view', + data: { + type: 'url', + url: 'http://www.google.com' + } + }); + }); + document.getElementById('close').addEventListener('click', function() { window.close(); }); diff --git a/apps/system/test/apps/fakeinstalledapp/index.html b/apps/system/test/apps/fakeinstalledapp/index.html new file mode 100644 index 000000000000..4da7ef6d393b --- /dev/null +++ b/apps/system/test/apps/fakeinstalledapp/index.html @@ -0,0 +1,10 @@ + + + + + JustInstalledApp + + +
+ + diff --git a/apps/system/test/apps/fakeinstalledapp/manifest.webapp b/apps/system/test/apps/fakeinstalledapp/manifest.webapp new file mode 100644 index 000000000000..12168b9d84bc --- /dev/null +++ b/apps/system/test/apps/fakeinstalledapp/manifest.webapp @@ -0,0 +1,22 @@ +{ + "name": "Anything", + "description": "Fake app to install after default launch set", + "developer": { + "name": "The Gaia Team", + "url": "https://github.com/mozilla-b2g/gaia" + }, + "launch_path": "/index.html", + "fullscreen": true, + "activities": { + "view": { + "disposition": "inline", + "href": "index.html", + "filters": { + "type": { + "required": true, + "value": ["url"] + } + } + } + } +} diff --git a/apps/system/test/marionette/default_activity_test.js b/apps/system/test/marionette/default_activity_test.js new file mode 100644 index 000000000000..db3ccda3d611 --- /dev/null +++ b/apps/system/test/marionette/default_activity_test.js @@ -0,0 +1,192 @@ +/* global __dirname */ +'use strict'; + +(function() { + var Server = require('../../../../shared/test/integration/server'); + var AppInstall = require('./lib/app_install'); + var assert = require('assert'); + + var CALLER_APP = 'app://activitycaller.gaiamobile.org'; + var CALLEE_APP = 'app://activitycallee.gaiamobile.org'; + + var appInstall, + system, + server; + + var setDefaultSelector = '[data-action=set-default-action]'; + + marionette('Triggering activity shows Set As Default option >', function() { + var client = marionette.client({ + settings: { + 'ftu.manifestURL': null, + 'lockscreen.enabled': false + }, + apps: { + 'activitycaller.gaiamobile.org': __dirname + '/../apps/activitycaller', + 'activitycallee.gaiamobile.org': __dirname + '/../apps/activitycallee' + } + }); + + function getDisplayAppOrigin() { + return client.executeScript(function() { + var app = window.wrappedJSObject.Service.currentApp; + return app.getTopMostWindow().origin; + }); + } + + suiteSetup(function(done) { + var appToInstall = __dirname + '/../apps/fakeinstalledapp/'; + Server.create(appToInstall, function(err, _server) { + server = _server; + done(); + }); + }); + + suiteTeardown(function() { + server.stop(); + }); + + setup(function() { + appInstall = new AppInstall(client); + system = client.loader.getAppClass('system'); + system.waitForStartup(); + }); + + test('Default Activity chosen >', function() { + client.apps.launch(CALLER_APP); + + // Try to launch CALLEE from CALLER + client.apps.switchToApp(CALLER_APP); + client.findElement('#testdefaultactivity').click(); + client.switchToFrame(); + + // Check that app choice appear, showing default activity checkbox + var checkbox = client.findElement(setDefaultSelector); + assert.ok(checkbox.displayed); + + // Activate 'Use as default' for the future + checkbox.click(); + + // Launch CALLEE from the selection + var selector = '[data-manifest="' + CALLEE_APP + '/manifest.webapp"]'; + client.findElement(selector).click(); + + client.switchToFrame(); + + // Check that CALLEE is open + assert.equal(getDisplayAppOrigin(), CALLEE_APP); + client.apps.switchToApp(CALLEE_APP); + assert.ok(client.findElement('#default-test').displayed()); + + client.apps.close(CALLEE_APP); + + // Try to launch CALLEE from CALLER again + client.apps.switchToApp(CALLER_APP); + client.findElement('#testdefaultactivity').click(); + client.switchToFrame(); + + // the app choice doesn't appear this time + client.findElement(setDefaultSelector, function(err, element) { + assert.equal(err.name, 'NoSuchElement', 'element not found'); + }); + + // Check that CALLEE is open + client.apps.switchToApp(CALLEE_APP); + assert.ok(client.findElement('#default-test').displayed()); + }); + + test('Default Activity ignored >', function() { + client.apps.launch(CALLER_APP); + + // Try to launch CALLEE from CALLER + client.apps.switchToApp(CALLER_APP); + client.findElement('#testdefaultactivity').click(); + client.switchToFrame(); + + // Check that app choice appear, showing default activity checkbox + var checkbox = client.findElement(setDefaultSelector); + assert.ok(checkbox.displayed); + + // Don't activate 'Use as default' + + // Launch CALLEE from the selection + var selector = '[data-manifest="' + CALLEE_APP + '/manifest.webapp"]'; + client.findElement(selector).click(); + + // Check that CALLEE is open + client.apps.switchToApp(CALLEE_APP); + assert.ok(client.findElement('#default-test').displayed()); + + client.apps.close(CALLEE_APP); + client.switchToFrame(); + + // Try to launch CALLEE from CALLER again + client.apps.switchToApp(CALLER_APP); + client.findElement('#testdefaultactivity').click(); + client.switchToFrame(); + + // the app choice appears again + checkbox = client.findElement(setDefaultSelector); + assert.ok(checkbox.displayed); + }); + + test('Reset after new app is installed >', function() { + client.apps.launch(CALLER_APP); + + // Try to launch CALLEE from CALLER + client.apps.switchToApp(CALLER_APP); + client.findElement('#testdefaultactivity').click(); + client.switchToFrame(); + + // Check that app choice appear, showing default activity checkbox + var checkbox = client.findElement(setDefaultSelector); + assert.ok(checkbox.displayed); + + // Activate 'Use as default' for the future + checkbox.click(); + + // Launch CALLEE from the selection + var selector = '[data-manifest="' + CALLEE_APP + '/manifest.webapp"]'; + client.findElement(selector).click(); + + client.switchToFrame(); + + // Check that CALLEE is open + assert.equal(getDisplayAppOrigin(), CALLEE_APP); + client.apps.switchToApp(CALLEE_APP); + assert.ok(client.findElement('#default-test').displayed()); + + client.apps.close(CALLEE_APP); + + // Try to launch CALLEE from CALLER again + client.apps.switchToApp(CALLER_APP); + client.findElement('#testdefaultactivity').click(); + client.switchToFrame(); + + // the app choice doesn't appear this time + client.findElement(setDefaultSelector, function(err, element) { + assert.equal(err.name, 'NoSuchElement', 'element not found'); + }); + + // Check that CALLEE is open + client.apps.switchToApp(CALLEE_APP); + assert.ok(client.findElement('#default-test').displayed()); + + client.apps.close(CALLEE_APP); + client.apps.close(CALLER_APP); + // Now we install the new app (with same activity receiver) + var serverManifestURL = server.url('manifest.webapp'); + appInstall.install(serverManifestURL); + + client.apps.launch(CALLER_APP); + client.apps.switchToApp(CALLER_APP); + client.findElement('#testdefaultactivity').click(); + client.switchToFrame(); + + // Check that app choice appears again + checkbox = client.findElement(setDefaultSelector); + assert.ok(checkbox.displayed); + }); + }); +}()); + diff --git a/apps/system/test/unit/action_menu_test.js b/apps/system/test/unit/action_menu_test.js index 2c51094139c3..08c97d3adc5d 100644 --- a/apps/system/test/unit/action_menu_test.js +++ b/apps/system/test/unit/action_menu_test.js @@ -14,6 +14,14 @@ suite('ActionMenu', function() { return screenElement.querySelector('[data-type="action"]'); } + function resetHTML() { + document.body.innerHTML = ''; + loadBodyHTML('/index.html'); + screenElement = document.getElementById('screen'); + // reload too in case is on memory + getMenu(); + } + suiteSetup(function() { activitiesMockup = [ { @@ -46,8 +54,8 @@ suite('ActionMenu', function() { realL10n = navigator.mozL10n; navigator.mozL10n = MockL10n; - loadBodyHTML('/index.html'); - screenElement = document.getElementById('screen'); + + resetHTML(); }); suiteTeardown(function() { @@ -55,8 +63,11 @@ suite('ActionMenu', function() { document.body.innerHTML = ''; }); - suite(' > Structure & Basic methods', function() { + setup(function() { + resetHTML(); + }); + test(' > init', function() { // We must have *only* one action menu in system var menu = new ActionMenu(genericActionsMockup, title); @@ -140,7 +151,6 @@ suite('ActionMenu', function() { menu.stop(); assert.isFalse(screenElement.classList.contains('action-menu')); }); - }); suite(' > handleEvent', function() { @@ -148,6 +158,7 @@ suite('ActionMenu', function() { var menu; setup(function() { + resetHTML(); menu = new ActionMenu(activitiesMockup, title); menu.start(); this.sinon.spy(menu, 'handleEvent'); @@ -179,6 +190,7 @@ suite('ActionMenu', function() { var menu; setup(function() { + resetHTML(); successCBStub = this.sinon.spy(); cancelCBStub = this.sinon.spy(); menu = new ActionMenu( @@ -216,6 +228,10 @@ suite('ActionMenu', function() { }); suite('preventFocusChange', function() { + setup(function() { + resetHTML(); + }); + test('focus is not changed when specified', function() { var menu = new ActionMenu(genericActionsMockup, title, null, null, true); this.sinon.spy(menu, 'preventFocusChange'); @@ -225,4 +241,21 @@ suite('ActionMenu', function() { assert.isTrue(menu.preventFocusChange.called); }); }); + + suite('askForDefaultChoice option', function() { + var menu; + + setup(function() { + resetHTML(); + menu = new ActionMenu( + genericActionsMockup, title, null, null, null, true); + menu.start(); + this.sinon.spy(menu, 'hide'); + }); + + test('checkbox is created', function() { + var checkbox = getMenu().querySelectorAll('.pack-checkbox'); + assert.equal(checkbox.length, 1); + }); + }); }); diff --git a/apps/system/test/unit/activities_test.js b/apps/system/test/unit/activities_test.js index 6c43297571fd..ac50d332761f 100644 --- a/apps/system/test/unit/activities_test.js +++ b/apps/system/test/unit/activities_test.js @@ -1,21 +1,23 @@ 'use strict'; -/* global MocksHelper, MockApplications, MockL10n, ActionMenu, Activities */ +/* global MocksHelper, MockApplications, MockL10n, MockDefaultActivityHelper, + ActionMenu, Activities, DefaultActivityHelper +*/ -requireApp('system/test/unit/mock_applications.js'); -requireApp('system/shared/test/unit/mocks/mock_settings_listener.js'); require('/shared/test/unit/mocks/mock_l10n.js'); -requireApp('system/js/action_menu.js'); +require('/shared/test/unit/mocks/mock_default_activity_helper.js'); +requireApp('system/test/unit/mock_applications.js'); requireApp('system/shared/js/manifest_helper.js'); +requireApp('system/js/action_menu.js'); requireApp('system/js/activities.js'); + var mocksForActivities = new MocksHelper([ 'Applications' ]).init(); suite('system/Activities', function() { var realL10n; + var realDefaultActivityHelper; var subject; - var stubById; - var fakeElement; var realApplications; var fakeLaunchConfig1 = { @@ -31,40 +33,35 @@ suite('system/Activities', function() { }; mocksForActivities.attachTestHelpers(); + suiteSetup(function() { realL10n = navigator.mozL10n; navigator.mozL10n = MockL10n; realApplications = window.applications; + realDefaultActivityHelper = window.DefaultActivityHelper; window.applications = MockApplications; + window.DefaultActivityHelper = MockDefaultActivityHelper; }); suiteTeardown(function() { navigator.mozL10n = realL10n; window.applications = realApplications; + window.DefaultActivityHelper = realDefaultActivityHelper; realApplications = null; }); setup(function() { this.sinon.useFakeTimers(); - - fakeElement = document.createElement('div'); - fakeElement.style.cssText = 'height: 100px; display: block;'; - stubById = this.sinon.stub(document, 'getElementById') - .returns(fakeElement.cloneNode(true)); - - subject = new Activities(); - }); - - teardown(function() { - this.sinon.clock.restore(); - stubById.restore(); }); suite('constructor', function() { test('adds event listeners', function() { - var addEventStub = this.sinon.stub(window, 'addEventListener'); + this.sinon.stub(window, 'addEventListener'); subject = new Activities(); - assert.ok(addEventStub.withArgs('mozChromeEvent').calledOnce); + assert.ok(window.addEventListener.withArgs('mozChromeEvent').calledOnce); + assert.ok(window.addEventListener.withArgs('appopened').calledOnce); + assert.ok(window.addEventListener + .withArgs('applicationinstall').calledOnce); }); }); @@ -82,16 +79,25 @@ suite('system/Activities', function() { }); test('hides actionMenu on appopended if it exists', function() { - var stub = this.sinon.stub(ActionMenu.prototype, 'hide'); + this.sinon.stub(ActionMenu.prototype, 'hide'); subject.actionMenu = new ActionMenu(); subject.handleEvent({type: 'appopened'}); - assert.ok(stub.calledOnce); + assert.ok(ActionMenu.prototype.hide.calledOnce); }); }); - suite('chooseActivity', function() { - test('chooses with 1 item', function() { - var stub = this.sinon.stub(subject, 'choose'); + suite('activity (multi)selection', function() { + setup(function() { + this.sinon.stub(DefaultActivityHelper, 'getDefaultAction') + .returns({ // instead of Promise.resolve() to return in same cycle + then: function(cb) { cb(null); } + }); + }); + + test('choice when only one item', function() { + var stub = this.sinon.stub(subject, 'choose', function(obj) { + console.log('called with ' + obj); + }); subject.chooseActivity({ id: 'single', choices: ['first'] @@ -99,19 +105,37 @@ suite('system/Activities', function() { assert.ok(stub.calledWith('0')); }); - test('opens action menu with multiple items', function() { - var dispatchStub = this.sinon.stub(window, 'dispatchEvent'); + test('opens action menu with multiple choice', function() { + this.sinon.stub(ActionMenu.prototype, 'start'); + this.sinon.stub(window, 'dispatchEvent'); subject.chooseActivity({ id: 'single', choices: ['first', 'second'] }); this.sinon.clock.tick(); - assert.equal(dispatchStub.getCall(0).args[0].type, + assert.equal(window.dispatchEvent.getCall(0).args[0].type, 'activityrequesting'); - assert.equal(dispatchStub.getCall(1).args[0].type, + assert.equal(window.dispatchEvent.getCall(1).args[0].type, 'activitymenuwillopen'); }); + test('only opens once if we get two activity-choice events', function() { + subject.actionMenu = null; + this.sinon.stub(ActionMenu.prototype, 'start'); + var evt = { + type: 'mozChromeEvent', + detail: { + type: 'activity-choice', + choices: ['first', 'second'] + } + }; + subject.handleEvent(evt); + this.sinon.clock.tick(); + subject.handleEvent(evt); + this.sinon.clock.tick(); + assert.ok(ActionMenu.prototype.start.calledOnce); + }); + test('does not allow a choice that would subvert forward lock', function() { var stub = this.sinon.stub(subject, 'choose'); var dispatchStub = this.sinon.stub(window, 'dispatchEvent'); @@ -135,7 +159,7 @@ suite('system/Activities', function() { 'activitymenuwillopen'); }); - test('does not allow another choice that would subvert forward lock', + test('does not allow another choice that would subvert forward lock', function() { var stub = this.sinon.stub(subject, 'choose'); var dispatchStub = this.sinon.stub(window, 'dispatchEvent'); @@ -198,33 +222,204 @@ suite('system/Activities', function() { assert.equal(stub.secondCall.args[0].type, 'activitymenuwillopen'); }); - }); + test('checks for default app on the list', function() { + var activity = { + id: 'single', + name: 'view', + activityType: 'image/*', + choices: [{ + manifest: 'app://gallery.gaiamobile.org/manifest.webapp' + },{ + manifest: 'app://camera.gaiamobile.org/manifest.webapp' + }] + }; + + subject.chooseActivity(activity); + assert.ok(DefaultActivityHelper.getDefaultAction.calledWith( + activity.name, activity.activityType)); + }); + + test('if not on the list, ignore', function() { + this.sinon.spy(subject, '_gotDefaultAction'); - suite('choose', function() { - test('calls _sendEvent', function() { - subject._id = 'foo'; - var stub = this.sinon.stub(subject, '_sendEvent'); - var formatted = { - id: 'foo', - type: 'activity-choice', - value: 0 + var activity = { + id: 'single', + name: 'pick', + activityType: 'image/*', + choices: [{ + manifest: 'app://gallery.gaiamobile.org/manifest.webapp' + },{ + manifest: 'app://camera.gaiamobile.org/manifest.webapp' + }] + }; + + subject.chooseActivity(activity); + assert.ok(DefaultActivityHelper.getDefaultAction.calledWith( + activity.name, activity.activityType)); + assert.ok(subject._gotDefaultAction.calledWith(null)); + }); + + test('if on the list, check for default launch associated', function() { + DefaultActivityHelper.getDefaultAction + .returns({ + then: function(cb) { cb('fakeManifest'); } + }); + this.sinon.spy(subject, '_gotDefaultAction'); + + var activity = { + id: 'single', + name: 'pick', + activityType: 'image/*', + choices: [{ + manifest: 'app://gallery.gaiamobile.org/manifest.webapp' + },{ + manifest: 'app://camera.gaiamobile.org/manifest.webapp' + }] }; - subject.choose(0); - assert.ok(stub.calledWith(formatted)); + + subject.chooseActivity(activity); + assert.ok(DefaultActivityHelper.getDefaultAction.calledWith( + activity.name, activity.activityType)); + assert.ok(subject._gotDefaultAction.calledWith('fakeManifest')); + }); + + suite('choosing an activity ', function() { + var sendEvent, + defaultAct, + formatted; + + setup(function() { + subject._detail = { + id: 'foo', + name: 'testactivity', + activityType: 'testtype', + choices: [{ + manifest: 'manifest' + }] + }; + + formatted = { + id: 'foo', + type: 'activity-choice', + value: 0, + setAsDefault: false + }; + + sendEvent = this.sinon.stub(subject, '_sendEvent'); + defaultAct = this.sinon.stub(DefaultActivityHelper, 'setDefaultAction'); + }); + + test('without default activity set >', function() { + var set_default = false; + formatted.setAsDefault = set_default; + subject.choose(0, set_default); + + assert.ok(sendEvent.calledWith(formatted), + 'calls _sendEvent WITHOUT default activity set'); + assert.isFalse(defaultAct.called, 'should not call the helper'); + }); + + test('with a default activity set >', function() { + var set_default = true; + formatted.setAsDefault = set_default; + subject.choose(0, set_default); + + assert.ok(sendEvent.calledWith(formatted), + 'calls _sendEvent WITH default activity set'); + assert.ok(defaultAct.calledWith('testactivity', 'testtype', 'manifest'), + 'should call the helper with the proper values'); + }); + }); + + suite('cancel selection', function() { + test('calls _sendEvent', function() { + subject._detail = { + id: 'foo' + }; + var stub = this.sinon.stub(subject, '_sendEvent'); + var formatted = { + id: 'foo', + type: 'activity-choice', + value: -1 + }; + subject.cancel(); + assert.ok(stub.calledWith(formatted)); + }); }); }); - suite('cancel', function() { - test('calls _sendEvent', function() { - subject._id = 'foo'; - var stub = this.sinon.stub(subject, '_sendEvent'); - var formatted = { - id: 'foo', - type: 'activity-choice', - value: -1 + suite('when a new app is installed', function() { + var app, + listedName, + listedType; + + setup(function() { + this.sinon.stub(DefaultActivityHelper, 'getDefaultAction'); + this.sinon.stub(DefaultActivityHelper, 'setDefaultAction'); + + app = { + 'manifest': { + 'activities': { + 'browse': { + 'filters': { + 'type': 'photos' + } + }, + 'pick': { + 'filters': { + 'type': ['image/*'] + } + }, + 'open': { + 'filters': { + 'type': ['video/*'] + } + } + } + } }; - subject.cancel(); - assert.ok(stub.calledWith(formatted)); + + listedName = 'pick'; + listedType = 'image/*'; + + subject = new Activities(); + }); + + test('manages the default launch for the app\'s activities', function() { + DefaultActivityHelper.getDefaultAction + .returns({ // instead of Promise.resolve() to return in same cycle + then: function(cb) { + cb(null); + } + }) + .withArgs(listedName, listedType).returns({ + then: function(cb) { + cb('app://fakeapp1.gaiamobile.org/manifest.webapp'); + } + }); + + window.dispatchEvent(new CustomEvent('applicationinstall',{ + detail: { + application: app + } + })); + + assert.ok(DefaultActivityHelper.getDefaultAction + .calledWith('browse', 'photos'), + 'check the list for the first activity'); + assert.ok(DefaultActivityHelper.getDefaultAction + .calledWith('pick', 'image/*'), + 'check the list for the second activity'); + assert.ok(DefaultActivityHelper.getDefaultAction + .calledWith('open', 'video/*'), + 'check the list for the third activity'); + + assert.ok(DefaultActivityHelper.setDefaultAction + .calledWith(listedName, listedType, null), + 'removes the default app when the activity has it associated'); + + assert.ok(DefaultActivityHelper.setDefaultAction.calledOnce, + 'not called for any other activity'); }); }); @@ -254,22 +449,4 @@ suite('system/Activities', function() { assert.equal(result.length, 1); }); }); - - suite('opens action menu', function() { - test('only opens once if we get two activity-choice events', function() { - var actionMenuStub = this.sinon.stub(ActionMenu.prototype, 'start'); - var evt = { - type: 'mozChromeEvent', - detail: { - type: 'activity-choice', - choices: [] - } - }; - subject.handleEvent(evt); - this.sinon.clock.tick(); - subject.handleEvent(evt); - this.sinon.clock.tick(); - assert.ok(actionMenuStub.calledOnce); - }); - }); }); diff --git a/shared/js/default_activity_helper.js b/shared/js/default_activity_helper.js new file mode 100644 index 000000000000..4d81545469d1 --- /dev/null +++ b/shared/js/default_activity_helper.js @@ -0,0 +1,169 @@ +/* global SettingsHelper */ +/* exported DefaultActivityHelper */ + +(function(exports) { + 'use strict'; + + // To add an element on the list, follow the example: + // { + // name: 'pick', + // type: ['image/jpeg', + // 'image/png', + // 'image/gif', + // 'image/bmp'], + // l10nId: 'default-activity-pickimage', + // settingsId: 'activity.default.pickimage' + // }, + // Note: list of supported activities on bug1039386 + var supportedActivities = [ + { + name: 'dial', + type: ['webtelephony/number'], + l10nId: 'default-activity-makecall', + settingsId: 'activity.default.makecall' + }, + { + name: 'new', + type: ['webcontacts/contact'], + l10nId: 'default-activity-opencontact', + settingsId: 'activity.default.opencontact' + }, + { + name: 'new', + type: ['mail'], + l10nId: 'default-activity-sendmail', + settingsId: 'activity.default.sendmail' + }, + { + name: 'new', + type: ['websms/sms'], + l10nId: 'default-activity-sendmessage', + settingsId: 'activity.default.sendmessage' + }, + { + name: 'open', + type: ['webcontacts/contact'], + l10nId: 'default-activity-opencontact', + settingsId: 'activity.default.opencontact' + }, + { + name: 'open', + type: ['image/jpeg', + 'image/png', + 'image/gif', + 'image/bmp'], + l10nId: 'default-activity-openimage', + settingsId: 'activity.default.openimage' + }, + { + name: 'open', + type: ['audio/mpeg', + 'audio/ogg', + 'audio/mp4'], + l10nId: 'default-activity-openaudio', + settingsId: 'activity.default.openaudio' + }, + { + name: 'open', + type: ['video/webm', + 'video/mp4', + 'video/3gpp', + 'video/youtube'], + l10nId: 'default-activity-openvideo', + settingsId: 'activity.default.openvideo' + }, + { + name: 'record', + type: ['photos'], + l10nId: 'default-activity-takephoto', + settingsId: 'activity.default.takephoto' + }, + { + name: 'record', + type: ['videos'], + l10nId: 'default-activity-takevideo', + settingsId: 'activity.default.takevideo' + }, + { + name: 'view', + type: ['url'], + l10nId: 'default-activity-openurl', + settingsId: 'activity.default.openurl' + }, + { + name: 'view', + type: ['application/pdf'], + l10nId: 'default-activity-openpdf', + settingsId: 'activity.default.openpdf' + }, + { + name: 'view', + type: ['video/webm', + 'video/mp4', + 'video/3gpp', + 'video/youtube'], + l10nId: 'default-activity-openvideo', + settingsId: 'activity.default.openvideo' + }, + { + name: 'update', + type: ['webcontacts/contact'], + l10nId: 'default-activity-opencontact', + settingsId: 'activity.default.opencontact' + } + ]; + + var DefaultActivityHelper = { + + /** + * Looks for an activity on the list given the name and the type + * @parameter {String} name Activity name + * @parameter {String} type Activity type + * @returns {Object} Config object for the activity + */ + getDefaultConfig: function(name, type) { + var config = supportedActivities.find(function(a) { + return a.name === name && a.type.indexOf(type) >= 0; + }); + + return config; + }, + + /** + * Returns the current choice as default for a given activity name and type + * @param {String} name Activity name + * @param {String} type Activity type + * @param {Function} cb callback executed when search finished + */ + getDefaultAction: function(name, type) { + var config = this.getDefaultConfig(name, type); + return new Promise((resolve, reject) => { + if (!config) { + resolve(null); + } + + SettingsHelper(config.settingsId, null).get((value) => { + resolve(value); + }); + }); + }, + + /** + * Sets a preference with the default choice for a given activity and type + * @param {String} name Activity name + * @param {String} type Activity type + * @param {String} choice App manifest of the chosen app + */ + setDefaultAction: function(name, type, choice) { + var config = this.getDefaultConfig(name, type); + if (!config) { + return; + } + + SettingsHelper(config.settingsId, null).set(choice); + } + }; + + exports.DefaultActivityHelper = DefaultActivityHelper; + +})(window); diff --git a/shared/locales/activities/activities.en-US.properties b/shared/locales/activities/activities.en-US.properties new file mode 100644 index 000000000000..c676322a6646 --- /dev/null +++ b/shared/locales/activities/activities.en-US.properties @@ -0,0 +1,31 @@ +# Default activities label + +# PICK +default-activity-pickimage = Take picture + +# DIAL +default-activity-makecall = Make call + +# NEW +default-activity-opencontact = Open contact +default-activity-sendmail = Send email +default-activity-sendmessage = Send message + +# OPEN +default-activity-opencontact = Open contact +default-activity-openimage = Open image +default-activity-openaudio = Open audio +default-activity-openvideo = Open video + +# RECORD +default-activity-takephoto = Take photo +default-activity-takevideo = Record video + +# VIEW +default-activity-openurl = Open link +default-activity-openpdf = Open PDF file +default-activity-openvideo = Open video + +# UPDATE +default-activity-opencontact = Open contact + diff --git a/shared/test/unit/mocks/mock_default_activity_helper.js b/shared/test/unit/mocks/mock_default_activity_helper.js new file mode 100644 index 000000000000..efee8a16e740 --- /dev/null +++ b/shared/test/unit/mocks/mock_default_activity_helper.js @@ -0,0 +1,30 @@ +'use strict'; + +window.MockDefaultActivityHelper = { + + getDefaultConfig: function(name, type) { + if (name === 'testactivity' && type === 'testtype') { + return { + name: 'testactivity', + type: ['testtype'] + }; + } else { + return undefined; + } + }, + + getDefaultAction: function(name, type) { + if (name === 'testactivity' && type === 'testtype') { + return { + 'aa': 'kk' + }; + } else { + return undefined; + } + }, + + setDefaultAction: function(name, type, choice) { + + } + +}; diff --git a/shared/test/unit/mocks/mock_navigator_moz_apps.js b/shared/test/unit/mocks/mock_navigator_moz_apps.js index b124938c73c8..8a9de5c0b19f 100644 --- a/shared/test/unit/mocks/mock_navigator_moz_apps.js +++ b/shared/test/unit/mocks/mock_navigator_moz_apps.js @@ -50,7 +50,10 @@ var MockNavigatormozApps = { } }; }, - uninstall: function() {} + uninstall: function() {}, + addEventListener: function() { + + } }, mLastRequest: null,