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

View file

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

View file

@ -0,0 +1,18 @@
/* global Components, Services */
'use strict';
Components.utils.import('resource://gre/modules/Services.jsm');
Services.obs.addObserver(function(document) {
if (!document || !document.location) {
return;
}
var window = document.defaultView.wrappedJSObject;
var fakeTelephony = {};
window.navigator.__defineGetter__('mozTelephony', function() {
return fakeTelephony;
});
}, 'document-element-inserted', false);

View file

@ -0,0 +1,463 @@
/* jshint node: true*/
/* global marionetteScriptFinished */
/* exported findElement */
'use strict';
function createElementGetters(object, parentName, elementNames) {
elementNames.forEach(function(name) {
Object.defineProperty(object.prototype, name, {
get: function() {
return this[parentName].findElement(object.Selectors[name]);
}
});
});
}
/***** Popup windows *****/
function CustomDialog(client) {
this.client = client;
}
CustomDialog.Selectors = Object.freeze({
element: 'form#dialog-screen',
cancelButton: 'button#dialog-no',
confirmButton: 'button#dialog-yes'
});
CustomDialog.prototype = {
waitUntilCreated: function() {
this.client.helper.waitForElement(CustomDialog.Selectors.element);
}
};
createElementGetters(
CustomDialog, 'client', ['cancelButton', 'confirmButton']
);
function ActionsMenu(client) {
this.client = client;
this.element = this.client.findElement(ActionsMenu.Selectors.menu);
}
ActionsMenu.Selectors = Object.freeze({
menu: 'gaia-menu#ringtone-actions',
shareButton: 'button[data-action="share"]',
deleteButton: 'button[data-action="delete"]',
cancelButton: 'button[data-action="cancel"]',
shareMenu: 'form[data-z-index-level="action-menu"]'
});
ActionsMenu.prototype = {
waitUntilOpened: function() {
this.client.waitFor(function() {
return this.element.displayed();
}.bind(this));
},
waitForDialog: function() {
var dialog = new CustomDialog(this.client);
dialog.waitUntilCreated();
return dialog;
},
// TODO(gareth): Move this shareMenu stuff into the helper.
get shareMenu() {
// Switch to the system app first.
this.client.switchToFrame();
return this.client.helper.waitForElement(ActionsMenu.Selectors.shareMenu);
},
shareWith: function(appName) {
var shareMenu = this.shareMenu;
var list = shareMenu.findElements('button');
for (var i = 0; i < list.length; i++) {
if (list[i].text() === appName) {
return list[i];
}
}
return null;
}
};
createElementGetters(
ActionsMenu, 'element', ['shareButton', 'deleteButton', 'cancelButton']
);
// ***** Sounds *****
function BaseSound(client, element) {
this.client = client;
this.element = element;
}
BaseSound.Selectors = Object.freeze({
name: '.name',
subtitle: 'p.subtitle'
});
BaseSound.prototype = {
get name() {
return this.element.findElement(BaseSound.Selectors.name).text();
},
get subtitle() {
var subtitle = this.element.findElement(BaseSound.Selectors.subtitle);
return subtitle ? subtitle.text() : undefined;
}
};
function ManageSound(client, element) {
BaseSound.call(this, client, element);
}
ManageSound.prototype = Object.create(BaseSound.prototype);
ManageSound.prototype.constructor = ManageSound;
ManageSound.Selectors = Object.freeze({
description: 'div.desc',
actionsButton: 'a.actions-button'
});
ManageSound.prototype.tap = function() {
this.element.findElement(ManageSound.Selectors.description).tap(20, 20);
};
ManageSound.prototype.openActions = function() {
this.element.findElement(ManageSound.Selectors.actionsButton).tap();
var actionsMenu = new ActionsMenu(this.client);
actionsMenu.waitUntilOpened();
return actionsMenu;
};
Object.defineProperty(ManageSound.prototype, 'playing', {
get: function() {
return this.element.getAttribute('data-playing') === 'true';
}
});
function PickSound(client, element) {
BaseSound.call(this, client, element);
}
PickSound.prototype = Object.create(BaseSound.prototype);
PickSound.prototype.constructor = PickSound;
PickSound.Selectors = Object.freeze({
input: 'gaia-radio',
subtitle: 'gaia-radio details'
});
Object.defineProperty(PickSound.prototype, 'selected', {
get: function() {
return Boolean(this.element.findElement(PickSound.Selectors.input)
.scriptWith(function(el) {
return el.wrappedJSObject.checked;
}));
}
});
Object.defineProperty(PickSound.prototype, 'subtitle', {
get: function() {
var subtitle = this.element.findElement(PickSound.Selectors.subtitle);
return subtitle ? subtitle.text() : undefined;
}
});
PickSound.prototype.select = function() {
this.element.tap();
this.client.waitFor(function() {
return this.selected;
}.bind(this));
};
// ***** Sound Lists *****
function BaseSoundList(client, element, Sound) {
this.client = client;
this.element = element;
this.Sound = Sound;
}
BaseSoundList.Selectors = Object.freeze({
header: 'gaia-subheader > span',
sound: 'li'
});
BaseSoundList.prototype = {
client: null,
displayed: function(callback) {
return this.element.displayed(callback);
},
get sounds() {
// hack to find quicker that we have no elements
var quickly = this.client.scope({ searchTimeout: 50 });
this.element.client = quickly;
var elts = this.element.findElements(
BaseSoundList.Selectors.sound
);
this.element.client = this.client;
return elts.map(function(element) {
return new this.Sound(this.client, element);
}.bind(this));
}
};
createElementGetters(BaseSoundList, 'element', ['header']);
function ManageSoundList(client, element) {
BaseSoundList.call(this, client, element, ManageSound);
}
ManageSoundList.prototype = Object.create(BaseSoundList.prototype);
ManageSoundList.prototype.constructor = ManageSoundList;
function PickSoundList(client, element) {
BaseSoundList.call(this, client, element, PickSound);
}
PickSoundList.prototype = Object.create(BaseSoundList.prototype);
PickSoundList.prototype.constructor = PickSoundList;
Object.defineProperty(PickSoundList.prototype, 'selectedSound', {
get: function() {
var sounds = this.sounds;
for (var i = 0; i < sounds.length; i++) {
if (sounds[i].selected) {
return sounds[i];
}
}
}
});
// ***** Ringtones Containers *****
function BaseRingtonesContainer(client, SoundList) {
this.client = client;
this.SoundList = SoundList;
this.client.helper.waitForElement(
BaseRingtonesContainer.Selectors.readyBody
);
}
BaseRingtonesContainer.Selectors = Object.freeze({
readyBody: 'body[data-ready]',
soundLists: 'article#list-parent > section'
});
BaseRingtonesContainer.prototype = {
client: null,
get soundLists() {
return this.client.findElements(
BaseRingtonesContainer.Selectors.soundLists
).map(function(element) {
return new this.SoundList(this.client, element);
}.bind(this));
},
};
function ManageRingtonesContainer(client) {
BaseRingtonesContainer.call(this, client, ManageSoundList);
}
ManageRingtonesContainer.prototype = Object.create(
BaseRingtonesContainer.prototype
);
ManageRingtonesContainer.prototype.constructor = ManageRingtonesContainer;
ManageRingtonesContainer.Selectors = Object.freeze({
backButton: '#header',
addButton: 'button#add',
shareFrame: 'iframe[data-url$="share.html"]'
});
createElementGetters(
ManageRingtonesContainer, 'client', ['backButton', 'addButton']
);
ManageRingtonesContainer.prototype.addCustomRingtone = function(info) {
return this.client.executeAsyncScript(function(info) {
var customRingtones = window.wrappedJSObject.customRingtones;
customRingtones.add(JSON.parse(info)).then(function(tone) {
marionetteScriptFinished({
// Use the private members of tone, since the getters are getting Xrayed
// into oblivion. See bug 987111.
name: tone._name, subtitle: tone._subtitle, id: tone._id
});
}, function(error) {
marionetteScriptFinished(null);
});
}, [JSON.stringify(info)]);
};
ManageRingtonesContainer.prototype.waitUntilLoaded = function() {
this.client.waitFor(function() {
return this.soundLists.length === 2;
}.bind(this));
};
ManageRingtonesContainer.prototype.waitForNewRingtoneWindow =
function(callback) {
this.client.switchToFrame();
var win = this.client.helper.waitForElement(
ManageRingtonesContainer.Selectors.shareFrame
);
this.client.switchToFrame(win);
callback(new NewRingtoneContainer(this.client));
this.client.switchToFrame();
this.client.apps.switchToApp(Ringtones.URL);
};
function PickRingtoneContainer(client) {
BaseRingtonesContainer.call(this, client, PickSoundList);
}
PickRingtoneContainer.prototype = Object.create(
BaseRingtonesContainer.prototype
);
PickRingtoneContainer.prototype.constructor = PickRingtoneContainer;
PickRingtoneContainer.Selectors = Object.freeze({
cancelButton: '#header',
setButton: 'button#set'
});
PickRingtoneContainer.prototype.waitUntilLoaded = function() {
this.client.waitFor(function() {
return this.soundLists.length === 3;
}.bind(this));
};
createElementGetters(
PickRingtoneContainer, 'client', ['cancelButton', 'setButton']
);
Object.defineProperty(PickRingtoneContainer.prototype, 'selectedSound', {
get: function() {
var lists = this.soundLists;
for (var i = 0; i < lists.length; i++) {
if (lists[i].selectedSound) {
return lists[i].selectedSound;
}
}
}
});
function NewRingtoneContainer(client) {
this.client = client;
// For some reason, we can't add blobs to indexedDB in Marionette
// tests, so just null out our blob.
this.client.executeScript(function() {
var customRingtones = window.wrappedJSObject.customRingtones;
var oldAdd = customRingtones.add;
customRingtones.add = function(info) {
return oldAdd.call(
customRingtones, {name: info.name, subtitle: info.subtitle}
);
};
});
}
NewRingtoneContainer.Selectors = Object.freeze({
cancelButton: '#header',
saveButton: 'button#save',
songTitle: '#songtitle',
artist: '#artist'
});
NewRingtoneContainer.prototype = {
get songTitle() {
return this.client.findElement(
NewRingtoneContainer.Selectors.songTitle
).text();
},
get artist() {
return this.client.findElement(
NewRingtoneContainer.Selectors.artist
).text();
},
waitForSongInfo: function(expectedTitle, expectedArtist) {
this.client.waitFor(function() {
return this.songTitle === expectedTitle && this.artist === expectedArtist;
}.bind(this));
}
};
createElementGetters(
NewRingtoneContainer, 'client', ['cancelButton', 'saveButton']
);
var SETTINGS_URL = 'app://settings.gaiamobile.org';
// XXX: This is a workaround for changes in the system app; see bug 950673.
// I neither know (nor care) why this works, but we should probably fix it
// up when a better way comes along.
function switchToAppFixed(client, url) {
client.switchToFrame();
var frame = client.findElement('iframe[src*="' + url + '"]');
client.switchToFrame(frame, {focus: true});
client.helper.wait(500);
}
/**
* Abstraction around Ringtones app.
* @constructor
* @param {Marionette.Client} client for operations.
*/
function Ringtones(client) {
this.client = client;
}
/**
* @type String Origin of Ringtones app
*/
Ringtones.URL = 'app://ringtones.gaiamobile.org';
module.exports = Ringtones;
Ringtones.prototype = {
client: null,
switchToMe: function() {
this.client.switchToFrame();
this.client.apps.switchToApp(Ringtones.URL);
},
inAlertTones: function(soundPanel, callback) {
soundPanel.clickAlertToneSelect();
this.switchToMe();
callback(new PickRingtoneContainer(this.client));
switchToAppFixed(this.client, SETTINGS_URL);
},
inRingTones: function(soundPanel, callback) {
soundPanel.clickRingToneSelect();
this.switchToMe();
callback(new PickRingtoneContainer(this.client));
switchToAppFixed(this.client, SETTINGS_URL);
},
inManager: function(soundPanel, callback) {
soundPanel.clickManageTones();
this.switchToMe();
callback(new ManageRingtonesContainer(this.client));
switchToAppFixed(this.client, SETTINGS_URL);
},
inShare: function(callback) {
this.switchToMe();
callback(new NewRingtoneContainer(this.client));
}
};

View file

@ -0,0 +1,35 @@
/* jshint node: true*/
/* exported injectTone */
'use strict';
var fs = require('fs');
var path = require('path');
function injectTone(client, details) {
var BASE_DIRS = {
'ringtone': 'Ringtones',
'alerttone': 'Notifications'
};
if (!(details.type in BASE_DIRS)) {
throw new Error('tone type not supported: ' + details.type);
}
var toneFolder = path.join(
client.fileManager.deviceStorage.getDeviceStoragePath(),
BASE_DIRS[details.type]
);
if (!fs.existsSync(toneFolder)) {
fs.mkdirSync(toneFolder);
}
var musicFolder = path.join(toneFolder, 'music');
if (!fs.existsSync(musicFolder)) {
fs.mkdirSync(musicFolder);
}
client.fileManager.add([
{ type: path.join(BASE_DIRS[details.type], 'music'),
filePath: details.filePath, filename: details.filename }
]);
}
module.exports = {injectTone: injectTone};

View file

@ -0,0 +1,347 @@
/* jshint node: true*/
/* global marionette, setup, suite, test*/
'use strict';
var assert = require('assert');
var sd_card = require('./lib/sd_card');
var Ringtones = require('./lib/ringtones');
var Settings = require('../../../settings/test/marionette/app/app');
var Music = require('../../../music/test/marionette/lib/music');
function assert_sorted(array, message) {
if (array.length === 0) {
return;
}
var prevElt = array[0];
for (var i = 1; i < array.length; i++) {
assert(prevElt.localeCompare(array[i]) < 0, message);
prevElt = array[i];
}
}
marionette('Ringtone management', function() {
var client = marionette.client({
profile: {
prefs: {
'device.storage.enabled': true,
'device.storage.testing': true,
'device.storage.prompt.testing': true
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var app, settingsApp;
var soundPanel;
setup(function() {
settingsApp = new Settings(client);
app = new Ringtones(client);
client.fileManager.removeAllFiles();
client.fileManager.add([
{ type: 'music', filePath: 'test_media/samples/Music/b2g.ogg' }
]);
settingsApp.launch();
soundPanel = settingsApp.soundPanel;
});
suite('Initial load', function() {
test('Load list of sounds', function() {
app.inManager(soundPanel, function(container) {
var soundLists = container.soundLists;
assert.equal(soundLists.length, 4,
'Should have found 4 sound lists, found ' +
soundLists.length);
var expectedInfo = [
{l10nID: 'section-title-builtin-ringtone', visible: true},
{l10nID: 'section-title-custom-ringtone', visible: false},
{l10nID: 'section-title-builtin-alerttone', visible: true},
{l10nID: 'section-title-custom-alerttone', visible: false}
];
var getName = function(element) {
return element.name.toLocaleLowerCase();
};
for (var i = 0; i < soundLists.length; i++) {
// Check basic properties of each section.
assert.equal(soundLists[i].header.getAttribute('data-l10n-id'),
expectedInfo[i].l10nID);
assert.equal(soundLists[i].sounds.length !== 0,
expectedInfo[i].visible);
assert.equal(soundLists[i].displayed(), expectedInfo[i].visible);
// Check that the tones are in sorted order.
var names = soundLists[i].sounds.map(getName);
assert_sorted(names, 'Tones should be sorted by name');
}
});
});
test('Load custom sound', function() {
// Inject a custom ringtone, then close and reopen the app to show it.
app.inManager(soundPanel, function(container) {
var tone = container.addCustomRingtone({
name: 'My ringtone', subtitle: 'Bob\'s Ringtones'
});
assert.equal(tone.name, 'My ringtone');
assert.equal(tone.subtitle, 'Bob\'s Ringtones');
container.backButton.tap(25, 25);
});
app.inManager(soundPanel, function(container) {
var customRingtones = container.soundLists[1];
assert.equal(customRingtones.sounds.length, 1,
'Custom ringtone list should have one item, found ' +
customRingtones.sounds.length);
assert.equal(customRingtones.displayed(), true,
'Custom ringtone list should be displayed');
var myTone = customRingtones.sounds[0];
assert.equal(myTone.name, 'My ringtone');
assert.equal(myTone.subtitle, 'Bob\'s Ringtones');
});
});
test('Load sd card ringtone', function() {
sd_card.injectTone(client, {
type: 'ringtone', filePath: 'test_media/samples/Music/b2g.ogg'
});
app.inManager(soundPanel, function(container) {
var customRingtones = container.soundLists[1];
assert.equal(customRingtones.sounds.length, 1,
'Custom ringtone list should have one item, found ' +
customRingtones.sounds.length);
assert.equal(customRingtones.displayed(), true,
'Custom ringtone list should be displayed');
var customAlertTones = container.soundLists[3];
assert.equal(customAlertTones.sounds.length, 0,
'Custom alert tone list should have no items, found ' +
customAlertTones.sounds.length);
assert.equal(customAlertTones.displayed(), false,
'Custom alert tone list should be hidden');
var myTone = customRingtones.sounds[0];
assert.equal(myTone.name, 'b2g');
assert.equal(myTone.subtitle, 'SD Card');
});
});
test('Load sd card alert tone', function() {
sd_card.injectTone(client, {
type: 'alerttone', filePath: 'test_media/samples/Music/b2g.ogg'
});
app.inManager(soundPanel, function(container) {
var customRingtones = container.soundLists[1];
assert.equal(customRingtones.sounds.length, 0,
'Custom ringtone list should have no items, found ' +
customRingtones.sounds.length);
assert.equal(customRingtones.displayed(), false,
'Custom ringtone list should be hidden');
var customAlertTones = container.soundLists[3];
assert.equal(customAlertTones.sounds.length, 1,
'Custom alert tone list should have one item, found ' +
customAlertTones.sounds.length);
assert.equal(customAlertTones.displayed(), true,
'Custom alert tone list should be displayed');
var myTone = customAlertTones.sounds[0];
assert.equal(myTone.name, 'b2g');
assert.equal(myTone.subtitle, 'SD Card');
});
});
});
suite('Creation', function() {
test('Create new ringtone', function() {
var musicApp = new Music(client);
app.inManager(soundPanel, function(container) {
container.addButton.tap();
musicApp.switchToMe();
musicApp.playFirstSong();
musicApp.finishPick();
container.waitForNewRingtoneWindow(function(win) {
win.waitForSongInfo('Boot To Gecko (B2G)', 'Minute With');
win.saveButton.tap();
});
var customSounds = container.soundLists[1];
client.waitFor(function() {
return customSounds.sounds.length === 1;
});
assert.equal(customSounds.displayed(), true,
'Custom ringtone list should be displayed');
var myTone = customSounds.sounds[0];
assert.equal(myTone.name, 'Boot To Gecko (B2G)');
assert.equal(myTone.subtitle, 'Minute With');
});
});
});
suite('Previewing', function() {
test('Previewing shows play icon', function() {
app.inManager(soundPanel, function(container) {
// Pick an alert tone, since they're short.
var tone = container.soundLists[2].sounds[0];
tone.tap();
client.waitFor(function() { return tone.playing; });
// The play icon should go away on its own once the tone ends.
client.waitFor(function() { return !tone.playing; });
});
});
test('Tapping twice hides play icon', function() {
app.inManager(soundPanel, function(container) {
var tone = container.soundLists[0].sounds[0];
tone.tap();
client.waitFor(function() { return tone.playing; });
tone.tap();
client.waitFor(function() { return !tone.playing; });
});
});
test('Tapping another tone hides first play icon', function() {
app.inManager(soundPanel, function(container) {
var tone1 = container.soundLists[0].sounds[0];
var tone2 = container.soundLists[0].sounds[1];
tone1.tap();
client.waitFor(function() { return tone1.playing; });
tone2.tap();
client.waitFor(function() { return tone2.playing; });
client.waitFor(function() { return !tone1.playing; });
});
});
});
suite('Actions', function() {
var toneInfos = [
{location: 'built-in', type: 'ringtone', deletable: false,
listIndex: 0, setup: function() {}},
{location: 'built-in', type: 'alerttone', deletable: false, listIndex: 2,
setup: function() {}},
{location: 'custom', type: 'ringtone', deletable: true, listIndex: 1,
setup: function() {
// Inject a custom ringtone, then close and reopen the app to show it.
app.inManager(soundPanel, function(container) {
container.addCustomRingtone({
name: 'My ringtone', subtitle: 'Bob\'s Ringtones', blob: {
name: 'my_ringtone.mp3'
}
});
container.backButton.tap(25, 25);
});
}},
{location: 'sd card', type: 'ringtone', deletable: true, listIndex: 1,
setup: function() {
sd_card.injectTone(client, {
type: 'ringtone', filePath: 'test_media/samples/Music/b2g.ogg'
});
}},
{location: 'sd card', type: 'alerttone', deletable: true, listIndex: 3,
setup: function() {
sd_card.injectTone(client, {
type: 'alerttone', filePath: 'test_media/samples/Music/b2g.ogg'
});
}}
];
toneInfos.forEach(function(info) {
suite('Actions for ' + info.location + ' ' + info.type, function() {
setup(info.setup);
test('Open action menu', function() {
app.inManager(soundPanel, function(container) {
var list = container.soundLists[info.listIndex];
var actionsMenu = list.sounds[0].openActions();
assert.equal(actionsMenu.shareButton.getAttribute('data-l10n-id'),
'actions-share-' + info.type);
assert.equal(actionsMenu.shareButton.displayed(), true,
'Share button should be displayed');
assert.equal(actionsMenu.deleteButton.getAttribute('data-l10n-id'),
'actions-delete-' + info.type);
assert.equal(actionsMenu.deleteButton.displayed(), info.deletable,
'Delete button should be ' +
(info.deletable ? 'displayed' : 'hidden'));
});
});
test('Share ringtone', function() {
app.inManager(soundPanel, function(container) {
var list = container.soundLists[info.listIndex];
// Make sure we get at least as far as being able to share to the
// SMS app. XXX: Later, this could be more detailed and actually
// check that the right data was sent...
var actionsMenu = list.sounds[0].openActions();
actionsMenu.shareButton.tap();
// Make sure we can't share with ourselves, since that's weird.
assert.equal(actionsMenu.shareWith('Ringtones'), null,
'Ringtones app shouldn\'t be in Share menu');
// Share with the messages app.
actionsMenu.shareWith('Messages').tap();
});
});
if (info.deletable) {
test('Delete ringtone', function() {
var numTones;
app.inManager(soundPanel, function(container) {
var list = container.soundLists[info.listIndex];
numTones = list.sounds.length;
var actionsMenu = list.sounds[0].openActions();
actionsMenu.deleteButton.tap();
var dialog = actionsMenu.waitForDialog();
dialog.cancelButton.tap();
assert.equal(list.sounds.length, numTones,
'Tone should not have been deleted');
actionsMenu = list.sounds[0].openActions();
actionsMenu.deleteButton.tap();
dialog = actionsMenu.waitForDialog();
dialog.confirmButton.tap();
assert.equal(list.sounds.length, numTones - 1,
'Tone should have been deleted');
container.backButton.tap(25, 25);
});
app.inManager(soundPanel, function(container) {
var list = container.soundLists[info.listIndex];
assert.equal(list.sounds.length, numTones - 1,
'Tone should have remained deleted');
});
});
}
});
});
});
});

View file

@ -0,0 +1,219 @@
/* jshint node: true*/
/* global marionette, setup, suite, test*/
'use strict';
var Ringtones = require('./lib/ringtones');
var Settings = require('../../../settings/test/marionette/app/app');
var assert = require('assert');
var sd_card = require('./lib/sd_card');
function assert_sorted(array, message) {
if (array.length === 0) {
return;
}
var prevElt = array[0];
for (var i = 1; i < array.length; i++) {
assert(prevElt.localeCompare(array[i]) < 0, message);
prevElt = array[i];
}
}
marionette('Ringtone picker', function() {
var client = marionette.client({
profile: {
prefs: {
'device.storage.enabled': true,
'device.storage.testing': true,
'device.storage.prompt.testing': true
}
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var settingsApp;
var soundPanel;
var app;
setup(function() {
settingsApp = new Settings(client);
app = new Ringtones(client, settingsApp);
client.fileManager.removeAllFiles();
client.contentScript.inject(__dirname +
'/lib/mocks/mock_navigator_moz_telephony.js');
settingsApp.launch();
soundPanel = settingsApp.soundPanel;
});
// We want to run the same tests on both the alerttones and ringtones,
// so we'll write the tests once and then generate a pair of suites.
var suiteInfos = [
{ name: 'Alert tones picker',
opener: 'inAlertTones',
type: 'alerttone',
allowNone: true
},
{ name: 'Ringtones picker',
opener: 'inRingTones',
type: 'ringtone',
allowNone: false
}
];
suiteInfos.forEach(function(suiteInfo) {
suite(suiteInfo.name, function() {
var otherType = suiteInfo.type === 'alerttone' ? 'ringtone' : 'alerttone';
test('Load list of sounds', function() {
app[suiteInfo.opener](soundPanel, function(container) {
var soundLists = container.soundLists;
assert.equal(soundLists.length, 4,
'Should have found 4 sound lists, found ' +
soundLists.length);
var expectedInfo = [
{l10nID: 'section-title-builtin-' + suiteInfo.type, visible: true},
{l10nID: 'section-title-custom-' + suiteInfo.type, visible: false},
{l10nID: 'section-title-builtin-' + otherType, visible: true},
{l10nID: 'section-title-custom-' + otherType, visible: false}
];
var getName = function(element) {
return element.name.toLocaleLowerCase();
};
for (var i = 0; i < soundLists.length; i++) {
// Check basic properties of each section.
assert.equal(soundLists[i].header.getAttribute('data-l10n-id'),
expectedInfo[i].l10nID);
assert.equal(soundLists[i].sounds.length !== 0,
expectedInfo[i].visible);
assert.equal(soundLists[i].displayed(), expectedInfo[i].visible);
// Check that the tones are in sorted order.
var names = soundLists[i].sounds.map(getName);
if (i === 0 && suiteInfo.allowNone) {
names.shift();
}
assert_sorted(names, 'Tones should be sorted by name');
}
// Make sure the "None" ringtone is listed first, if appropriate.
assert.equal(soundLists[0].sounds[0].name === 'None',
suiteInfo.allowNone,
'"None" ringtone should ' +
(suiteInfo.allowNone ? '' : 'not ') +
'be shown');
});
});
test('Load custom sound', function() {
// Inject a custom ringtone, then close and reopen the app to show it.
app.inManager(soundPanel, function(container) {
var tone = container.addCustomRingtone({
name: 'My ringtone', subtitle: 'Bob\'s Ringtones'
});
assert.equal(tone.name, 'My ringtone');
assert.equal(tone.subtitle, 'Bob\'s Ringtones');
container.backButton.tap(25, 25);
});
app[suiteInfo.opener](soundPanel, function(container) {
var index = suiteInfo.type === 'ringtone' ? 1 : 3;
var customRingtones = container.soundLists[index];
assert.equal(customRingtones.sounds.length, 1,
'Custom ringtone list should have one item, found ' +
customRingtones.sounds.length);
assert.equal(customRingtones.displayed(), true,
'Custom ringtone list should be displayed');
var myTone = customRingtones.sounds[0];
assert.equal(myTone.name, 'My ringtone');
assert.equal(myTone.subtitle, 'Bob\'s Ringtones');
});
});
test('Load sd card sound', function() {
sd_card.injectTone(client, {
type: suiteInfo.type, filePath: 'test_media/samples/Music/b2g.ogg'
});
app[suiteInfo.opener](soundPanel, function(container) {
var customTones = container.soundLists[1];
assert.equal(
customTones.sounds.length, 1,
'Custom ' + suiteInfo.type + ' list should have one item, found ' +
customTones.sounds.length
);
assert.equal(
customTones.displayed(), true,
'Custom ' + suiteInfo.type + ' list should be displayed'
);
var customOtherTones = container.soundLists[3];
assert.equal(
customOtherTones.sounds.length, 0,
'Custom ' + otherType + ' list should have no items, found ' +
customOtherTones.sounds.length
);
assert.equal(
customOtherTones.displayed(), false,
'Custom ' + otherType + ' list should be hidden'
);
var myTone = customTones.sounds[0];
assert.equal(myTone.name, 'b2g');
assert.equal(myTone.subtitle, 'SD Card');
});
});
test('Set selected sound', function() {
var originalButtonText = soundPanel.getSelectedTone(suiteInfo.type);
var toneIndex, toneName;
app[suiteInfo.opener](soundPanel, function(container) {
// Select the first sound with a different name from the current
// button's text.
var tone = container.soundLists[0].sounds[0];
toneIndex = 0;
if (tone.name === originalButtonText) {
tone = container.soundLists[0].sounds[1];
toneIndex = 1;
}
toneName = tone.name;
tone.select();
container.setButton.tap();
});
var buttonText = soundPanel.getSelectedTone(suiteInfo.type);
assert.strictEqual(buttonText, toneName);
// Check that reopening the picker selects the right sound.
app[suiteInfo.opener](soundPanel, function(container) {
assert.ok(container.soundLists[0].sounds[toneIndex].selected);
});
});
test('Cancel setting selected sound', function() {
var originalButtonText = soundPanel.getSelectedTone(suiteInfo.type);
app[suiteInfo.opener](soundPanel, function(container) {
// Select the first sound with a different name from the current
// button's text.
var tone = container.soundLists[0].sounds[0];
if (tone.name === originalButtonText) {
tone = container.soundLists[0].sounds[1];
}
tone.select();
container.cancelButton.tap(25, 25);
});
var buttonText = soundPanel.getSelectedTone(suiteInfo.type);
assert.strictEqual(buttonText, originalButtonText);
});
});
});
});

View file

@ -0,0 +1,71 @@
/* jshint node: true*/
/* global marionette, setup, suite, test*/
'use strict';
var assert = require('assert');
var Ringtones = require('./lib/ringtones');
var Settings = require('../../../settings/test/marionette/app/app');
var Music = require('../../../music/test/marionette/lib/music');
marionette('Share as ringtone', function() {
var client = marionette.client({
profile: {
prefs: {
'device.storage.enabled': true,
'device.storage.testing': true,
'device.storage.prompt.testing': true
}
}
});
var app;
setup(function() {
app = new Ringtones(client);
client.fileManager.removeAllFiles();
client.fileManager.add([
{ type: 'music', filePath: 'test_media/samples/Music/b2g.ogg' }
]);
});
suite('Share activity', function() {
test('Create new ringtone', function() {
try {
var musicApp = new Music(client);
var settingsApp = new Settings(client);
musicApp.launch();
musicApp.waitForFirstTile();
musicApp.switchToSongsView();
musicApp.playFirstSong();
musicApp.shareWith('Ringtones');
app.inShare(function(container) {
container.waitForSongInfo('Boot To Gecko (B2G)', 'Minute With');
container.saveButton.tap();
});
client.switchToFrame();
settingsApp.launch();
var soundPanel = settingsApp.soundPanel;
app.inManager(soundPanel, function(container) {
var customSounds = container.soundLists[1];
assert.equal(customSounds.sounds.length, 1,
'Custom ringtone list should have one item');
assert.equal(customSounds.displayed(), true,
'Custom ringtone list should be displayed');
var myTone = customSounds.sounds[0];
assert.equal(myTone.name, 'Boot To Gecko (B2G)');
assert.equal(myTone.subtitle, 'Minute With');
});
} catch(e) {
assert.ok(false, 'Exception ' + e.stack);
}
});
});
});

View file

@ -0,0 +1,57 @@
/* global assert, require, suite, test */
'use strict';
require('/js/built_in_ringtones.js');
suite('built-in ringtones', function() {
var toneTypes = ['ringtone', 'alerttone'];
var baseURLs = {
'ringtone': '/shared/resources/media/ringtones/',
'alerttone': '/shared/resources/media/notifications/'
};
test('toneTypes', function() {
assert.equal(window.builtInRingtones.toneTypes.toString(),
toneTypes.toString());
});
toneTypes.forEach(function(toneType) {
test('list(\'' + toneType + '\')', function(done) {
window.builtInRingtones.list(toneType).then(function(tones) {
done(function() {
assert.ok(Array.isArray(tones));
tones.forEach(function(tone) {
assert.ok(new RegExp('^builtin:' + toneType + '/').test(tone.id));
assert.equal(tone.url.substr(0, baseURLs[toneType].length),
baseURLs[toneType]);
assert.equal(tone.shareable, true);
assert.equal(tone.deletable, false);
});
});
}, function(error) {
done(error);
});
});
test('get(toneID)', function(done) {
window.builtInRingtones.list(toneType).then(function(tones) {
var expectedTone = tones[0];
var toneID = expectedTone.id;
window.builtInRingtones.get(toneID).then(function(tone) {
done(function() {
assert.equal(tone.id, expectedTone.id);
assert.equal(tone.l10nID, expectedTone.l10nID);
assert.equal(tone.url.substr(0, baseURLs[toneType].length),
baseURLs[toneType]);
assert.equal(tone.shareable, true);
assert.equal(tone.deletable, false);
});
}, function(error) {
done(error);
});
});
});
});
});

View file

@ -0,0 +1,200 @@
/* global assert, require, setup, suite, test */
'use strict';
require('/js/custom_ringtones.js');
suite('custom ringtones', function() {
var testBlob;
setup(function(done) {
testBlob = new Blob();
window.customRingtones.clear().then(function() {
done();
});
});
suite('empty database', function() {
test('list()', function(done) {
window.customRingtones.list().then(function(tones) {
done(function() {
assert.ok(Array.isArray(tones));
assert.equal(tones.length, 0);
});
}, function(error) {
done(error);
});
});
test('add()', function(done) {
var info = {name: 'My ringtone', blob: testBlob};
// Add a ringtone, and then get the blob from it and test everything!
window.customRingtones.add(info).then(function(tone) {
return tone.getBlob().then(function(blob) {
return {tone: tone, blob: blob};
});
}).then(function(result) {
done(function() {
assert.equal(result.tone.name, 'My ringtone');
assert.ok(/^custom:/.test(result.tone.id));
assert.ok(/^blob:/.test(result.tone.url));
assert.equal(result.blob, testBlob);
assert.equal(result.tone.shareable, true);
assert.equal(result.tone.deletable, true);
});
}, function(error) {
done(error);
});
});
});
suite('non-empty database', function() {
var testTone;
var toneDeepEqual = function (test, expected, msg) {
assert.equal(test.name, expected.name, msg);
assert.equal(test.blob, expected.blob, msg);
assert.equal(test.id, expected.id, msg);
assert.equal(test.subtitle, expected.subtitle, msg);
};
setup(function(done) {
var info = {name: 'My ringtone', blob: testBlob};
window.customRingtones.add(info).then(function(tone) {
testTone = tone;
done();
}, function(error) {
done(error);
});
});
test('list()', function(done) {
window.customRingtones.list().then(function(tones) {
done(function() {
assert.ok(Array.isArray(tones));
assert.equal(tones.length, 1);
toneDeepEqual(tones[0], testTone);
});
}, function(error) {
done(error);
});
});
test('remove()', function(done) {
window.customRingtones.remove(testTone.id).then(function() {
return window.customRingtones.list().then(function(tones) {
return tones;
});
}).then(function(tones) {
done(function() {
assert.equal(tones.length, 0);
});
}, function(error) {
done(error);
});
});
test('get()', function(done) {
window.customRingtones.get(testTone.id).then(function(tone) {
done(function() {
toneDeepEqual(tone, testTone);
});
}, function(error) {
done(error);
});
});
});
suite('deduplication', function() {
var toneID;
setup(function(done) {
var info = {name: 'My ringtone'};
window.customRingtones.add(info).then(function(tone) {
toneID = tone.id;
done();
}, function(error) {
done(error);
});
});
test('add with explicit num', function(done) {
var info = {name: 'My other ringtone (2)'};
window.customRingtones.add(info).then(function(tone) {
done(function() {
assert.equal(tone.name, info.name);
});
}, function(error) {
done(error);
});
});
test('add with explicit num of 0', function(done) {
var info = {name: 'My other ringtone (0)'};
window.customRingtones.add(info).then(function(tone) {
done(function() {
assert.equal(tone.name, info.name);
});
}, function(error) {
done(error);
});
});
test('add duplicate', function(done) {
var info = {name: 'My ringtone'};
window.customRingtones.add(info).then(function(tone) {
done(function() {
assert.equal(tone.name, info.name + ' (1)');
});
}, function(error) {
done(error);
});
});
test('add duplicate with different subtitle', function(done) {
var info = {name: 'My ringtone', subtitle: 'Bob\'s Ringtones'};
window.customRingtones.add(info).then(function(tone) {
done(function() {
assert.equal(tone.name, info.name);
});
}, function(error) {
done(error);
});
});
test('add duplicate with explicit num', function(done) {
var info = {name: 'My ringtone'};
window.customRingtones.add(info).then(function(tone) {
var info2 = {name: 'My ringtone (1)'};
return window.customRingtones.add(info2);
}).then(function(tone) {
done(function() {
assert.equal(tone.name, info.name + ' (2)');
});
}, function(error) {
done(error);
});
});
// This test ensure that if we have "My ringtone (1)" but no "My ringtone",
// we can add a ringtone named "My ringtone" (without a unique num)
test('add duplicate with missing unique nums', function(done) {
var info = {name: 'My ringtone'};
window.customRingtones.add(info).then(function(tone) {
return window.customRingtones.remove(toneID);
}).then(function() {
return window.customRingtones.add(info);
}).then(function(tone) {
done(function() {
assert.equal(tone.name, info.name);
});
}, function(error) {
done(error);
});
});
});
});

View file

@ -0,0 +1,30 @@
'use strict';
var MockEnumerateAll = function(storages, dir, options) {
var callback = null;
var index = 0;
function nextFile(self) {
if (!(dir in MockEnumerateAll.files)) {
self.result = null;
} else if (index == MockEnumerateAll.files[dir].length) {
self.result = null;
} else {
self.result = { name: dir + '/' + MockEnumerateAll.files[dir][index++] };
}
callback.call(self);
}
return {
result: null,
set onsuccess(cb) {
callback = cb;
nextFile(this);
},
continue: function() {
nextFile(this);
}
};
};
MockEnumerateAll.files = {};

View file

@ -0,0 +1,113 @@
/* global MockGetDeviceStorage, MockGetDeviceStorages, MockEnumerateAll,
assert, require, setup, suite, suiteSetup, suiteTeardown,
teardown, test */
'use strict';
require('/shared/test/unit/mocks/mock_navigator_getdevicestorage.js');
require('/shared/test/unit/mocks/mock_navigator_getdevicestorages.js');
require('/test/unit/mock_enumerate_all.js');
suite('sd card ringtones', function() {
var toneTypes = ['ringtone', 'alerttone'];
var folderNames = {'ringtone': 'Ringtones', 'alerttone': 'Notifications'};
var realDeviceStorage, realDeviceStorages;
suiteSetup(function(done) {
realDeviceStorages = navigator.getDeviceStorages;
navigator.getDeviceStorages = MockGetDeviceStorages;
realDeviceStorage = navigator.getDeviceStorage;
navigator.getDeviceStorage = MockGetDeviceStorage;
window.enumerateAll = MockEnumerateAll;
require('/js/sd_card_ringtones.js', function() {
done();
});
});
suiteTeardown(function() {
navigator.getDeviceStorages = realDeviceStorages;
realDeviceStorages = null;
navigator.getDeviceStorage = realDeviceStorage;
realDeviceStorage = null;
});
toneTypes.forEach(function(toneType) {
suite(toneType, function() {
suite('empty sd card', function() {
test('list()', function(done) {
window.sdCardRingtones.list(toneType).then(function(tones) {
done(function() {
assert.ok(Array.isArray(tones));
assert.equal(tones.length, 0);
});
}, function(error) {
done(error);
});
});
});
suite('non-empty sd card', function() {
setup(function() {
MockEnumerateAll.files[folderNames.ringtone] = [
'ringtone-1.mp3', 'folder/ringtone-2.mp3', '.ringtone-3.mp3',
'.hidden/ringtone-4.mp3'
];
MockEnumerateAll.files[folderNames.alerttone] = [
'alerttone-1.mp3', 'folder/alerttone-2.mp3', '.alerttone-3.mp3',
'.hidden/alerttone-4.mp3'];
});
teardown(function() {
MockEnumerateAll.files = {};
});
test('list()', function(done) {
window.sdCardRingtones.list(toneType).then(function(tones) {
done(function() {
assert.ok(Array.isArray(tones));
assert.equal(tones.length, 2);
var expected = [
{ name: toneType + '-1',
filename: folderNames[toneType] + '/' + toneType + '-1.mp3' },
{ name: toneType + '-2',
filename: folderNames[toneType] + '/folder/' + toneType +
'-2.mp3' }
];
for (var i = 0; i < expected.length; i++) {
assert.equal(tones[i].name, expected[i].name);
assert.equal(tones[i].filename, expected[i].filename);
assert.equal(tones[i].id, 'sdcard:' + expected[i].filename);
assert.equal(tones[i].shareable, true);
assert.equal(tones[i].deletable, true);
}
});
}, function(error) {
done(error);
});
});
test('get()', function(done) {
var filename = folderNames[toneType] + '/' + toneType + '-1.mp3';
var id = 'sdcard:' + filename;
window.sdCardRingtones.get(id).then(function(tone) {
done(function() {
assert.equal(tone.name, toneType + '-1');
assert.equal(tone.filename, filename);
assert.equal(tone.id, id);
assert.equal(tone.shareable, true);
assert.equal(tone.deletable, true);
});
}, function(error) {
done(error);
});
});
});
});
});
});

View file

@ -0,0 +1,198 @@
/* global assert, MockNavigatorSettings, Promise, require, setup, suite,
teardown, test */
'use strict';
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/js/built_in_ringtones.js');
require('/js/system_tones.js');
function DummyTone(name, id, blob) {
this.name = name;
this.id = id;
this.blob = blob;
}
DummyTone.prototype = {
getBlob: function() {
return new Promise(function(resolve, reject) {
resolve(this.blob);
}.bind(this));
}
};
function getSettingsBase(toneType) {
switch (toneType) {
case 'ringtone':
return 'dialer.ringtone';
case 'alerttone':
return 'notification.ringtone';
default:
throw new Error('tone type not supported');
}
}
function getSettings(settingsKeys) {
return Promise.all(settingsKeys.map(function(setting) {
return new Promise(function(resolve, reject) {
var req = navigator.mozSettings.createLock().get(setting);
req.onsuccess = function() {
resolve(req.result[setting]);
};
req.onerror = function() {
reject(req.error);
};
});
}));
}
suite('system tones', function() {
var nativeSettings;
setup(function() {
nativeSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
// Mock mozL10n so that setTone works.
navigator.mozL10n = {
once: function(callback) {
callback();
},
get: function(key) {
return null;
}
};
});
teardown(function() {
MockNavigatorSettings.mTeardown();
navigator.mozSettings = nativeSettings;
});
suite('systemTones.set()', function() {
['ringtone', 'alerttone'].forEach(function(toneType) {
test(toneType, function(done) {
var tone = new DummyTone('My Tone', 'id', null);
var key = getSettingsBase(toneType);
var settingsKeys = [key, key + '.name', key + '.id'];
window.systemTones.set(toneType, tone).then(
getSettings.bind(null, settingsKeys)
).then(
function(settings) {
done(function() {
assert.equal(settings[0], null);
assert.equal(settings[1], 'My Tone');
assert.equal(settings[2], 'id');
});
},
function(error) {
done(error);
}
);
});
});
});
suite('systemTones.getDefault()', function() {
var defaultIDs = {};
setup(function(done) {
var settings = {};
settings[getSettingsBase('ringtone') + '.default.id'] =
defaultIDs.ringtone = 'builtin:ringtone/ringer_firefox';
settings[getSettingsBase('alerttone') + '.default.id'] =
defaultIDs.alerttone = 'builtin:alerttone/notifier_firefox';
var req = navigator.mozSettings.createLock().set(settings);
req.onsuccess = function() { done(); };
req.onerror = function() {
done(req.error);
};
});
['ringtone', 'alerttone'].forEach(function(toneType) {
test(toneType, function(done) {
window.systemTones.getDefault(toneType).then(function(tone) {
done(function() {
assert.equal(tone.id, defaultIDs[toneType]);
assert.equal(tone.shareable, true);
assert.equal(tone.deletable, false);
});
}, function(error) {
done(error);
});
});
});
});
suite('systemTones.isInUse()', function() {
['ringtone', 'alerttone'].forEach(function(toneType) {
test(toneType + ', in use', function(done) {
var tone = new DummyTone('My Tone', 'id', null);
window.systemTones.set(toneType, tone).then(function() {
return window.systemTones.isInUse(tone);
}).then(
function(inUseAs) {
done(function() {
assert.equal(inUseAs.length, 1);
assert.equal(inUseAs[0], toneType);
});
},
function(error) {
done(error);
}
);
});
test(toneType + ', not in use', function(done) {
var tone = new DummyTone('My Tone', 'id', null);
var unusedTone = new DummyTone('My Other Tone', 'id2', null);
window.systemTones.set(toneType, tone).then(function() {
return window.systemTones.isInUse(unusedTone);
}).then(
function(inUseAs) {
done(function() {
assert.equal(inUseAs.length, 0);
});
},
function(error) {
done(error);
}
);
});
});
test('ringtone and alerttone, in use', function(done) {
var tone = new DummyTone('My Tone', 'id', null);
window.systemTones.set('ringtone', tone).then(function() {
return window.systemTones.set('alerttone', tone);
}).then(function() {
return window.systemTones.isInUse(tone);
}).then(
function(inUseAs) {
done(function() {
assert.equal(inUseAs.length, 2);
assert.equal(inUseAs[0], 'ringtone');
assert.equal(inUseAs[1], 'alerttone');
});
},
function(error) {
done(error);
}
);
});
});
});