From 33f54971464be54f1b362a44c31ffedad115f8e7 Mon Sep 17 00:00:00 2001 From: gasolin Date: Mon, 31 Mar 2014 16:12:27 +0800 Subject: [PATCH] Bug 973445 - [settings] refactor Sound panel with AMD pattern - add tone tests --- apps/settings/README.md | 2 +- apps/settings/elements/sound.html | 3 +- apps/settings/js/config/require.js | 16 +- apps/settings/js/panels/sound/panel.js | 29 +++ apps/settings/js/panels/sound/sound.js | 186 ++++++++++++++++++ apps/settings/js/sound.js | 147 -------------- .../test/unit/mock_settings_listener.js | 13 ++ .../test/unit/panels/sound/sound_test.js | 183 +++++++++++++++++ .../mock_navigator_moz_mobile_connections.js | 4 + 9 files changed, 430 insertions(+), 153 deletions(-) create mode 100644 apps/settings/js/panels/sound/panel.js create mode 100644 apps/settings/js/panels/sound/sound.js delete mode 100644 apps/settings/js/sound.js create mode 100644 apps/settings/test/unit/mock_settings_listener.js create mode 100644 apps/settings/test/unit/panels/sound/sound_test.js diff --git a/apps/settings/README.md b/apps/settings/README.md index 1c61e419f752..2aaecc407c47 100644 --- a/apps/settings/README.md +++ b/apps/settings/README.md @@ -180,7 +180,7 @@ If the old module called Settings.mozSettings, Use SettingsCache.getString() ins ```js var SettingsCache = require('modules/settings_cache'); ... - SettingsCache.getSettings(function(result){ + SettingsCache.getSettings(function(result) { var onlineSupportTitle = result['support.onlinesupport.title']; ... }); diff --git a/apps/settings/elements/sound.html b/apps/settings/elements/sound.html index dce5410dd931..2fdb353a2421 100644 --- a/apps/settings/elements/sound.html +++ b/apps/settings/elements/sound.html @@ -106,8 +106,7 @@

Other Sounds

- - + diff --git a/apps/settings/js/config/require.js b/apps/settings/js/config/require.js index e4f93abe9141..996343cff328 100644 --- a/apps/settings/js/config/require.js +++ b/apps/settings/js/config/require.js @@ -9,15 +9,18 @@ require.config({ 'settings': { exports: 'Settings' }, + 'shared/keyboard_helper': { + exports: 'KeyboardHelper' + }, 'shared/lazy_loader': { exports: 'LazyLoader' }, + 'shared/omadrm/fl': { + exports: 'ForwardLock' + }, 'shared/screen_layout': { exports: 'ScreenLayout' }, - 'shared/keyboard_helper': { - exports: 'KeyboardHelper' - }, 'shared/settings_listener': { exports: 'SettingsListener' } @@ -44,6 +47,13 @@ require.config({ { name: 'panels/help/panel', exclude: ['main'] + }, + { + name: 'panels/sound/panel', + exclude: [ + 'main', + 'shared/settings_listener' + ] } ] }); diff --git a/apps/settings/js/panels/sound/panel.js b/apps/settings/js/panels/sound/panel.js new file mode 100644 index 000000000000..f7af54fd0a3e --- /dev/null +++ b/apps/settings/js/panels/sound/panel.js @@ -0,0 +1,29 @@ +/** + * Used to show Personalization/Sound panel + */ +define(function(require) { + 'use strict'; + + var SettingsPanel = require('modules/settings_panel'); + var Sound = require('panels/sound/sound'); + + return function ctor_sound_panel() { + var sound = Sound(); + + return SettingsPanel({ + onInit: function(panel) { + var elements = { + toneSelector: panel.querySelector('#touch-tone-selector'), + alertTone: panel.querySelector('#alert-tone-selection'), + ringTone: panel.querySelector('#ring-tone-selection'), + ringer: panel.querySelector('#ringer') + }; + + sound.init(elements); + }, + onUninit: function() { + sound.uninit(); + } + }); + }; +}); diff --git a/apps/settings/js/panels/sound/sound.js b/apps/settings/js/panels/sound/sound.js new file mode 100644 index 000000000000..1f836bbb1908 --- /dev/null +++ b/apps/settings/js/panels/sound/sound.js @@ -0,0 +1,186 @@ +/* global getSupportedNetworkInfo, URL, MozActivity */ +/** + * Handle sound panel functionality + */ +define(function(require) { + 'use strict'; + + var ForwardLock = require('shared/omadrm/fl'); + var SettingsListener = require('shared/settings_listener'); + + var Sound = function() { + this.elements = null; + this.tones = null; + }; + + Sound.prototype = { + /** + * initialization + */ + init: function sound_init(elements) { + this.elements = elements; + + if (window.navigator.mozMobileConnections) { + var mobileConnections = window.navigator.mozMobileConnections; + // Show the touch tone selector if and only if we're on a CDMA network + var toneSelector = this.elements.toneSelector; + Array.prototype.forEach.call(mobileConnections, + function(mobileConnection) { + getSupportedNetworkInfo(mobileConnection, function(result) { + toneSelector.hidden = toneSelector.hidden && !result.cdma; + }); + }); + } + + // initialize the ring tone and alert tone menus. + this._configureTones(); + this._handleTones(); + }, + + uninit: function sound_uninit() { + this.elements = null; + this.tones = null; + }, + + _configureTones: function sound_configureTones() { + /** + * This array has one element for each selectable tone that appears in the + * "Tones" section of ../elements/sound.html. + */ + this.tones = [{ + pickType: 'alerttone', + settingsKey: 'notification.ringtone', + allowNone: true, // Allow "None" as a choice for alert tones. + button: this.elements.alertTone + }]; + + // If we're a telephone, then show the section for ringtones, too. + if (navigator.mozTelephony) { + this.tones.push({ + pickType: 'ringtone', + settingsKey: 'dialer.ringtone', + allowNone: false, // The ringer must always have an actual sound. + button: this.elements.ringTone + }); + this.elements.ringer.hidden = false; + } + }, + + _handleTones: function sound_handleTones() { + var _ = navigator.mozL10n.get; + var self = this; + + // For each kind of tone, hook up the button that will allow the user + // to select a sound for that kind of tone. + this.tones.forEach(function(tone) { + var namekey = tone.settingsKey + '.name'; + // The button looks like a select element. By default it just reads + // "change". But we want it to display the name of the current tone. + // So we look up that name in the settings database. + SettingsListener.observe(namekey, '', function(tonename) { + tone.button.textContent = tonename || _('change'); + }); + + // When the user clicks the button, we launch an activity that lets + // the user select new ringtone. + tone.button.onclick = function() { + /** + * Before we can start the Pick activity, we need to know if there + * is locked content on the phone because we don't want the user to + * see "Purchased Media" as a choice if there isn't any purchased + * media on the phone. The ForwardLock secret key is not generated + * until it is needed, so we can use its existance to + * determine whether to show the Purchased Media app. + */ + ForwardLock.getKey(function(secret) { + var activity = new MozActivity({ + name: 'pick', + data: { + type: tone.pickType, + allowNone: tone.allowNone, + // If we have a secret then there is locked content on the phone + // so include it as a choice for the user + includeLocked: (secret !== null) + } + }); + + activity.onsuccess = function() { + var blob = activity.result.blob; // The returned ringtone sound + var name = activity.result.name; // The name of this ringtone + + if (!blob) { + if (tone.allowNone) { + // If we allow a null blob, then everything is okay + self._setRingtone(blob, name, tone.settingsKey); + } else { + // Otherwise this is an error and we should not change the + // current setting. (The ringtones app should never return + // a null blob if allowNone is false, but other apps might.) + alert(_('unplayable-ringtone')); + } + return; + } + + // If we got a locked ringtone, we have to unlock it first + if (blob.type.split('/')[1] === ForwardLock.mimeSubtype) { + ForwardLock.unlockBlob(secret, blob, function(unlocked) { + self._checkRingtone(unlocked, name, tone); + }); + } else { // Otherwise we can just use the blob directly. + self._checkRingtone(blob, name, tone); + } + }; + }); + }; + }); + }, + /** + * Make sure that the blob we got from the activity is actually + * a playable audio file. It would be very bad to set an corrupt + * blob as a ringtone because then the phone wouldn't ring! + */ + _checkRingtone: function sound_checkRingtone(blob, name, tone) { + var _ = navigator.mozL10n.get; + var oldRingtoneName = tone.button.textContent; + tone.button.textContent = _('savingringtone'); + + var player = new Audio(); + player.preload = 'metadata'; + player.src = URL.createObjectURL(blob); + player.oncanplay = function() { + release(); + // this will update the button text + this._setRingtone(blob, name, tone.settingsKey); + }.bind(this); + player.onerror = function() { + release(); + tone.button.textContent = oldRingtoneName; + alert(_('unplayable-ringtone')); + }; + + function release() { + URL.revokeObjectURL(player.src); + player.removeAttribute('src'); + player.load(); + } + }, + /* + * Save the sound in the settings db so that other apps can + * use it. + * Also save the sound name in the db so we can display it in the future. + * And update the button text to the new name now. + */ + _setRingtone: function sound_setRingtone(blob, name, settingsKey) { + // Update the settings database. This will cause the button + // text to change as well because of the SettingsListener above. + var values = {}; + values[settingsKey] = blob; + values[settingsKey + '.name'] = name || ''; + navigator.mozSettings.createLock().set(values); + } + }; + + return function ctor_sound() { + return new Sound(); + }; +}); diff --git a/apps/settings/js/sound.js b/apps/settings/js/sound.js deleted file mode 100644 index 096a805f07bb..000000000000 --- a/apps/settings/js/sound.js +++ /dev/null @@ -1,147 +0,0 @@ -/* global getSupportedNetworkInfo, SettingsListener, ForwardLock, URL, - MozActivity */ -/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */ -/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ - -(function() { - 'use strict'; - - var _ = navigator.mozL10n.get; - (function() { - var mobileConnections = window.navigator.mozMobileConnections; - // Show the touch tone selector if and only if we're on a CDMA network - var toneSelector = document.getElementById('touch-tone-selector'); - Array.prototype.forEach.call(mobileConnections, function(mobileConnection) { - getSupportedNetworkInfo(mobileConnection, function(result) { - toneSelector.hidden = toneSelector.hidden && !result.cdma; - }); - }); - })(); - // Now initialize the ring tone and alert tone menus. - - // This array has one element for each selectable tone that appears in the - // "Tones" section of ../elements/sound.html. - var tones = [ - { - pickType: 'alerttone', - settingsKey: 'notification.ringtone', - allowNone: true, // Allow "None" as a choice for alert tones. - button: document.getElementById('alert-tone-selection') - } - ]; - - // If we're a telephone, then show the section for ringtones, too. - if (navigator.mozTelephony) { - tones.push({ - pickType: 'ringtone', - settingsKey: 'dialer.ringtone', - allowNone: false, // The ringer must always have an actual sound. - button: document.getElementById('ring-tone-selection') - }); - document.getElementById('ringer').hidden = false; - } - - // For each kind of tone, hook up the button that will allow the user - // to select a sound for that kind of tone. - tones.forEach(function(tone) { - var namekey = tone.settingsKey + '.name'; - - // The button looks like a select element. By default it just reads - // "change". But we want it to display the name of the current tone. - // So we look up that name in the settings database. - SettingsListener.observe(namekey, '', function(tonename) { - tone.button.textContent = tonename || _('change'); - }); - - // When the user clicks the button, we launch an activity that lets - // the user select new ringtone. - tone.button.onclick = function() { - - // Before we can start the Pick activity, we need to know if there - // is locked content on the phone because we don't want the user to - // see "Purchased Media" as a choice if there isn't any purchased - // media on the phone. The ForwardLock secret key is not generated - // until it is needed, so we can use its existance to determine whether - // to show the Purchased Media app. - ForwardLock.getKey(function(secret) { - var activity = new MozActivity({ - name: 'pick', - data: { - type: tone.pickType, - allowNone: tone.allowNone, - // If we have a secret then there is locked content on the phone - // so include it as a choice for the user - includeLocked: (secret !== null) - } - }); - - activity.onsuccess = function() { - var blob = activity.result.blob; // The returned ringtone sound - var name = activity.result.name; // The name of this ringtone - - if (!blob) { - if (tone.allowNone) { - // If we allow a null blob, then everything is okay - setRingtone(blob, name); - } - else { - // Otherwise this is an error and we should not change the - // current setting. (The ringtones app should never return - // a null blob if allowNone is false, but other apps might.) - alert(_('unplayable-ringtone')); - } - return; - } - - // If we got a locked ringtone, we have to unlock it first - if (blob.type.split('/')[1] === ForwardLock.mimeSubtype) { - ForwardLock.unlockBlob(secret, blob, function(unlocked) { - checkRingtone(unlocked, name); - }); - } else { // Otherwise we can just use the blob directly. - checkRingtone(blob, name); - } - - // Make sure that the blob we got from the activity is actually - // a playable audio file. It would be very bad to set an corrupt - // blob as a ringtone because then the phone wouldn't ring! - function checkRingtone(blob, name) { - var oldRingtoneName = tone.button.textContent; - tone.button.textContent = _('savingringtone'); - - var player = new Audio(); - player.preload = 'metadata'; - player.src = URL.createObjectURL(blob); - player.oncanplay = function() { - release(); - setRingtone(blob, name); // this will update the button text - }; - player.onerror = function() { - release(); - tone.button.textContent = oldRingtoneName; - alert(_('unplayable-ringtone')); - }; - - function release() { - URL.revokeObjectURL(player.src); - player.removeAttribute('src'); - player.load(); - } - } - - // Save the sound in the settings db so that other apps can use it. - // Also save the sound name in the db so we can display it in the - // future. And update the button text to the new name now. - function setRingtone(blob, name) { - // Update the settings database. This will cause the button - // text to change as well because of the SettingsListener above. - var values = {}; - values[tone.settingsKey] = blob; - values[namekey] = name || ''; - navigator.mozSettings.createLock().set(values); - } - }; - }); - }; - }); -}()); diff --git a/apps/settings/test/unit/mock_settings_listener.js b/apps/settings/test/unit/mock_settings_listener.js new file mode 100644 index 000000000000..db2e700e5588 --- /dev/null +++ b/apps/settings/test/unit/mock_settings_listener.js @@ -0,0 +1,13 @@ +/* global define */ +/* SettingsListener mock for AMD panel */ +define(function() { + 'use strict'; + + var ctor = { + observe: function sl_observe(namekey, arg, callback) { + callback(namekey); + } + }; + + return ctor; +}); diff --git a/apps/settings/test/unit/panels/sound/sound_test.js b/apps/settings/test/unit/panels/sound/sound_test.js new file mode 100644 index 000000000000..6ff0ff0aec9b --- /dev/null +++ b/apps/settings/test/unit/panels/sound/sound_test.js @@ -0,0 +1,183 @@ +/* global MockNavigatorMozTelephony */ +'use strict'; +mocha.globals([ + 'MockL10n', + 'telephonyAddCall', + 'telephonyAddCdmaCall', + 'MockNavigatorMozTelephony', + 'MockMobileconnection', + 'MockNavigatorMozMobileConnections', + 'MockNavigatorSettings', + 'ForwardLock', + 'getSupportedNetworkInfo' +]); + +suite('Sound > ', function() { + var Sound; + var realL10n, realTelephony, realMozSettings, realMozMobileConnections; + + suiteSetup(function(done) { + testRequire([ + 'shared_mocks/mock_navigator_moz_telephony', + 'unit/mock_l10n', + 'unit/mock_settings_listener', + 'panels/sound/sound', + 'shared_mocks/mock_navigator_moz_settings', + 'shared_mocks/mock_navigator_moz_mobile_connections' + ], + { //mock map + 'panels/sound/sound': { + 'shared/settings_listener': 'unit/mock_settings_listener' + } + }, + function(MockNavigatorMozTelephony, MockL10n, + MockSettingsListener, module, MockNavigatorSettings) { + Sound = module; + // mock l10n + realL10n = window.navigator.mozL10n; + window.navigator.mozL10n = MockL10n; + // mock mozMobileConnections + realMozMobileConnections = window.navigator.mozMobileConnections; + window.navigator.mozMobileConnections = + window.MockNavigatorMozMobileConnections; + // mock mozTelephony + realTelephony = navigator.mozTelephony; + navigator.mozTelephony = MockNavigatorMozTelephony; + // mock mozSettings + realMozSettings = navigator.mozSettings; + navigator.mozSettings = MockNavigatorSettings; + + done(); + }); + }); + + suiteTeardown(function() { + window.navigator.mozL10n = realL10n; + navigator.mozTelephony = realTelephony; + navigator.mozSettings = realMozSettings; + }); + + suite('initiation', function() { + var sound; + setup(function() { + sound = Sound(); + var mock_elements = {}; + + this.sinon.stub(sound, '_configureTones'); + this.sinon.stub(sound, '_handleTones'); + window.getSupportedNetworkInfo = function() {}; + this.sinon.stub(window, 'getSupportedNetworkInfo'); + sound.init(mock_elements); + }); + + teardown(function() { + window.getSupportedNetworkInfo.restore(); + }); + + test('we would call _configureTones and _handleTones in init', function() { + assert.ok(sound._configureTones.called); + assert.ok(sound._handleTones.called); + }); + + test('we would call getSupportedNetworkInfo in init', function() { + assert.ok(window.getSupportedNetworkInfo.called); + }); + }); + + suite('_configureTones', function() { + var mock_elements; + var sound; + setup(function() { + sound = Sound(); + mock_elements = { + ringer: document.createElement('div') + }; + + this.sinon.stub(sound, '_handleTones'); + window.getSupportedNetworkInfo = function() {}; + this.sinon.stub(window, 'getSupportedNetworkInfo'); + }); + + teardown(function() { + window.getSupportedNetworkInfo.restore(); + }); + + test('we would push alerttone by default in tones list', function() { + navigator.mozTelephony = null; + sound.init(mock_elements); + assert.equal(sound.tones.length, 1); + assert.equal(sound.tones[0].pickType, 'alerttone'); + }); + + test('If device support telephony API, show the section for ringtones', + function() { + navigator.mozTelephony = MockNavigatorMozTelephony; + sound.init(mock_elements); + assert.equal(sound.tones.length, 2); + assert.equal(sound.tones[1].pickType, 'ringtone'); + }); + }); + + suite('_handleTones', function() { + var sound; + setup(function() { + sound = Sound(); + sound.tones = [{ + pickType: 'alerttone', + settingsKey: 'notification.ringtone', + allowNone: true, + button: document.createElement('div') + }]; + + sound._handleTones(); + }); + + test('we would set button textContent in SettingsListener', function() { + assert.equal(sound.tones[0].button.textContent, + 'notification.ringtone.name'); + }); + }); + + suite('_checkRingtone', function() { + var sound; + setup(function() { + sound = Sound(); + sound.tones = [{ + pickType: 'alerttone', + settingsKey: 'notification.ringtone', + allowNone: true, + button: document.createElement('div') + }]; + + var blob = this.sinon.stub(); + this.sinon.stub(window.URL, 'createObjectURL'); + sound._checkRingtone(blob, 'mock_name', sound.tones[0]); + }); + + teardown(function() { + window.URL.createObjectURL.restore(); + }); + + test('button textContent is changed in _checkRingtone', function() { + assert.equal(sound.tones[0].button.textContent, 'savingringtone'); + }); + }); + + suite('_setRingtone', function() { + var blob; + var sound; + setup(function() { + sound = Sound(); + blob = this.sinon.stub(); + sound._setRingtone(blob, 'aloha', 'notification.ringtone'); + }); + + test('we would set ringtone value via _setRingtone', function() { + assert.equal( + navigator.mozSettings.mSettings['notification.ringtone.name'], + 'aloha'); + assert.equal(navigator.mozSettings.mSettings['notification.ringtone'], + blob); + }); + }); +}); diff --git a/shared/test/unit/mocks/mock_navigator_moz_mobile_connections.js b/shared/test/unit/mocks/mock_navigator_moz_mobile_connections.js index 1a80ca70dbfc..ad31e79d8651 100644 --- a/shared/test/unit/mocks/mock_navigator_moz_mobile_connections.js +++ b/shared/test/unit/mocks/mock_navigator_moz_mobile_connections.js @@ -1,5 +1,9 @@ 'use strict'; +/** + * Will provide 1 Mobileconnection by default. call mAddMobileConnection and + * mRemoveMobileConnection to add/remove extra connections. + */ (function() { function MockMobileconnection() { var props = ['voice', 'data', 'iccId', 'radioState', 'iccInfo'];