From 6d0af5f8bee621f9d3928e9fca7993118b20c16b Mon Sep 17 00:00:00 2001 From: Mike de Boer Date: Fri, 27 Mar 2015 14:31:53 +0100 Subject: [PATCH] Bug 1132301: Part 2 - add navigator.mozLoop methods to allow interaction between Loop and the Social API. r=Standard8,mixedpuppy --- browser/components/loop/MozLoopAPI.jsm | 199 +++++++++++++++++- .../loop/test/mochitest/browser.ini | 1 + .../mochitest/browser_mozLoop_socialShare.js | 139 ++++++++++++ .../components/loop/test/mochitest/head.js | 28 +++ 4 files changed, 363 insertions(+), 4 deletions(-) create mode 100644 browser/components/loop/test/mochitest/browser_mozLoop_socialShare.js diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index 68a8f03b39b..a33044c7c11 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -27,6 +27,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.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) @@ -199,6 +201,10 @@ function injectLoopAPI(targetWindow) { let roomsAPI; let callsAPI; let savedWindowListeners = new Map(); + let socialProviders; + const kShareWidgetId = "social-share-button"; + let socialShareButtonListenersAdded = false; + let api = { /** @@ -912,20 +918,205 @@ 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") { + 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); diff --git a/browser/components/loop/test/mochitest/browser.ini b/browser/components/loop/test/mochitest/browser.ini index a499988fb06..d25f580b25e 100644 --- a/browser/components/loop/test/mochitest/browser.ini +++ b/browser/components/loop/test/mochitest/browser.ini @@ -22,6 +22,7 @@ skip-if = buildapp == 'mulet' [browser_mozLoop_pluralStrings.js] [browser_mozLoop_sharingListeners.js] skip-if = e10s +[browser_mozLoop_socialShare.js] [browser_mozLoop_telemetry.js] skip-if = e10s [browser_toolbarbutton.js] diff --git a/browser/components/loop/test/mochitest/browser_mozLoop_socialShare.js b/browser/components/loop/test/mochitest/browser_mozLoop_socialShare.js new file mode 100644 index 00000000000..7659dd8ee86 --- /dev/null +++ b/browser/components/loop/test/mochitest/browser_mozLoop_socialShare.js @@ -0,0 +1,139 @@ +/* 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", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png", + shareURL: "https://example.com/browser/browser/base/content/test/social/share.html" +}; +const kShareProviderInvalid = { + name: "provider 1", + origin: "https://example2.com" +}; + +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); +}); diff --git a/browser/components/loop/test/mochitest/head.js b/browser/components/loop/test/mochitest/head.js index 086b53fd3e7..2b80ff6ec9a 100644 --- a/browser/components/loop/test/mochitest/head.js +++ b/browser/components/loop/test/mochitest/head.js @@ -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