Bug 1132301: Part 2 - add navigator.mozLoop methods to allow interaction between Loop and the Social API. r=Standard8,mixedpuppy

This commit is contained in:
Mike de Boer 2015-04-10 13:23:05 +02:00
parent 3ed15773de
commit 5defbba425
5 changed files with 391 additions and 9 deletions

View File

@ -29,6 +29,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
"resource://gre/modules/UpdateChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
"resource:///modules/UITour.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Social",
"resource:///modules/Social.jsm");
XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
return Cc["@mozilla.org/xre/app-info;1"]
.getService(Ci.nsIXULAppInfo)
@ -207,6 +209,10 @@ function injectLoopAPI(targetWindow) {
let roomsAPI;
let callsAPI;
let savedWindowListeners = new Map();
let socialProviders;
const kShareWidgetId = "social-share-button";
let socialShareButtonListenersAdded = false;
let api = {
/**
@ -918,20 +924,212 @@ function injectLoopAPI(targetWindow) {
value: function(windowId, active) {
MozLoopService.setScreenShareState(windowId, active);
}
},
/**
* Checks if the Social Share widget is available in any of the registered
* widget areas (navbar, MenuPanel, etc).
*
* @return {Boolean} `true` if the widget is available and `false` when it's
* still in the Customization palette.
*/
isSocialShareButtonAvailable: {
enumerable: true,
writable: true,
value: function() {
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (!win || !win.CustomizableUI) {
return false;
}
let widget = win.CustomizableUI.getWidget(kShareWidgetId);
if (widget) {
if (!socialShareButtonListenersAdded) {
let eventName = "social:" + kShareWidgetId;
Services.obs.addObserver(onShareWidgetChanged, eventName + "-added", false);
Services.obs.addObserver(onShareWidgetChanged, eventName + "-removed", false);
socialShareButtonListenersAdded = true;
}
return !!widget.areaType;
}
return false;
}
},
/**
* Add the Social Share widget to the navbar area, but only when it's not
* located anywhere else than the Customization palette.
*/
addSocialShareButton: {
enumerable: true,
writable: true,
value: function() {
// Don't do anything if the button is already available.
if (api.isSocialShareButtonAvailable.value()) {
return;
}
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (!win || !win.CustomizableUI) {
return;
}
win.CustomizableUI.addWidgetToArea(kShareWidgetId, win.CustomizableUI.AREA_NAVBAR);
}
},
/**
* Activates the Social Share panel with the Social Provider panel opened
* when the popup open.
*/
addSocialShareProvider: {
enumerable: true,
writable: true,
value: function() {
// Don't do anything if the button is _not_ available.
if (!api.isSocialShareButtonAvailable.value()) {
return;
}
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (!win || !win.SocialShare) {
return;
}
win.SocialShare.showDirectory();
}
},
/**
* Returns a sorted list of Social Providers that can share URLs. See
* `updateSocialProvidersCache()` for more information.
*
* @return {Array} Sorted list of share-capable Social Providers.
*/
getSocialShareProviders: {
enumerable: true,
writable: true,
value: function() {
if (socialProviders) {
return socialProviders;
}
return updateSocialProvidersCache();
}
},
/**
* Share a room URL through a Social Provider with the provided title message.
* This action will open the share panel, which is anchored to the Social
* Share widget.
*
* @param {String} providerOrigin Identifier of the targeted Social Provider
* @param {String} roomURL URL that points to the standalone client
* @param {String} title Message that augments the URL inside the
* share message
* @param {String} [body] Optional longer message to be displayed
* similar to the body of an email
*/
socialShareRoom: {
enumerable: true,
writable: true,
value: function(providerOrigin, roomURL, title, body = null) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (!win || !win.SocialShare) {
return;
}
let graphData = {
url: roomURL,
title: title
};
if (body) {
graphData.body = body;
}
win.SocialShare.sharePage(providerOrigin, graphData);
}
}
};
function onStatusChanged(aSubject, aTopic, aData) {
let event = new targetWindow.CustomEvent("LoopStatusChanged");
/**
* Send an event to the content window to indicate that the state on the chrome
* side was updated.
*
* @param {name} name Name of the event, defaults to 'LoopStatusChanged'
*/
function sendEvent(name = "LoopStatusChanged") {
if (typeof targetWindow.CustomEvent != "function") {
MozLoopService.log.debug("Could not send event to content document, " +
"because it's being destroyed or we're in a unit test where " +
"`targetWindow` is mocked.");
return;
}
let event = new targetWindow.CustomEvent(name);
targetWindow.dispatchEvent(event);
};
}
function onStatusChanged(aSubject, aTopic, aData) {
sendEvent();
}
function onDOMWindowDestroyed(aSubject, aTopic, aData) {
if (targetWindow && aSubject != targetWindow)
return;
Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed");
Services.obs.removeObserver(onStatusChanged, "loop-status-changed");
};
// Stop listening for changes in the social provider list, if necessary.
if (socialProviders)
Services.obs.removeObserver(updateSocialProvidersCache, "social:providers-changed");
if (socialShareButtonListenersAdded) {
let eventName = "social:" + kShareWidgetId;
Services.obs.removeObserver(onShareWidgetChanged, eventName + "-added");
Services.obs.removeObserver(onShareWidgetChanged, eventName + "-removed");
}
}
function onShareWidgetChanged(aSubject, aTopic, aData) {
sendEvent("LoopShareWidgetChanged");
}
/**
* Retrieves a list of Social Providers from the Social API that are explicitly
* capable of sharing URLs.
* It also adds a listener that is fired whenever a new Provider is added or
* removed.
*
* @return {Array} Sorted list of share-capable Social Providers.
*/
function updateSocialProvidersCache() {
let providers = [];
for (let provider of Social.providers) {
if (!provider.shareURL) {
continue;
}
// Only pass the relevant data on to content.
providers.push({
iconURL: provider.iconURL,
name: provider.name,
origin: provider.origin
});
}
let providersWasSet = !!socialProviders;
// Replace old with new.
socialProviders = cloneValueInto(providers.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase())), targetWindow);
// Start listening for changes in the social provider list, if we're not
// doing that yet.
if (!providersWasSet) {
Services.obs.addObserver(updateSocialProvidersCache, "social:providers-changed", false);
} else {
// Dispatch an event to content to let stores freshen-up.
sendEvent("LoopSocialProvidersChanged");
}
return socialProviders;
}
let contentObj = Cu.createObjectIn(targetWindow);
Object.defineProperties(contentObj, api);

View File

@ -20,6 +20,7 @@ skip-if = e10s
[browser_mozLoop_doNotDisturb.js]
skip-if = buildapp == 'mulet'
[browser_mozLoop_pluralStrings.js]
[browser_mozLoop_socialShare.js]
[browser_mozLoop_sharingListeners.js]
skip-if = e10s
[browser_mozLoop_telemetry.js]

View File

@ -54,9 +54,19 @@ function promiseWindowIdReceivedNewTab(handlers = []) {
return Promise.all(promiseHandlers);
};
function removeTabs() {
function promiseRemoveTab(tab) {
return new Promise(resolve => {
gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
resolve();
});
gBrowser.removeTab(tab);
});
}
function* removeTabs() {
for (let createdTab of createdTabs) {
gBrowser.removeTab(createdTab);
yield promiseRemoveTab(createdTab);
}
createdTabs = [];
@ -79,7 +89,7 @@ add_task(function* test_singleListener() {
// Now remove the listener.
gMozLoopAPI.removeBrowserSharingListener(handlers[0].listener);
removeTabs();
yield removeTabs();
});
add_task(function* test_multipleListener() {
@ -122,7 +132,7 @@ add_task(function* test_multipleListener() {
// Cleanup.
gMozLoopAPI.removeBrowserSharingListener(handlers[1].listener);
removeTabs();
yield removeTabs();
});
add_task(function* test_infoBar() {
@ -186,6 +196,6 @@ add_task(function* test_infoBar() {
// Cleanup.
gMozLoopAPI.removeBrowserSharingListener(handlers[0].listener);
removeTabs();
yield removeTabs();
Services.prefs.clearUserPref(kPrefBrowserSharingInfoBar);
});

View File

@ -0,0 +1,145 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This is an integration test from navigator.mozLoop through to the end
* effects - rather than just testing MozLoopAPI alone.
*/
Cu.import("resource://gre/modules/Promise.jsm");
const {SocialService} = Cu.import("resource://gre/modules/SocialService.jsm", {});
add_task(loadLoopPanel);
const kShareWidgetId = "social-share-button";
const kShareProvider = {
name: "provider 1",
origin: "https://example.com",
iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png",
shareURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html"
};
const kShareProviderInvalid = {
name: "provider 1",
origin: "https://example2.com"
};
registerCleanupFunction(function* () {
yield new Promise(resolve => SocialService.disableProvider(kShareProvider.origin, resolve));
yield new Promise(resolve => SocialService.disableProvider(kShareProviderInvalid.origin, resolve));
Assert.strictEqual(Social.providers.length, 0, "all providers should be removed");
SocialShare.uninit();
});
add_task(function* test_mozLoop_isSocialShareButtonAvailable() {
Assert.ok(gMozLoopAPI, "mozLoop should exist");
// First make sure the Social Share button is not available. This is probably
// already the case, but make it explicit here.
CustomizableUI.removeWidgetFromArea(kShareWidgetId);
Assert.ok(!gMozLoopAPI.isSocialShareButtonAvailable(),
"Social Share button should not be available");
// Add the widget to the navbar.
CustomizableUI.addWidgetToArea(kShareWidgetId, CustomizableUI.AREA_NAVBAR);
Assert.ok(gMozLoopAPI.isSocialShareButtonAvailable(),
"Social Share button should be available");
// Add the widget to the MenuPanel.
CustomizableUI.addWidgetToArea(kShareWidgetId, CustomizableUI.AREA_PANEL);
Assert.ok(gMozLoopAPI.isSocialShareButtonAvailable(),
"Social Share button should still be available");
// Test button removal during the same session.
CustomizableUI.removeWidgetFromArea(kShareWidgetId);
Assert.ok(!gMozLoopAPI.isSocialShareButtonAvailable(),
"Social Share button should not be available");
});
add_task(function* test_mozLoop_addSocialShareButton() {
gMozLoopAPI.addSocialShareButton();
Assert.ok(gMozLoopAPI.isSocialShareButtonAvailable(),
"Social Share button should be available");
let widget = CustomizableUI.getWidget(kShareWidgetId);
Assert.strictEqual(widget.areaType, CustomizableUI.TYPE_TOOLBAR,
"Social Share button should be placed in the navbar");
CustomizableUI.removeWidgetFromArea(kShareWidgetId);
});
add_task(function* test_mozLoop_addSocialShareProvider() {
gMozLoopAPI.addSocialShareButton();
gMozLoopAPI.addSocialShareProvider();
yield promiseWaitForCondition(() => SocialShare.panel.state == "open");
Assert.equal(SocialShare.iframe.getAttribute("src"), "about:providerdirectory",
"Provider directory page should be visible");
SocialShare.panel.hidePopup();
CustomizableUI.removeWidgetFromArea(kShareWidgetId);
});
add_task(function* test_mozLoop_getSocialShareProviders() {
Assert.strictEqual(gMozLoopAPI.getSocialShareProviders().length, 0,
"Provider list should be empty initially");
// Add a provider.
yield new Promise(resolve => SocialService.addProvider(kShareProvider, resolve));
let providers = gMozLoopAPI.getSocialShareProviders();
Assert.strictEqual(providers.length, 1,
"The newly added provider should be part of the list");
let provider = providers[0];
Assert.strictEqual(provider.iconURL, kShareProvider.iconURL, "Icon URLs should match");
Assert.strictEqual(provider.name, kShareProvider.name, "Names should match");
Assert.strictEqual(provider.origin, kShareProvider.origin, "Origins should match");
// Add another provider that should not be picked up by Loop.
yield new Promise(resolve => SocialService.addProvider(kShareProviderInvalid, resolve));
providers = gMozLoopAPI.getSocialShareProviders();
Assert.strictEqual(providers.length, 1,
"The newly added provider should not be part of the list");
// Let's add a valid second provider object.
let provider2 = Object.create(kShareProvider);
provider2.name = "Wildly different name";
provider2.origin = "https://example3.com";
yield new Promise(resolve => SocialService.addProvider(provider2, resolve));
providers = gMozLoopAPI.getSocialShareProviders();
Assert.strictEqual(providers.length, 2,
"The newly added provider should be part of the list");
Assert.strictEqual(providers[1].name, provider2.name,
"Providers should be ordered alphabetically");
// Remove the second valid provider.
yield new Promise(resolve => SocialService.disableProvider(provider2.origin, resolve));
providers = gMozLoopAPI.getSocialShareProviders();
Assert.strictEqual(providers.length, 1,
"The uninstalled provider should not be part of the list");
Assert.strictEqual(providers[0].name, kShareProvider.name, "Names should match");
});
add_task(function* test_mozLoop_socialShareRoom() {
gMozLoopAPI.addSocialShareButton();
gMozLoopAPI.socialShareRoom(kShareProvider.origin, "https://someroom.com", "Some Title");
yield promiseWaitForCondition(() => SocialShare.panel.state == "open");
Assert.equal(SocialShare.iframe.getAttribute("origin"), kShareProvider.origin,
"Origins should match");
Assert.equal(SocialShare.iframe.getAttribute("src"), kShareProvider.shareURL,
"Provider's share page should be displayed");
SocialShare.panel.hidePopup();
CustomizableUI.removeWidgetFromArea(kShareWidgetId);
});

View File

@ -71,6 +71,34 @@ function promiseGetMozLoopAPI() {
});
}
function waitForCondition(condition, nextTest, errorMsg) {
var tries = 0;
var interval = setInterval(function() {
if (tries >= 30) {
ok(false, errorMsg);
moveOn();
}
var conditionPassed;
try {
conditionPassed = condition();
} catch (e) {
ok(false, e + "\n" + e.stack);
conditionPassed = false;
}
if (conditionPassed) {
moveOn();
}
tries++;
}, 100);
var moveOn = function() { clearInterval(interval); nextTest(); };
}
function promiseWaitForCondition(aConditionFn) {
let deferred = Promise.defer();
waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass.");
return deferred.promise;
}
/**
* Loads the loop panel by clicking the button and waits for its open to complete.
* It also registers