Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-09-17 21:59:14 -04:00
commit 323aceac34
56 changed files with 2654 additions and 2195 deletions

File diff suppressed because it is too large Load Diff

View File

@ -258,12 +258,6 @@ XPCOMUtils.defineLazyGetter(this, "PageMenu", function() {
*/
function pageShowEventHandlers(persisted) {
XULBrowserWindow.asyncUpdateUI();
// The PluginClickToPlay events are not fired when navigating using the
// BF cache. |persisted| is true when the page is loaded from the
// BF cache, so this code reshows the notification if necessary.
if (persisted)
gPluginHandler.reshowClickToPlayNotification();
}
function UpdateBackForwardCommands(aWebNavigation) {
@ -780,13 +774,6 @@ var gBrowserInit = {
gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false);
// Note that the XBL binding is untrusted
gBrowser.addEventListener("PluginBindingAttached", gPluginHandler, true, true);
gBrowser.addEventListener("PluginCrashed", gPluginHandler, true);
gBrowser.addEventListener("PluginOutdated", gPluginHandler, true);
gBrowser.addEventListener("PluginInstantiated", gPluginHandler, true);
gBrowser.addEventListener("PluginRemoved", gPluginHandler, true);
gBrowser.addEventListener("NewPluginInstalled", gPluginHandler.newPluginInstalled, true);
Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false);
@ -2512,14 +2499,14 @@ let BrowserOnClick = {
let button = event.originalTarget;
if (button.id == "tryAgain") {
let browser = gBrowser.getBrowserForDocument(ownerDoc);
#ifdef MOZ_CRASHREPORTER
if (ownerDoc.getElementById("checkSendReport").checked) {
let browser = gBrowser.getBrowserForDocument(ownerDoc);
TabCrashReporter.submitCrashReport(browser);
}
#endif
TabCrashReporter.reloadCrashedTabs();
TabCrashReporter.reloadCrashedTab(browser);
}
},

View File

@ -16,6 +16,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
"resource://gre/modules/LoginManagerContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
"resource://gre/modules/InsecurePasswordUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
"resource:///modules/PluginContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
@ -493,6 +495,9 @@ ClickEventHandler.init();
ContentLinkHandler.init(this);
// TODO: Load this lazily so the JSM is run only if a relevant event/message fires.
let pluginContent = new PluginContent(global);
addEventListener("DOMWebNotificationClicked", function(event) {
sendAsyncMessage("DOMWebNotificationClicked", {});
}, false);

View File

@ -1315,11 +1315,11 @@ nsContextMenu.prototype = {
},
playPlugin: function() {
gPluginHandler._showClickToPlayNotification(this.browser, this.target, true);
gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
},
hidePlugin: function() {
gPluginHandler.hideClickToPlayOverlay(this.target);
gPluginHandler.contextMenuCommand(this.browser, this.target, "hide");
},
// Generate email address and put it on clipboard.

View File

@ -8,6 +8,72 @@ const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browse
const gTestRoot = getRootDirectory(gTestPath);
var gTestBrowser = null;
/**
* Frame script that will be injected into the test browser
* to cause the crash, and then manipulate the crashed plugin
* UI. Specifically, after the crash, we ensure that the
* crashed plugin UI is using the right style rules and that
* the submit URL opt-in defaults to checked. Then, we fill in
* a comment with the crash report, uncheck the submit URL
* opt-in, and send the crash reports.
*/
function frameScript() {
function fail(reason) {
sendAsyncMessage("test:crash-plugin:fail", {
reason: `Failure from frameScript: ${reason}`,
});
}
addMessageListener("test:crash-plugin", () => {
let doc = content.document;
addEventListener("PluginCrashed", (event) => {
let plugin = doc.getElementById("test");
if (!plugin) {
fail("Could not find plugin element");
return;
}
let getUI = (anonid) => {
return doc.getAnonymousElementByAttribute(plugin, "anonid", anonid);
};
let style = content.getComputedStyle(getUI("pleaseSubmit"));
if (style.display != "block") {
fail("Submission UI visibility is not correct. Expected block, "
+ " got " + style.display);
return;
}
getUI("submitComment").value = "a test comment";
if (!getUI("submitURLOptIn").checked) {
fail("URL opt-in should default to true.");
return;
}
getUI("submitURLOptIn").click();
getUI("submitButton").click();
});
let plugin = doc.getElementById("test");
try {
plugin.crash()
} catch(e) {
}
});
addMessageListener("test:plugin-submit-status", () => {
let doc = content.document;
let plugin = doc.getElementById("test");
let submitStatusEl =
doc.getAnonymousElementByAttribute(plugin, "anonid", "submitStatus");
let submitStatus = submitStatusEl.getAttribute("status");
sendAsyncMessage("test:plugin-submit-status:result", {
submitStatus: submitStatus,
});
});
}
// Test that plugin crash submissions still work properly after
// click-to-play activation.
@ -31,14 +97,18 @@ function test() {
let tab = gBrowser.loadOneTab("about:blank", { inBackground: false });
gTestBrowser = gBrowser.getBrowserForTab(tab);
gTestBrowser.addEventListener("PluginCrashed", onCrash, false);
let mm = gTestBrowser.messageManager;
mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", false);
mm.addMessageListener("test:crash-plugin:fail", (message) => {
ok(false, message.data.reason);
});
gTestBrowser.addEventListener("load", onPageLoad, true);
Services.obs.addObserver(onSubmitStatus, "crash-report-status", false);
registerCleanupFunction(function cleanUp() {
env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
env.set("MOZ_CRASHREPORTER_URL", serverURL);
gTestBrowser.removeEventListener("PluginCrashed", onCrash, false);
gTestBrowser.removeEventListener("load", onPageLoad, true);
Services.obs.removeObserver(onSubmitStatus, "crash-report-status");
gBrowser.removeCurrentTab();
@ -70,31 +140,8 @@ function afterBindingAttached() {
}
function pluginActivated() {
let plugin = gTestBrowser.contentDocument.getElementById("test");
try {
plugin.crash();
} catch (e) {
// The plugin crashed in the above call, an exception is expected.
}
}
function onCrash() {
try {
let plugin = gBrowser.contentDocument.getElementById("test");
let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin);
let style =
gBrowser.contentWindow.getComputedStyle(elt("pleaseSubmit"));
is(style.display, "block", "Submission UI visibility should be correct");
elt("submitComment").value = "a test comment";
is(elt("submitURLOptIn").checked, true, "URL opt-in should default to true");
EventUtils.synthesizeMouseAtCenter(elt("submitURLOptIn"), {}, gTestBrowser.contentWindow);
EventUtils.synthesizeMouseAtCenter(elt("submitButton"), {}, gTestBrowser.contentWindow);
// And now wait for the submission status notification.
}
catch (err) {
failWithException(err);
}
let mm = gTestBrowser.messageManager;
mm.sendAsyncMessage("test:crash-plugin");
}
function onSubmitStatus(subj, topic, data) {
@ -128,19 +175,23 @@ function onSubmitStatus(subj, topic, data) {
ok(val === undefined,
"URL should be absent from extra data when opt-in not checked");
// Execute this later in case the event to change submitStatus has not
// have been dispatched yet.
executeSoon(function () {
let plugin = gBrowser.contentDocument.getElementById("test");
let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin);
is(elt("submitStatus").getAttribute("status"), data,
"submitStatus data should match");
let submitStatus = null;
let mm = gTestBrowser.messageManager;
mm.addMessageListener("test:plugin-submit-status:result", (message) => {
submitStatus = message.data.submitStatus;
});
mm.sendAsyncMessage("test:plugin-submit-status");
let condition = () => submitStatus;
waitForCondition(condition, () => {
is(submitStatus, data, "submitStatus data should match");
finish();
}, "Waiting for submitStatus to be reported from frame script");
}
catch (err) {
failWithException(err);
}
finish();
}
function getPropertyBagValue(bag, key) {

View File

@ -29,11 +29,11 @@ function handleEvent() {
function part1() {
gBrowser.selectedBrowser.removeEventListener("PluginBindingAttached", handleEvent);
ok(PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should have a click-to-play notification in the initial tab");
gNextTest = part2;
gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
gNewWindow.addEventListener("load", handleEvent, true);
waitForNotificationPopup("click-to-play-plugins", gBrowser.selectedBrowser, () => {
gNextTest = part2;
gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
gNewWindow.addEventListener("load", handleEvent, true);
});
}
function part2() {
@ -62,10 +62,10 @@ function part4() {
function part5() {
gBrowser.selectedBrowser.removeEventListener("PluginBindingAttached", handleEvent);
ok(PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should have a click-to-play notification in the initial tab");
gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
waitForFocus(part6, gNewWindow);
waitForNotificationPopup("click-to-play-plugins", gBrowser.selectedBrowser, () => {
gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
waitForFocus(part6, gNewWindow);
});
}
function part6() {
@ -92,8 +92,10 @@ function part8() {
let plugin = gNewWindow.gBrowser.selectedBrowser.contentDocument.getElementById("test");
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
ok(objLoadingContent.activated, "plugin should be activated now");
waitForCondition(() => objLoadingContent.activated, shutdown, "plugin should be activated now");
}
function shutdown() {
gNewWindow.close();
gNewWindow = null;
finish();

View File

@ -74,13 +74,15 @@ function test1() {
let plugin = doc.getElementById("test");
ok(plugin, "Test 1, Found plugin in page");
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
ok(overlay.classList.contains("visible"), "Test 1, Plugin overlay should exist, not be hidden");
let closeIcon = doc.getAnonymousElementByAttribute(plugin, "anonid", "closeIcon")
waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
ok(overlay.classList.contains("visible"), "Test 1, Plugin overlay should exist, not be hidden");
let closeIcon = doc.getAnonymousElementByAttribute(plugin, "anonid", "closeIcon");
EventUtils.synthesizeMouseAtCenter(closeIcon, {}, frame.contentWindow);
let condition = () => !overlay.classList.contains("visible");
waitForCondition(condition, test2, "Test 1, Waited too long for the overlay to become invisible.");
EventUtils.synthesizeMouseAtCenter(closeIcon, {}, frame.contentWindow);
let condition = () => !overlay.classList.contains("visible");
waitForCondition(condition, test2, "Test 1, Waited too long for the overlay to become invisible.");
});
}
function test2() {

View File

@ -63,43 +63,37 @@ function runAfterPluginBindingAttached(func) {
// Tests for the notification bar for hidden plugins.
function test1() {
let notification = PopupNotifications.getNotification("click-to-play-plugins");
ok(notification, "Test 1: There should be a plugin notification");
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
() => {
info("Test 1 - expecting a notification bar for hidden plugins.");
waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
waitForNotificationBar("plugin-hidden", gTestBrowser, () => {
// Don't use setTestPluginEnabledState here because we already saved the
// prior value
getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
prepareTest(test2, gTestRoot + "plugin_small.html");
},
"Test 1, expected to have a plugin notification bar");
});
});
}
function test2() {
let notification = PopupNotifications.getNotification("click-to-play-plugins");
ok(notification, "Test 2: There should be a plugin notification");
info("Test 2 - expecting no plugin notification bar on visible plugins.");
waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
() => {
getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
prepareTest(test3, gTestRoot + "plugin_overlayed.html");
},
"Test 2, expected to not have a plugin notification bar");
waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
() => {
getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
prepareTest(test3, gTestRoot + "plugin_overlayed.html");
},
"expected to not have a plugin notification bar"
);
});
}
function test3() {
let notification = PopupNotifications.getNotification("click-to-play-plugins");
ok(notification, "Test 3: There should be a plugin notification");
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
test3b,
"Test 3, expected the plugin infobar to be triggered when plugin was overlayed");
info("Test 3 - expecting a plugin notification bar when plugins are overlaid");
waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
waitForNotificationBar("plugin-hidden", gTestBrowser, test3b);
});
}
function test3b()
@ -118,13 +112,10 @@ function test3b()
}
function test4() {
let notification = PopupNotifications.getNotification("click-to-play-plugins");
ok(notification, "Test 4: There should be a plugin notification");
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
test4b,
"Test 4, expected the plugin infobar to be triggered when plugin was overlayed");
info("Test 4 - expecting a plugin notification bar when plugins are overlaid offscreen")
waitForNotificationPopup("click-to-play-plugins", gTestBrowser, () => {
waitForNotificationBar("plugin-hidden", gTestBrowser, test4b);
});
}
function test4b() {
@ -141,9 +132,6 @@ function test4b() {
prepareTest(runAfterPluginBindingAttached(test5), gHttpTestRoot + "plugin_small.html");
}
// Test that the notification bar is getting dismissed when directly activating plugins
// via the doorhanger.
function test5() {
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
@ -151,23 +139,27 @@ function test5() {
"Test 5, expected a notification bar for hidden plugins");
}
// Test that the notification bar is getting dismissed when directly activating plugins
// via the doorhanger.
function test6() {
let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
ok(notification, "Test 6, Should have a click-to-play notification");
let plugin = gTestBrowser.contentDocument.getElementById("test");
ok(plugin, "Test 6, Found plugin in page");
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
"Test 6, Plugin should be click-to-play");
info("Test 6 - expecting the doorhanger to be dismissed when directly activating plugins.");
waitForNotificationPopup("click-to-play-plugins", gTestBrowser, (notification) => {
let plugin = gTestBrowser.contentDocument.getElementById("test");
ok(plugin, "Test 6, Found plugin in page");
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
"Test 6, Plugin should be click-to-play");
// simulate "always allow"
notification.reshow();
PopupNotifications.panel.firstChild._primaryButton.click();
// simulate "always allow"
notification.reshow();
PopupNotifications.panel.firstChild._primaryButton.click();
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
test7,
"Test 6, expected the notification bar for hidden plugins to get dismissed");
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
test7,
"Test 6, expected the notification bar for hidden plugins to get dismissed");
});
}
function test7() {

View File

@ -60,10 +60,8 @@ function test1b() {
// Click the activate button on doorhanger to make sure it works
popupNotification.reshow();
PopupNotifications.panel.firstChild._primaryButton.click();
ok(objLoadingContent.activated, "Test 1b, Doorhanger should activate plugin");
test1c();
var condition = function() objLoadingContent.activated;
waitForCondition(condition, test1c, "Test 1b, Waited too long for plugin activation");
}
function test1c() {

View File

@ -36,12 +36,14 @@ function pluginBindingAttached() {
ok(testplugin, "should have test plugin");
var secondtestplugin = doc.getElementById("secondtest");
ok(!secondtestplugin, "should not yet have second test plugin");
var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
ok(notification, "should have popup notification");
// We don't set up the action list until the notification is shown
notification.reshow();
is(notification.options.pluginData.size, 1, "should be 1 type of plugin in the popup notification");
XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addSecondPlugin();
var notification;
waitForNotificationPopup("click-to-play-plugins", gTestBrowser, (notification => {
ok(notification, "should have popup notification");
// We don't set up the action list until the notification is shown
notification.reshow();
is(notification.options.pluginData.size, 1, "should be 1 type of plugin in the popup notification");
XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addSecondPlugin();
}));
} else if (gNumPluginBindingsAttached == 2) {
var doc = gTestBrowser.contentDocument;
var testplugin = doc.getElementById("test");
@ -51,8 +53,8 @@ function pluginBindingAttached() {
var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
ok(notification, "should have popup notification");
notification.reshow();
is(notification.options.pluginData.size, 2, "should be 2 types of plugin in the popup notification");
finish();
let condition = () => (notification.options.pluginData.size == 2);
waitForCondition(condition, finish, "Waited too long for 2 types of plugins in popup notification");
} else {
ok(false, "if we've gotten here, something is quite wrong");
}

View File

@ -59,11 +59,11 @@ function onCrash(event) {
is (propVal, val, "Correct property in detail propBag: " + name + ".");
}
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
let notification = notificationBox.getNotificationWithValue("plugin-crashed");
ok(notification, "Infobar was shown.");
is(notification.priority, notificationBox.PRIORITY_WARNING_MEDIUM, "Correct priority.");
is(notification.getAttribute("label"), "The GlobalTestPlugin plugin has crashed.", "Correct message.");
finish();
waitForNotificationBar("plugin-crashed", gTestBrowser, (notification) => {
let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
ok(notification, "Infobar was shown.");
is(notification.priority, notificationBox.PRIORITY_WARNING_MEDIUM, "Correct priority.");
is(notification.getAttribute("label"), "The GlobalTestPlugin plugin has crashed.", "Correct message.");
finish();
});
}

View File

@ -6,9 +6,80 @@ Cu.import("resource://gre/modules/CrashSubmit.jsm", this);
Cu.import("resource://gre/modules/Services.jsm");
const CRASH_URL = "http://example.com/browser/browser/base/content/test/plugins/plugin_crashCommentAndURL.html";
const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
/**
* Frame script that will be injected into the test browser
* to cause plugin crashes, and then manipulate the crashed plugin
* UI. The specific actions and checks that occur in the frame
* script for the crashed plugin UI are set in the
* test:crash-plugin message object sent from the parent. The actions
* and checks that the parent can specify are:
*
* pleaseSubmitStyle: the display style that the pleaseSubmit anonymous element
* should have - example "block", "none".
* submitComment: the comment that should be put into the crash report
* urlOptIn: true if the submitURLOptIn element should be checked.
* sendCrashMessage: if true, the frame script will send a
* test:crash-plugin:crashed message when the plugin has
* crashed. This is used for the last test case, and
* causes the frame script to skip any of the crashed
* plugin UI manipulation, since the last test shows
* no crashed plugin UI.
*/
function frameScript() {
function fail(reason) {
sendAsyncMessage("test:crash-plugin:fail", {
reason: `Failure from frameScript: ${reason}`,
});
}
addMessageListener("test:crash-plugin", (message) => {
addEventListener("PluginCrashed", function onPluginCrashed(event) {
removeEventListener("PluginCrashed", onPluginCrashed);
let doc = content.document;
let plugin = doc.getElementById("plugin");
if (!plugin) {
fail("Could not find plugin element");
return;
}
let getUI = (anonid) => {
return doc.getAnonymousElementByAttribute(plugin, "anonid", anonid);
};
let style = content.getComputedStyle(getUI("pleaseSubmit"));
if (style.display != message.data.pleaseSubmitStyle) {
fail("Submission UI visibility is not correct. Expected " +
`${message.data.pleaseSubmitStyle} and got ${style.display}`);
return;
}
if (message.data.sendCrashMessage) {
let propBag = event.detail.QueryInterface(Ci.nsIPropertyBag2);
let crashID = propBag.getPropertyAsAString("pluginDumpID");
sendAsyncMessage("test:crash-plugin:crashed", {
crashID: crashID,
});
return;
}
if (message.data.submitComment) {
getUI("submitComment").value = message.data.submitComment;
}
getUI("submitURLOptIn").checked = message.data.urlOptIn;
getUI("submitButton").click();
});
let plugin = content.document.getElementById("test");
try {
plugin.crash()
} catch(e) {
}
});
}
function test() {
// Crashing the plugin takes up a lot of time, so extend the test timeout.
requestLongerTimeout(runs.length);
@ -29,14 +100,18 @@ function test() {
let tab = gBrowser.loadOneTab("about:blank", { inBackground: false });
let browser = gBrowser.getBrowserForTab(tab);
browser.addEventListener("PluginCrashed", onCrash, false);
let mm = browser.messageManager;
mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", true);
mm.addMessageListener("test:crash-plugin:fail", (message) => {
ok(false, message.data.reason);
});
Services.obs.addObserver(onSubmitStatus, "crash-report-status", false);
registerCleanupFunction(function cleanUp() {
env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
env.set("MOZ_CRASHREPORTER_URL", serverURL);
gBrowser.selectedBrowser.removeEventListener("PluginCrashed", onCrash,
false);
Services.obs.removeObserver(onSubmitStatus, "crash-report-status");
gBrowser.removeCurrentTab();
});
@ -76,6 +151,24 @@ function doNextRun() {
memo[arg] = currentRun[arg];
return memo;
}, {});
let mm = gBrowser.selectedBrowser.messageManager;
if (!currentRun.shouldSubmittionUIBeVisible) {
mm.addMessageListener("test:crash-plugin:crash", function onCrash(message) {
mm.removeMessageListener("test:crash-plugin:crash", onCrash);
ok(!!message.data.crashID, "pluginDumpID should be set");
CrashSubmit.delete(message.data.crashID);
doNextRun();
});
}
mm.sendAsyncMessage("test:crash-plugin", {
pleaseSubmitStyle: currentRun.shouldSubmissionUIBeVisible ? "block" : "none",
submitComment: currentRun.comment,
urlOptIn: currentRun.urlOptIn,
sendOnCrashMessage: !currentRun.shouldSubmissionUIBeVisible,
});
gBrowser.loadURI(CRASH_URL + "?" +
encodeURIComponent(JSON.stringify(args)));
// And now wait for the crash.
@ -86,37 +179,6 @@ function doNextRun() {
}
}
function onCrash(event) {
try {
let plugin = gBrowser.contentDocument.getElementById("plugin");
let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin);
let style =
gBrowser.contentWindow.getComputedStyle(elt("pleaseSubmit"));
is(style.display,
currentRun.shouldSubmissionUIBeVisible ? "block" : "none",
"Submission UI visibility should be correct");
if (!currentRun.shouldSubmissionUIBeVisible) {
// Done with this run. We don't submit the crash, so we will have to
// remove the dump manually.
let propBag = event.detail.QueryInterface(Ci.nsIPropertyBag2);
let crashID = propBag.getPropertyAsAString("pluginDumpID");
ok(!!crashID, "pluginDumpID should be set");
CrashSubmit.delete(crashID);
doNextRun();
return;
}
elt("submitComment").value = currentRun.comment;
elt("submitURLOptIn").checked = currentRun.urlOptIn;
elt("submitButton").click();
// And now wait for the submission status notification.
}
catch (err) {
failWithException(err);
doNextRun();
}
}
function onSubmitStatus(subj, topic, data) {
try {
// Wait for success or failed, doesn't matter which.

View File

@ -56,6 +56,7 @@ TabOpenListener.prototype = {
function test() {
waitForExplicitFinish();
SimpleTest.requestCompleteLog();
requestLongerTimeout(2);
registerCleanupFunction(function() {
clearAllPluginPermissions();
@ -797,7 +798,10 @@ function test24a() {
// simulate "always allow"
notification.reshow();
PopupNotifications.panel.firstChild._primaryButton.click();
prepareTest(test24b, gHttpTestRoot + "plugin_test.html");
waitForCondition(() => objLoadingContent.activated, () => {
prepareTest(test24b, gHttpTestRoot + "plugin_test.html");
}, "Test 24a, plugin should now be activated.");
}
// did the "always allow" work as intended?
@ -805,11 +809,11 @@ function test24b() {
var plugin = gTestBrowser.contentDocument.getElementById("test");
ok(plugin, "Test 24b, Found plugin in page");
var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
ok(objLoadingContent.activated, "Test 24b, plugin should be activated");
setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml",
function() {
prepareTest(runAfterPluginBindingAttached(test24c), gHttpTestRoot + "plugin_test.html");
});
waitForCondition(() => objLoadingContent.activated, () => {
setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml", () => {
prepareTest(runAfterPluginBindingAttached(test24c), gHttpTestRoot + "plugin_test.html");
});
}, "Test 24b, plugin should be activated");
}
// the plugin is now blocklisted, so it should not automatically load
@ -820,13 +824,13 @@ function test24c() {
ok(plugin, "Test 24c, Found plugin in page");
var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "Test 24c, Plugin should be vulnerable/updatable");
ok(!objLoadingContent.activated, "Test 24c, plugin should not be activated");
waitForCondition(() => !objLoadingContent.activated, () => {
// simulate "always allow"
notification.reshow();
PopupNotifications.panel.firstChild._primaryButton.click();
// simulate "always allow"
notification.reshow();
PopupNotifications.panel.firstChild._primaryButton.click();
prepareTest(test24d, gHttpTestRoot + "plugin_test.html");
prepareTest(test24d, gHttpTestRoot + "plugin_test.html");
}, "Test 24c, plugin should not be activated");
}
// We should still be able to always allow a plugin after we've seen that it's
@ -835,15 +839,14 @@ function test24d() {
var plugin = gTestBrowser.contentDocument.getElementById("test");
ok(plugin, "Test 24d, Found plugin in page");
var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
ok(objLoadingContent.activated, "Test 24d, plugin should be activated");
// this resets the vulnerable plugin permission
setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml",
function() {
clearAllPluginPermissions();
resetBlocklist();
prepareTest(test25, gTestRoot + "plugin_syncRemoved.html");
});
waitForCondition(() => objLoadingContent.activated, () => {
// this resets the vulnerable plugin permission
setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", () => {
clearAllPluginPermissions();
resetBlocklist();
prepareTest(test25, gTestRoot + "plugin_syncRemoved.html");
});
}, "Test 24d, plugin should be activated");
}
function test25() {

View File

@ -108,3 +108,28 @@ function setAndUpdateBlocklist(aURL, aCallback) {
function resetBlocklist() {
Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL);
}
function waitForNotificationPopup(notificationID, browser, callback) {
let notification;
waitForCondition(
() => (notification = PopupNotifications.getNotification(notificationID, browser)),
() => {
ok(notification, `Successfully got the ${notificationID} notification popup`);
callback(notification);
},
`Waited too long for the ${notificationID} notification popup`
);
}
function waitForNotificationBar(notificationID, browser, callback) {
let notification;
let notificationBox = gBrowser.getNotificationBox(browser);
waitForCondition(
() => (notification = notificationBox.getNotificationWithValue(notificationID)),
() => {
ok(notification, `Successfully got the ${notificationID} notification bar`);
callback(notification);
},
`Waited too long for the ${notificationID} notification bar`
);
}

View File

@ -1992,7 +1992,7 @@
return;
}
let host = gPluginHandler._getHostFromPrincipal(this.notification.browser.contentWindow.document.nodePrincipal);
let host = this.notification.options.host;
this._setupDescription("pluginActivateMultiple.message", null, host);
var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox");

View File

@ -1261,19 +1261,27 @@ CustomizeMode.prototype = {
},
onLWThemesMenuShowing: function(aEvent) {
AddonManager.getAddonsByTypes(["theme"], function(aThemes) {
function buildToolbarButton(doc, aTheme) {
function previewTheme(aEvent) {
LightweightThemeManager.previewTheme(aEvent.target.theme);
}
function resetPreview() {
LightweightThemeManager.resetPreview();
}
const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}";
const RECENT_LWT_COUNT = 5;
function previewTheme(aEvent) {
LightweightThemeManager.previewTheme(aEvent.target.theme);
}
function resetPreview() {
LightweightThemeManager.resetPreview();
}
AddonManager.getAddonByID(DEFAULT_THEME_ID, function(aDefaultTheme) {
let doc = this.window.document;
function buildToolbarButton(aTheme) {
let tbb = doc.createElement("toolbarbutton");
tbb.theme = aTheme;
tbb.setAttribute("label", aTheme.name);
tbb.setAttribute("image", aTheme.iconURL);
tbb.setAttribute("tooltiptext", aTheme.description);
if (aTheme.description)
tbb.setAttribute("tooltiptext", aTheme.description);
tbb.setAttribute("tabindex", "0");
tbb.classList.add("customization-lwtheme-menu-theme");
tbb.setAttribute("aria-checked", aTheme.isActive);
@ -1289,24 +1297,27 @@ CustomizeMode.prototype = {
return tbb;
}
const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}";
// Order the themes so the Default theme is always at the beginning.
aThemes.sort((a,b) => {a.id != DEFAULT_THEME_ID});
let doc = this.window.document;
let themes = [aDefaultTheme];
let lwts = LightweightThemeManager.usedThemes;
if (lwts.length > RECENT_LWT_COUNT)
lwts.length = RECENT_LWT_COUNT;
let currentLwt = LightweightThemeManager.currentTheme;
for (let lwt of lwts) {
lwt.isActive = !!currentLwt && (lwt.id == currentLwt.id);
themes.push(lwt);
}
let footer = doc.getElementById("customization-lwtheme-menu-footer");
let panel = footer.parentNode;
let themesInMyThemesSection = 0;
let recommendedLabel = doc.getElementById("customization-lwtheme-menu-recommended");
for (let theme of aThemes) {
// Only allow the Default full theme to be shown in this list.
if ("skinnable" in theme &&
theme.id != DEFAULT_THEME_ID) {
continue;
}
let tbb = buildToolbarButton(doc, theme);
for (let theme of themes) {
let tbb = buildToolbarButton(theme);
tbb.addEventListener("command", function() {
this.theme.userDisabled = false;
if ("userDisabled" in this.theme)
this.theme.userDisabled = false;
else
LightweightThemeManager.currentTheme = this.theme;
this.parentNode.hidePopup();
});
panel.insertBefore(tbb, recommendedLabel);
@ -1321,7 +1332,7 @@ CustomizeMode.prototype = {
for (let theme of recommendedThemes) {
theme.name = sb.GetStringFromName("lightweightThemes." + theme.id + ".name");
theme.description = sb.GetStringFromName("lightweightThemes." + theme.id + ".description");
let tbb = buildToolbarButton(doc, theme);
let tbb = buildToolbarButton(theme);
tbb.addEventListener("command", function() {
LightweightThemeManager.setLocalTheme(this.theme);
recommendedThemes = recommendedThemes.filter((aTheme) => { return aTheme.id != this.theme.id; });

View File

@ -453,7 +453,7 @@ loop.panel = (function(_, mozL10n) {
var UserIdentity = React.createClass({displayName: 'UserIdentity',
render: function() {
return (
React.DOM.p({className: "user-identity"},
React.DOM.p({className: "user-identity"},
this.props.displayName
)
);
@ -510,10 +510,10 @@ loop.panel = (function(_, mozL10n) {
)
),
React.DOM.div({className: "footer"},
React.DOM.div({className: "user-details"},
UserIdentity({displayName: displayName}),
React.DOM.div({className: "user-details"},
UserIdentity({displayName: displayName}),
AvailabilityDropdown(null)
),
),
AuthLink(null),
SettingsDropdown(null)
)
@ -522,37 +522,6 @@ loop.panel = (function(_, mozL10n) {
}
});
var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
/**
* DOM document object.
* @type {HTMLDocument}
*/
document: undefined,
routes: {
"": "home"
},
initialize: function(options) {
options = options || {};
if (!options.document) {
throw new Error("missing required document");
}
},
/**
* Default entry point.
*/
home: function() {
this._notifications.reset();
var client = new loop.Client({
baseServerUrl: navigator.mozLoop.serverUrl
});
this.loadReactComponent(
PanelView({client: client, notifications: this._notifications}));
}
});
/**
* Panel initialisation.
*/
@ -561,11 +530,15 @@ loop.panel = (function(_, mozL10n) {
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
router = new PanelRouter({
document: document,
notifications: new sharedModels.NotificationCollection()
var client = new loop.Client({
baseServerUrl: navigator.mozLoop.serverUrl
});
Backbone.history.start();
var notifications = new sharedModels.NotificationCollection()
React.renderComponent(PanelView({
client: client,
notifications: notifications}
), document.querySelector("#main"));
document.body.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection());
@ -582,7 +555,6 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
PanelRouter: PanelRouter,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView
};

View File

@ -522,37 +522,6 @@ loop.panel = (function(_, mozL10n) {
}
});
var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
/**
* DOM document object.
* @type {HTMLDocument}
*/
document: undefined,
routes: {
"": "home"
},
initialize: function(options) {
options = options || {};
if (!options.document) {
throw new Error("missing required document");
}
},
/**
* Default entry point.
*/
home: function() {
this._notifications.reset();
var client = new loop.Client({
baseServerUrl: navigator.mozLoop.serverUrl
});
this.loadReactComponent(
<PanelView client={client} notifications={this._notifications}/>);
}
});
/**
* Panel initialisation.
*/
@ -561,11 +530,15 @@ loop.panel = (function(_, mozL10n) {
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
router = new PanelRouter({
document: document,
notifications: new sharedModels.NotificationCollection()
var client = new loop.Client({
baseServerUrl: navigator.mozLoop.serverUrl
});
Backbone.history.start();
var notifications = new sharedModels.NotificationCollection()
React.renderComponent(<PanelView
client={client}
notifications={notifications}
/>, document.querySelector("#main"));
document.body.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection());
@ -582,7 +555,6 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
PanelRouter: PanelRouter,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView
};

View File

@ -22,11 +22,9 @@
<script type="text/javascript" src="loop/shared/js/utils.js"></script>
<script type="text/javascript" src="loop/shared/js/models.js"></script>
<script type="text/javascript" src="loop/shared/js/router.js"></script>
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
<script type="text/javascript" src="loop/js/panel.js"></script>
</body>
</html>

View File

@ -10,7 +10,10 @@
<link rel="stylesheet" type="text/css" href="shared/css/common.css">
<link rel="stylesheet" type="text/css" href="shared/css/conversation.css">
<link rel="stylesheet" type="text/css" href="css/webapp.css">
<link type="application/l10n" href="l10n/data.ini">
<link rel="localization" href="l10n/loop.{locale}.properties">
<meta name="locales" content="en-US" />
<meta name="default_locale" content="en-US" />
</head>
<body class="standalone">
@ -26,7 +29,7 @@
window.OTProperties.cssURL = window.OTProperties.assetURL + 'css/ot.css';
</script>
<script type="text/javascript" src="shared/libs/sdk.js"></script>
<script type="text/javascript" src="libs/l10n-gaia-4e35bf8f0569.js"></script>
<script type="text/javascript" src="libs/l10n-gaia-02ca67948fe8.js"></script>
<script type="text/javascript" src="shared/libs/react-0.11.1.js"></script>
<script type="text/javascript" src="shared/libs/jquery-2.1.0.js"></script>
<script type="text/javascript" src="shared/libs/lodash-2.4.1.js"></script>
@ -38,7 +41,6 @@
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/router.js"></script>
<script type="text/javascript" src="shared/js/websocket.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>

View File

@ -15,8 +15,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
baseServerUrl = loop.config.serverUrl;
sharedViews = loop.shared.views;
/**
* App router.
@ -332,8 +331,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
var privacy_notice_name = mozL10n.get("privacy_notice_link_text");
var tosHTML = mozL10n.get("legal_text_and_links", {
"terms_of_use_url": "<a target=_blank href='" +
"https://accounts.firefox.com/legal/terms'>" + tos_link_name + "</a>",
"terms_of_use_url": "<a target=_blank href='/legal/terms'>" +
tos_link_name + "</a>",
"privacy_notice_url": "<a target=_blank href='" +
"https://www.mozilla.org/privacy/'>" + privacy_notice_name + "</a>"
});
@ -414,31 +413,109 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
/**
* Webapp Router.
* This view manages the outgoing conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var WebappRouter = loop.shared.router.BaseConversationRouter.extend({
routes: {
"": "home",
"unsupportedDevice": "unsupportedDevice",
"unsupportedBrowser": "unsupportedBrowser",
"call/expired": "expired",
"call/pending/:token": "pendingConversation",
"call/ongoing/:token": "loadConversation",
"call/:token": "initiate"
var OutgoingConversationView = React.createClass({displayName: 'OutgoingConversationView',
propTypes: {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired
},
initialize: function(options) {
this.helper = options.helper;
if (!this.helper) {
throw new Error("WebappRouter requires a helper object");
getInitialState: function() {
return {
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("call:outgoing", this.startCall, this);
this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this._endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
/**
* Renders the conversation views.
*/
render: function() {
switch (this.state.callStatus) {
case "failure":
case "end":
case "start": {
return (
StartConversationView({
model: this.props.conversation,
notifications: this.props.notifications,
client: this.props.client}
)
);
}
case "pending": {
return PendingConversationView({websocket: this._websocket});
}
case "connected": {
return (
sharedViews.ConversationView({
sdk: this.props.sdk,
model: this.props.conversation,
video: {enabled: this.props.conversation.hasVideoStream("outgoing")}}
)
);
}
case "expired": {
return (
CallUrlExpiredView({helper: this.props.helper})
);
}
default: {
return HomeView(null)
}
}
// Load default view
this.loadReactComponent(HomeView(null));
},
_onSessionExpired: function() {
this.navigate("/call/expired", {trigger: true});
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
console.log(error);
this.props.notifications.errorL10n("connection_error_see_console_notification");
this.setState({callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.props.notifications.warnL10n("peer_ended_conversation2");
this.setState({callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
this.props.notifications.warnL10n("network_disconnected");
this.setState({callStatus: "end"});
},
/**
@ -446,33 +523,31 @@ loop.webapp = (function($, _, OT, mozL10n) {
* server.
*/
setupOutgoingCall: function() {
var loopToken = this._conversation.get("loopToken");
var loopToken = this.props.conversation.get("loopToken");
if (!loopToken) {
this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
} else {
var callType = this._conversation.get("selectedCallType");
var callType = this.props.conversation.get("selectedCallType");
this._conversation.once("call:outgoing", this.startCall, this);
this._client.requestCallInfo(this._conversation.get("loopToken"),
callType, function(err, sessionData) {
this.props.client.requestCallInfo(this.props.conversation.get("loopToken"),
callType, function(err, sessionData) {
if (err) {
switch (err.errno) {
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
// missing OR expired; we treat this information as if the url is always
// expired.
case 105:
this._onSessionExpired();
this.setState({callStatus: "expired"});
break;
default:
this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
break;
}
return;
}
this._conversation.outgoing(sessionData);
this.props.conversation.outgoing(sessionData);
}.bind(this));
}
},
@ -481,15 +556,15 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Actually starts the call.
*/
startCall: function() {
var loopToken = this._conversation.get("loopToken");
var loopToken = this.props.conversation.get("loopToken");
if (!loopToken) {
this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
this.navigate("call/pending/" + loopToken, {
trigger: true
});
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
return;
}
this._setupWebSocket();
this.setState({callStatus: "pending"});
},
/**
@ -498,17 +573,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
*
* @param {string} loopToken The session token to use.
*/
_setupWebSocketAndCallView: function() {
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"),
callId: this._conversation.get("callId"),
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId"),
});
this._websocket.promiseConnect().then(function() {
}.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifications.errorL10n("cannot_start_call_session_not_ready");
this.props.notifications.errorL10n("cannot_start_call_session_not_ready");
return;
}.bind(this));
@ -522,7 +597,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this._conversation.streamsConnected()) {
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
@ -534,7 +609,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
_handleWebSocketProgress: function(progressData) {
switch(progressData.state) {
case "connecting": {
this._handleCallConnecting();
// We just go straight to the connected view as the media gets set up.
this.setState({callStatus: "connected"});
break;
}
case "terminated": {
@ -546,116 +622,69 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
},
/**
* Handles a call moving to the connecting stage.
*/
_handleCallConnecting: function() {
var loopToken = this._conversation.get("loopToken");
if (!loopToken) {
this._notifications.errorL10n("missing_conversation_info");
return;
}
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
},
/**
* Handles call rejection.
*
* @param {String} reason The reason the call was terminated.
*/
_handleCallTerminated: function(reason) {
this.endCall();
this.setState({callStatus: "end"});
// For reasons other than cancel, display some notification text.
if (reason !== "cancel") {
// XXX This should really display the call failed view - bug 1046959
// will implement this.
this._notifications.errorL10n("call_timeout_notification_text");
this.props.notifications.errorL10n("call_timeout_notification_text");
}
},
/**
* @override {loop.shared.router.BaseConversationRouter.endCall}
* Handles ending a call by resetting the view to the start state.
*/
endCall: function() {
var route = "home";
if (this._conversation.get("loopToken")) {
route = "call/" + this._conversation.get("loopToken");
_endCall: function() {
this.setState({callStatus: "end"});
},
});
/**
* Webapp Root View. This is the main, single, view that controls the display
* of the webapp page.
*/
var WebappRootView = React.createClass({displayName: 'WebappRootView',
propTypes: {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
unsupportedDevice: this.props.helper.isIOS(navigator.platform),
unsupportedBrowser: !this.props.sdk.checkSystemRequirements(),
};
},
render: function() {
if (this.state.unsupportedDevice) {
return UnsupportedDeviceView(null);
} else if (this.state.unsupportedBrowser) {
return UnsupportedBrowserView(null);
} else if (this.props.conversation.get("loopToken")) {
return (
OutgoingConversationView({
client: this.props.client,
conversation: this.props.conversation,
helper: this.props.helper,
notifications: this.props.notifications,
sdk: this.props.sdk}
)
);
} else {
return HomeView(null);
}
this.navigate(route, {trigger: true});
},
/**
* Default entry point.
*/
home: function() {
this.loadReactComponent(HomeView(null));
},
unsupportedDevice: function() {
this.loadReactComponent(UnsupportedDeviceView(null));
},
unsupportedBrowser: function() {
this.loadReactComponent(UnsupportedBrowserView(null));
},
expired: function() {
this.loadReactComponent(CallUrlExpiredView({helper: this.helper}));
},
/**
* Loads conversation launcher view, setting the received conversation token
* to the current conversation model. If a session is currently established,
* terminates it first.
*
* @param {String} loopToken Loop conversation token.
*/
initiate: function(loopToken) {
// Check if a session is ongoing; if so, terminate it
if (this._conversation.get("ongoing")) {
this._conversation.endSession();
}
this._conversation.set("loopToken", loopToken);
var startView = StartConversationView({
model: this._conversation,
notifications: this._notifications,
client: this._client
});
this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
this._conversation.once("change:publishedStream", this._checkConnected, this);
this._conversation.once("change:subscribedStream", this._checkConnected, this);
this.loadReactComponent(startView);
},
pendingConversation: function(loopToken) {
if (!this._conversation.isSessionReady()) {
// User has loaded this url directly, actually setup the call.
return this.navigate("call/" + loopToken, {trigger: true});
}
this._setupWebSocketAndCallView();
this.loadReactComponent(PendingConversationView({
websocket: this._websocket
}));
},
/**
* Loads conversation establishment view.
*
*/
loadConversation: function(loopToken) {
if (!this._conversation.isSessionReady()) {
// User has loaded this url directly, actually setup the call.
return this.navigate("call/" + loopToken, {trigger: true});
}
this.loadReactComponent(sharedViews.ConversationView({
sdk: OT,
model: this._conversation,
video: {enabled: this._conversation.hasVideoStream("outgoing")}
}));
}
});
@ -673,6 +702,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
isIOS: function(platform) {
return this._iOSRegex.test(platform);
},
locationHash: function() {
return window.location.hash;
}
};
@ -682,40 +715,43 @@ loop.webapp = (function($, _, OT, mozL10n) {
function init() {
var helper = new WebappHelper();
var client = new loop.StandaloneClient({
baseServerUrl: baseServerUrl
baseServerUrl: loop.config.serverUrl
});
var router = new WebappRouter({
helper: helper,
notifications: new sharedModels.NotificationCollection(),
client: client,
conversation: new sharedModels.ConversationModel({}, {
sdk: OT
})
var notifications = new sharedModels.NotificationCollection();
var conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
Backbone.history.start();
if (helper.isIOS(navigator.platform)) {
router.navigate("unsupportedDevice", {trigger: true});
} else if (!OT.checkSystemRequirements()) {
router.navigate("unsupportedBrowser", {trigger: true});
// Obtain the loopToken and pass it to the conversation
var locationHash = helper.locationHash();
if (locationHash) {
conversation.set("loopToken", locationHash.match(/\#call\/(.*)/)[1]);
}
React.renderComponent(WebappRootView({
client: client,
conversation: conversation,
helper: helper,
notifications: notifications,
sdk: OT}
), document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
document.documentElement.lang = mozL10n.language.code;
document.documentElement.dir = mozL10n.language.direction;
}
return {
baseServerUrl: baseServerUrl,
CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView,
OutgoingConversationView: OutgoingConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView,
init: init,
PromoteFirefoxView: PromoteFirefoxView,
WebappHelper: WebappHelper,
WebappRouter: WebappRouter
WebappRootView: WebappRootView
};
})(jQuery, _, window.OT, navigator.mozL10n);

View File

@ -15,8 +15,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
baseServerUrl = loop.config.serverUrl;
sharedViews = loop.shared.views;
/**
* App router.
@ -332,8 +331,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
var privacy_notice_name = mozL10n.get("privacy_notice_link_text");
var tosHTML = mozL10n.get("legal_text_and_links", {
"terms_of_use_url": "<a target=_blank href='" +
"https://accounts.firefox.com/legal/terms'>" + tos_link_name + "</a>",
"terms_of_use_url": "<a target=_blank href='/legal/terms'>" +
tos_link_name + "</a>",
"privacy_notice_url": "<a target=_blank href='" +
"https://www.mozilla.org/privacy/'>" + privacy_notice_name + "</a>"
});
@ -414,31 +413,109 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
/**
* Webapp Router.
* This view manages the outgoing conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var WebappRouter = loop.shared.router.BaseConversationRouter.extend({
routes: {
"": "home",
"unsupportedDevice": "unsupportedDevice",
"unsupportedBrowser": "unsupportedBrowser",
"call/expired": "expired",
"call/pending/:token": "pendingConversation",
"call/ongoing/:token": "loadConversation",
"call/:token": "initiate"
var OutgoingConversationView = React.createClass({
propTypes: {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired
},
initialize: function(options) {
this.helper = options.helper;
if (!this.helper) {
throw new Error("WebappRouter requires a helper object");
getInitialState: function() {
return {
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("call:outgoing", this.startCall, this);
this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this._endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
/**
* Renders the conversation views.
*/
render: function() {
switch (this.state.callStatus) {
case "failure":
case "end":
case "start": {
return (
<StartConversationView
model={this.props.conversation}
notifications={this.props.notifications}
client={this.props.client}
/>
);
}
case "pending": {
return <PendingConversationView websocket={this._websocket} />;
}
case "connected": {
return (
<sharedViews.ConversationView
sdk={this.props.sdk}
model={this.props.conversation}
video={{enabled: this.props.conversation.hasVideoStream("outgoing")}}
/>
);
}
case "expired": {
return (
<CallUrlExpiredView helper={this.props.helper} />
);
}
default: {
return <HomeView />
}
}
// Load default view
this.loadReactComponent(<HomeView />);
},
_onSessionExpired: function() {
this.navigate("/call/expired", {trigger: true});
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
console.log(error);
this.props.notifications.errorL10n("connection_error_see_console_notification");
this.setState({callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.props.notifications.warnL10n("peer_ended_conversation2");
this.setState({callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
this.props.notifications.warnL10n("network_disconnected");
this.setState({callStatus: "end"});
},
/**
@ -446,33 +523,31 @@ loop.webapp = (function($, _, OT, mozL10n) {
* server.
*/
setupOutgoingCall: function() {
var loopToken = this._conversation.get("loopToken");
var loopToken = this.props.conversation.get("loopToken");
if (!loopToken) {
this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
} else {
var callType = this._conversation.get("selectedCallType");
var callType = this.props.conversation.get("selectedCallType");
this._conversation.once("call:outgoing", this.startCall, this);
this._client.requestCallInfo(this._conversation.get("loopToken"),
callType, function(err, sessionData) {
this.props.client.requestCallInfo(this.props.conversation.get("loopToken"),
callType, function(err, sessionData) {
if (err) {
switch (err.errno) {
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
// missing OR expired; we treat this information as if the url is always
// expired.
case 105:
this._onSessionExpired();
this.setState({callStatus: "expired"});
break;
default:
this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
break;
}
return;
}
this._conversation.outgoing(sessionData);
this.props.conversation.outgoing(sessionData);
}.bind(this));
}
},
@ -481,15 +556,15 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Actually starts the call.
*/
startCall: function() {
var loopToken = this._conversation.get("loopToken");
var loopToken = this.props.conversation.get("loopToken");
if (!loopToken) {
this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
this.navigate("call/pending/" + loopToken, {
trigger: true
});
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
return;
}
this._setupWebSocket();
this.setState({callStatus: "pending"});
},
/**
@ -498,17 +573,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
*
* @param {string} loopToken The session token to use.
*/
_setupWebSocketAndCallView: function() {
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"),
callId: this._conversation.get("callId"),
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId"),
});
this._websocket.promiseConnect().then(function() {
}.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifications.errorL10n("cannot_start_call_session_not_ready");
this.props.notifications.errorL10n("cannot_start_call_session_not_ready");
return;
}.bind(this));
@ -522,7 +597,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this._conversation.streamsConnected()) {
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
@ -534,7 +609,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
_handleWebSocketProgress: function(progressData) {
switch(progressData.state) {
case "connecting": {
this._handleCallConnecting();
// We just go straight to the connected view as the media gets set up.
this.setState({callStatus: "connected"});
break;
}
case "terminated": {
@ -546,116 +622,69 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
},
/**
* Handles a call moving to the connecting stage.
*/
_handleCallConnecting: function() {
var loopToken = this._conversation.get("loopToken");
if (!loopToken) {
this._notifications.errorL10n("missing_conversation_info");
return;
}
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
},
/**
* Handles call rejection.
*
* @param {String} reason The reason the call was terminated.
*/
_handleCallTerminated: function(reason) {
this.endCall();
this.setState({callStatus: "end"});
// For reasons other than cancel, display some notification text.
if (reason !== "cancel") {
// XXX This should really display the call failed view - bug 1046959
// will implement this.
this._notifications.errorL10n("call_timeout_notification_text");
this.props.notifications.errorL10n("call_timeout_notification_text");
}
},
/**
* @override {loop.shared.router.BaseConversationRouter.endCall}
* Handles ending a call by resetting the view to the start state.
*/
endCall: function() {
var route = "home";
if (this._conversation.get("loopToken")) {
route = "call/" + this._conversation.get("loopToken");
_endCall: function() {
this.setState({callStatus: "end"});
},
});
/**
* Webapp Root View. This is the main, single, view that controls the display
* of the webapp page.
*/
var WebappRootView = React.createClass({
propTypes: {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
unsupportedDevice: this.props.helper.isIOS(navigator.platform),
unsupportedBrowser: !this.props.sdk.checkSystemRequirements(),
};
},
render: function() {
if (this.state.unsupportedDevice) {
return <UnsupportedDeviceView />;
} else if (this.state.unsupportedBrowser) {
return <UnsupportedBrowserView />;
} else if (this.props.conversation.get("loopToken")) {
return (
<OutgoingConversationView
client={this.props.client}
conversation={this.props.conversation}
helper={this.props.helper}
notifications={this.props.notifications}
sdk={this.props.sdk}
/>
);
} else {
return <HomeView />;
}
this.navigate(route, {trigger: true});
},
/**
* Default entry point.
*/
home: function() {
this.loadReactComponent(<HomeView />);
},
unsupportedDevice: function() {
this.loadReactComponent(<UnsupportedDeviceView />);
},
unsupportedBrowser: function() {
this.loadReactComponent(<UnsupportedBrowserView />);
},
expired: function() {
this.loadReactComponent(CallUrlExpiredView({helper: this.helper}));
},
/**
* Loads conversation launcher view, setting the received conversation token
* to the current conversation model. If a session is currently established,
* terminates it first.
*
* @param {String} loopToken Loop conversation token.
*/
initiate: function(loopToken) {
// Check if a session is ongoing; if so, terminate it
if (this._conversation.get("ongoing")) {
this._conversation.endSession();
}
this._conversation.set("loopToken", loopToken);
var startView = StartConversationView({
model: this._conversation,
notifications: this._notifications,
client: this._client
});
this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
this._conversation.once("change:publishedStream", this._checkConnected, this);
this._conversation.once("change:subscribedStream", this._checkConnected, this);
this.loadReactComponent(startView);
},
pendingConversation: function(loopToken) {
if (!this._conversation.isSessionReady()) {
// User has loaded this url directly, actually setup the call.
return this.navigate("call/" + loopToken, {trigger: true});
}
this._setupWebSocketAndCallView();
this.loadReactComponent(PendingConversationView({
websocket: this._websocket
}));
},
/**
* Loads conversation establishment view.
*
*/
loadConversation: function(loopToken) {
if (!this._conversation.isSessionReady()) {
// User has loaded this url directly, actually setup the call.
return this.navigate("call/" + loopToken, {trigger: true});
}
this.loadReactComponent(sharedViews.ConversationView({
sdk: OT,
model: this._conversation,
video: {enabled: this._conversation.hasVideoStream("outgoing")}
}));
}
});
@ -673,6 +702,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
isIOS: function(platform) {
return this._iOSRegex.test(platform);
},
locationHash: function() {
return window.location.hash;
}
};
@ -682,40 +715,43 @@ loop.webapp = (function($, _, OT, mozL10n) {
function init() {
var helper = new WebappHelper();
var client = new loop.StandaloneClient({
baseServerUrl: baseServerUrl
baseServerUrl: loop.config.serverUrl
});
var router = new WebappRouter({
helper: helper,
notifications: new sharedModels.NotificationCollection(),
client: client,
conversation: new sharedModels.ConversationModel({}, {
sdk: OT
})
var notifications = new sharedModels.NotificationCollection();
var conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
Backbone.history.start();
if (helper.isIOS(navigator.platform)) {
router.navigate("unsupportedDevice", {trigger: true});
} else if (!OT.checkSystemRequirements()) {
router.navigate("unsupportedBrowser", {trigger: true});
// Obtain the loopToken and pass it to the conversation
var locationHash = helper.locationHash();
if (locationHash) {
conversation.set("loopToken", locationHash.match(/\#call\/(.*)/)[1]);
}
React.renderComponent(<WebappRootView
client={client}
conversation={conversation}
helper={helper}
notifications={notifications}
sdk={OT}
/>, document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
document.documentElement.lang = mozL10n.language.code;
document.documentElement.dir = mozL10n.language.direction;
}
return {
baseServerUrl: baseServerUrl,
CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView,
OutgoingConversationView: OutgoingConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView,
init: init,
PromoteFirefoxView: PromoteFirefoxView,
WebappHelper: WebappHelper,
WebappRouter: WebappRouter
WebappRootView: WebappRootView
};
})(jQuery, _, window.OT, navigator.mozL10n);

View File

@ -1,2 +0,0 @@
@import url(loop.en-US.properties)

View File

@ -575,7 +575,7 @@
comment: /^\s*#|^\s*$/,
entity: /^([^=\s]+)\s*=\s*(.+)$/,
multiline: /[^\\]\\$/,
macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i,
index: /\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,
unicode: /\\u([0-9a-fA-F]{1,4})/g,
entries: /[\r\n]+/,
controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g
@ -635,7 +635,7 @@
if (!(prop in obj)) {
obj[prop] = {'_': {}};
} else if (typeof(obj[prop]) === 'string') {
obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}};
obj[prop] = {'_index': parseIndex(obj[prop]), '_': {}};
}
obj[prop]._[key] = value;
}
@ -687,17 +687,22 @@
return unescapeUnicode(str);
}
function parseMacro(str) {
var match = str.match(parsePatterns.macro);
function parseIndex(str) {
var match = str.match(parsePatterns.index);
if (!match) {
throw new L10nError('Malformed macro');
throw new L10nError('Malformed index');
}
return [match[1], match[2]];
var parts = Array.prototype.slice.call(match, 1);
return parts.filter(function(part) {
return !!part;
});
}
}
var KNOWN_MACROS = ['plural'];
var MAX_PLACEABLE_LENGTH = 2500;
var MAX_PLACEABLES = 100;
var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g;
@ -739,7 +744,7 @@
// if resolve fails, we want the exception to bubble up and stop the whole
// resolving process; however, we still need to clean up the dirty flag
try {
val = resolve(ctxdata, this.env, this.value, this.index);
val = resolveValue(ctxdata, this.env, this.value, this.index);
} finally {
this.dirty = false;
}
@ -772,7 +777,11 @@
return entity;
};
function subPlaceable(ctxdata, env, match, id) {
function resolveIdentifier(ctxdata, env, id) {
if (KNOWN_MACROS.indexOf(id) > -1) {
return env['__' + id];
}
if (ctxdata && ctxdata.hasOwnProperty(id) &&
(typeof ctxdata[id] === 'string' ||
(typeof ctxdata[id] === 'number' && !isNaN(ctxdata[id])))) {
@ -785,17 +794,29 @@
if (!(env[id] instanceof Entity)) {
env[id] = new Entity(id, env[id], env);
}
var value = env[id].resolve(ctxdata);
if (typeof value === 'string') {
// prevent Billion Laughs attacks
if (value.length >= MAX_PLACEABLE_LENGTH) {
throw new L10nError('Too many characters in placeable (' +
value.length + ', max allowed is ' +
MAX_PLACEABLE_LENGTH + ')');
}
return value;
}
return env[id].resolve(ctxdata);
}
return undefined;
}
function subPlaceable(ctxdata, env, match, id) {
var value = resolveIdentifier(ctxdata, env, id);
if (typeof value === 'number') {
return value;
}
if (typeof value === 'string') {
// prevent Billion Laughs attacks
if (value.length >= MAX_PLACEABLE_LENGTH) {
throw new L10nError('Too many characters in placeable (' +
value.length + ', max allowed is ' +
MAX_PLACEABLE_LENGTH + ')');
}
return value;
}
return match;
}
@ -813,7 +834,43 @@
return value;
}
function resolve(ctxdata, env, expr, index) {
function resolveSelector(ctxdata, env, expr, index) {
var selector = resolveIdentifier(ctxdata, env, index[0]);
if (selector === undefined) {
throw new L10nError('Unknown selector: ' + index[0]);
}
if (typeof selector !== 'function') {
// selector is a simple reference to an entity or ctxdata
return selector;
}
var argLength = index.length - 1;
if (selector.length !== argLength) {
throw new L10nError('Macro ' + index[0] + ' expects ' +
selector.length + ' argument(s), yet ' + argLength +
' given');
}
var argValue = resolveIdentifier(ctxdata, env, index[1]);
if (selector === env.__plural) {
// special cases for zero, one, two if they are defined on the hash
if (argValue === 0 && 'zero' in expr) {
return 'zero';
}
if (argValue === 1 && 'one' in expr) {
return 'one';
}
if (argValue === 2 && 'two' in expr) {
return 'two';
}
}
return selector(argValue);
}
function resolveValue(ctxdata, env, expr, index) {
if (typeof expr === 'string') {
return interpolate(ctxdata, env, expr);
}
@ -825,30 +882,17 @@
}
// otherwise, it's a dict
if (index && ctxdata && ctxdata.hasOwnProperty(index[1])) {
var argValue = ctxdata[index[1]];
// special cases for zero, one, two if they are defined on the hash
if (argValue === 0 && 'zero' in expr) {
return resolve(ctxdata, env, expr.zero);
}
if (argValue === 1 && 'one' in expr) {
return resolve(ctxdata, env, expr.one);
}
if (argValue === 2 && 'two' in expr) {
return resolve(ctxdata, env, expr.two);
}
var selector = env.__plural(argValue);
if (index) {
// try to use the index in order to select the right dict member
var selector = resolveSelector(ctxdata, env, expr, index);
if (expr.hasOwnProperty(selector)) {
return resolve(ctxdata, env, expr[selector]);
return resolveValue(ctxdata, env, expr[selector]);
}
}
// if there was no index or no selector was found, try 'other'
if ('other' in expr) {
return resolve(ctxdata, env, expr.other);
return resolveValue(ctxdata, env, expr.other);
}
return undefined;
@ -1071,7 +1115,7 @@
var idToFetch = this.isPseudo ? ctx.defaultLocale : this.id;
for (var i = 0; i < ctx.resLinks.length; i++) {
var path = ctx.resLinks[i].replace('{{locale}}', idToFetch);
var path = ctx.resLinks[i].replace('{locale}', idToFetch);
var type = path.substr(path.lastIndexOf('.') + 1);
switch (type) {
@ -1120,7 +1164,9 @@
this.isLoading = false;
this.defaultLocale = 'en-US';
this.availableLocales = [];
this.supportedLocales = [];
this.resLinks = [];
this.locales = {};
@ -1218,6 +1264,19 @@
this._emitter.emit('ready');
}
this.registerLocales = function (defLocale, available) {
/* jshint boss:true */
this.availableLocales = [this.defaultLocale = defLocale];
if (available) {
for (var i = 0, loc; loc = available[i]; i++) {
if (this.availableLocales.indexOf(loc) === -1) {
this.availableLocales.push(loc);
}
}
}
};
this.requestLocales = function requestLocales() {
if (this.isLoading && !this.isReady) {
throw new L10nError('Context not ready');
@ -1225,8 +1284,15 @@
this.isLoading = true;
var requested = Array.prototype.slice.call(arguments);
if (requested.length === 0) {
throw new L10nError('No locales requested');
}
var supported = negotiate(requested.concat(this.defaultLocale),
var reqPseudo = requested.filter(function(loc) {
return loc in PSEUDO_STRATEGIES;
});
var supported = negotiate(this.availableLocales.concat(reqPseudo),
requested,
this.defaultLocale);
freeze.call(this, supported);
@ -1280,11 +1346,12 @@
var DEBUG = true;
var DEBUG = false;
var isPretranslated = false;
var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur'];
var nodeObserver = null;
var pendingElements = null;
var manifest = {};
var moConfig = {
attributes: true,
@ -1343,7 +1410,8 @@
rePlaceables: rePlaceables,
getTranslatableChildren: getTranslatableChildren,
translateDocument: translateDocument,
loadINI: loadINI,
onManifestInjected: onManifestInjected,
onMetaInjected: onMetaInjected,
fireLocalizedEvent: fireLocalizedEvent,
PropertiesParser: PropertiesParser,
compile: compile,
@ -1355,8 +1423,8 @@
navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n));
if (DEBUG) {
navigator.mozL10n.ctx.addEventListener('error', console);
navigator.mozL10n.ctx.addEventListener('warning', console);
navigator.mozL10n.ctx.addEventListener('error', console.error);
navigator.mozL10n.ctx.addEventListener('warning', console.warn);
}
function getDirection(lang) {
@ -1444,57 +1512,130 @@
}
function initResources() {
var nodes =
document.head.querySelectorAll('link[type="application/l10n"],' +
'script[type="application/l10n"]');
var iniLinks = [];
/* jshint boss:true */
var manifestFound = false;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var nodeName = node.nodeName.toLowerCase();
switch (nodeName) {
case 'link':
var url = node.getAttribute('href');
var type = url.substr(url.lastIndexOf('.') + 1);
if (type === 'ini') {
iniLinks.push(url);
}
this.ctx.resLinks.push(url);
var nodes = document.head
.querySelectorAll('link[rel="localization"],' +
'link[rel="manifest"],' +
'meta[name="locales"],' +
'meta[name="default_locale"],' +
'script[type="application/l10n"]');
for (var i = 0, node; node = nodes[i]; i++) {
var type = node.getAttribute('rel') || node.nodeName.toLowerCase();
switch (type) {
case 'manifest':
manifestFound = true;
onManifestInjected.call(this, node.getAttribute('href'), initLocale);
break;
case 'localization':
this.ctx.resLinks.push(node.getAttribute('href'));
break;
case 'meta':
onMetaInjected.call(this, node);
break;
case 'script':
var lang = node.getAttribute('lang');
var locale = this.ctx.getLocale(lang);
locale.addAST(JSON.parse(node.textContent));
onScriptInjected.call(this, node);
break;
}
}
var iniLoads = iniLinks.length;
if (iniLoads === 0) {
initLocale.call(this);
return;
// if after scanning the head any locales have been registered in the ctx
// it's safe to initLocale without waiting for manifest.webapp
if (this.ctx.availableLocales.length) {
return initLocale.call(this);
}
function onIniLoaded(err) {
if (err) {
this.ctx._emitter.emit('error', err);
}
if (--iniLoads === 0) {
initLocale.call(this);
}
}
for (i = 0; i < iniLinks.length; i++) {
loadINI.call(this, iniLinks[i], onIniLoaded.bind(this));
// if no locales were registered so far and no manifest.webapp link was
// found we still call initLocale with just the default language available
if (!manifestFound) {
this.ctx.registerLocales(this.ctx.defaultLocale);
return initLocale.call(this);
}
}
function onMetaInjected(node) {
if (this.ctx.availableLocales.length) {
return;
}
switch (node.getAttribute('name')) {
case 'locales':
manifest.locales = node.getAttribute('content').split(',').map(
Function.prototype.call, String.prototype.trim);
break;
case 'default_locale':
manifest.defaultLocale = node.getAttribute('content');
break;
}
if (Object.keys(manifest).length === 2) {
this.ctx.registerLocales(manifest.defaultLocale, manifest.locales);
manifest = {};
}
}
function onScriptInjected(node) {
var lang = node.getAttribute('lang');
var locale = this.ctx.getLocale(lang);
locale.addAST(JSON.parse(node.textContent));
}
function onManifestInjected(url, callback) {
if (this.ctx.availableLocales.length) {
return;
}
io.loadJSON(url, function parseManifest(err, json) {
if (this.ctx.availableLocales.length) {
return;
}
if (err) {
this.ctx._emitter.emit('error', err);
this.ctx.registerLocales(this.ctx.defaultLocale);
if (callback) {
callback.call(this);
}
return;
}
// default_locale and locales might have been already provided by meta
// elements which take precedence; check if we already have them
if (!('defaultLocale' in manifest)) {
if (json.default_locale) {
manifest.defaultLocale = json.default_locale;
} else {
manifest.defaultLocale = this.ctx.defaultLocale;
this.ctx._emitter.emit(
'warning', new L10nError('default_locale missing from manifest'));
}
}
if (!('locales' in manifest)) {
if (json.locales) {
manifest.locales = Object.keys(json.locales);
} else {
this.ctx._emitter.emit(
'warning', new L10nError('locales missing from manifest'));
}
}
this.ctx.registerLocales(manifest.defaultLocale, manifest.locales);
manifest = {};
if (callback) {
callback.call(this);
}
}.bind(this));
}
function initLocale() {
this.ctx.requestLocales(navigator.language);
this.ctx.requestLocales.apply(
this.ctx, navigator.languages || [navigator.language]);
window.addEventListener('languagechange', function l10n_langchange() {
navigator.mozL10n.language.code = navigator.language;
});
this.ctx.requestLocales.apply(
this.ctx, navigator.languages || [navigator.language]);
}.bind(this));
}
function localizeMutations(mutations) {
@ -1565,83 +1706,6 @@
/* jshint -W104 */
function loadINI(url, callback) {
var ctx = this.ctx;
io.load(url, function(err, source) {
var pos = ctx.resLinks.indexOf(url);
if (err) {
// remove the ini link from resLinks
ctx.resLinks.splice(pos, 1);
return callback(err);
}
if (!source) {
ctx.resLinks.splice(pos, 1);
return callback(new Error('Empty file: ' + url));
}
var patterns = parseINI(source, url).resources.map(function(x) {
return x.replace('en-US', '{{locale}}');
});
ctx.resLinks.splice.apply(ctx.resLinks, [pos, 1].concat(patterns));
callback();
});
}
function relativePath(baseUrl, url) {
if (url[0] === '/') {
return url;
}
var dirs = baseUrl.split('/')
.slice(0, -1)
.concat(url.split('/'))
.filter(function(path) {
return path !== '.';
});
return dirs.join('/');
}
var iniPatterns = {
'section': /^\s*\[(.*)\]\s*$/,
'import': /^\s*@import\s+url\((.*)\)\s*$/i,
'entry': /[\r\n]+/
};
function parseINI(source, iniPath) {
var entries = source.split(iniPatterns.entry);
var locales = ['en-US'];
var genericSection = true;
var uris = [];
var match;
for (var i = 0; i < entries.length; i++) {
var line = entries[i];
// we only care about en-US resources
if (genericSection && iniPatterns['import'].test(line)) {
match = iniPatterns['import'].exec(line);
var uri = relativePath(iniPath, match[1]);
uris.push(uri);
continue;
}
// but we need the list of all locales in the ini, too
if (iniPatterns.section.test(line)) {
genericSection = false;
match = iniPatterns.section.exec(line);
locales.push(match[1]);
}
}
return {
locales: locales,
resources: uris
};
}
/* jshint -W104 */
function translateDocument() {
document.documentElement.lang = this.language.code;
document.documentElement.dir = this.language.direction;

View File

@ -55,55 +55,42 @@ describe("loop.panel", function() {
sandbox.restore();
});
describe("loop.panel.PanelRouter", function() {
describe("#constructor", function() {
it("should require a notifications collection", function() {
expect(function() {
new loop.panel.PanelRouter();
}).to.Throw(Error, /missing required notifications/);
});
it("should require a document", function() {
expect(function() {
new loop.panel.PanelRouter({notifications: notifications});
}).to.Throw(Error, /missing required document/);
});
describe("#init", function() {
beforeEach(function() {
sandbox.stub(React, "renderComponent");
sandbox.stub(document.mozL10n, "initialize");
sandbox.stub(document.mozL10n, "get").returns("Fake title");
});
describe("constructed", function() {
var router;
it("should initalize L10n", function() {
loop.panel.init();
beforeEach(function() {
router = createTestRouter({
hidden: true,
addEventListener: sandbox.spy()
});
sinon.assert.calledOnce(document.mozL10n.initialize);
sinon.assert.calledWithExactly(document.mozL10n.initialize,
navigator.mozLoop);
});
sandbox.stub(router, "loadReactComponent");
});
it("should render the panel view", function() {
loop.panel.init();
describe("#home", function() {
beforeEach(function() {
sandbox.stub(notifications, "reset");
});
sinon.assert.calledOnce(React.renderComponent);
sinon.assert.calledWith(React.renderComponent,
sinon.match(function(value) {
return TestUtils.isDescriptorOfType(value,
loop.panel.PanelView);
}));
});
it("should clear all pending notifications", function() {
router.home();
it("should dispatch an loopPanelInitialized", function(done) {
function listener() {
done();
}
sinon.assert.calledOnce(notifications.reset);
});
window.addEventListener("loopPanelInitialized", listener);
it("should load the home view", function() {
router.home();
loop.panel.init();
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWithExactly(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.panel.PanelView);
}));
});
});
window.removeEventListener("loopPanelInitialized", listener);
});
});

View File

@ -20,7 +20,7 @@
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
<script src="../../standalone/content/libs/l10n-gaia-4e35bf8f0569.js"></script>
<script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
<!-- test dependencies -->
<script src="vendor/mocha-1.17.1.js"></script>

View File

@ -20,7 +20,7 @@
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
<script src="../../standalone/content/libs/l10n-gaia-4e35bf8f0569.js"></script>
<script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
<!-- test dependencies -->
<script src="../shared/vendor/mocha-1.17.1.js"></script>
<script src="../shared/vendor/chai-1.9.0.js"></script>
@ -35,7 +35,6 @@
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>

View File

@ -17,118 +17,80 @@ describe("loop.webapp", function() {
beforeEach(function() {
sandbox = sinon.sandbox.create();
// conversation#outgoing sets timers, so we need to use fake ones
// to prevent random failures.
sandbox.useFakeTimers();
notifications = new sharedModels.NotificationCollection();
loop.config.pendingCallTimeout = 1000;
});
afterEach(function() {
sandbox.restore();
delete loop.config.pendingCallTimeout;
});
describe("#init", function() {
var WebappRouter;
var conversationSetStub;
beforeEach(function() {
WebappRouter = loop.webapp.WebappRouter;
sandbox.stub(WebappRouter.prototype, "navigate");
sandbox.stub(WebappRouter.prototype, "loadReactComponent");
sandbox.stub(React, "renderComponent");
sandbox.stub(loop.webapp.WebappHelper.prototype,
"locationHash").returns("#call/fake-Token");
conversationSetStub =
sandbox.stub(sharedModels.ConversationModel.prototype, "set");
});
afterEach(function() {
Backbone.history.stop();
});
it("should navigate to the unsupportedDevice route if the sdk detects " +
"the device is running iOS", function() {
sandbox.stub(loop.webapp.WebappHelper.prototype, "isIOS").returns(true);
it("should create the WebappRootView", function() {
loop.webapp.init();
sinon.assert.calledOnce(WebappRouter.prototype.navigate);
sinon.assert.calledWithExactly(WebappRouter.prototype.navigate,
"unsupportedDevice", {trigger: true});
sinon.assert.calledOnce(React.renderComponent);
sinon.assert.calledWith(React.renderComponent,
sinon.match(function(value) {
return TestUtils.isDescriptorOfType(value,
loop.webapp.WebappRootView);
}));
});
it("should navigate to the unsupportedBrowser route if the sdk detects " +
"the browser is unsupported", function() {
sandbox.stub(loop.webapp.WebappHelper.prototype, "isIOS").returns(false);
sandbox.stub(window.OT, "checkSystemRequirements").returns(false);
it("should set the loopToken on the conversation", function() {
loop.webapp.init();
sinon.assert.calledOnce(WebappRouter.prototype.navigate);
sinon.assert.calledWithExactly(WebappRouter.prototype.navigate,
"unsupportedBrowser", {trigger: true});
sinon.assert.called(conversationSetStub);
sinon.assert.calledWithExactly(conversationSetStub, "loopToken", "fake-Token");
});
});
describe("WebappRouter", function() {
var router, conversation, client;
describe("OutgoingConversationView", function() {
var ocView, conversation, client;
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
loop.webapp.OutgoingConversationView(props));
}
beforeEach(function() {
client = new loop.StandaloneClient({
baseServerUrl: "http://fake.example.com"
});
sandbox.stub(client, "requestCallInfo");
sandbox.stub(client, "requestCallUrlInfo");
conversation = new sharedModels.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000
sdk: {}
});
sandbox.stub(loop.webapp.WebappRouter.prototype, "loadReactComponent");
router = new loop.webapp.WebappRouter({
helper: {},
conversation.set("loopToken", "fakeToken");
ocView = mountTestComponent({
helper: new loop.webapp.WebappHelper(),
client: client,
conversation: conversation,
notifications: notifications
});
sandbox.stub(router, "navigate");
});
describe("#initialize", function() {
it("should require a conversation option", function() {
expect(function() {
new loop.webapp.WebappRouter();
}).to.Throw(Error, /missing required conversation/);
notifications: notifications,
sdk: {}
});
});
describe("#startCall", function() {
beforeEach(function() {
sandbox.stub(router, "_setupWebSocketAndCallView");
describe("start", function() {
it("should display the StartConversationView", function() {
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
});
it("should navigate back home if session token is missing", function() {
router.startCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "home");
});
it("should notify the user if session token is missing", function() {
sandbox.stub(notifications, "errorL10n");
router.startCall();
sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifications.errorL10n,
"missing_conversation_info");
});
it("should navigate to the pending view if session token is available",
function() {
conversation.set("loopToken", "fake");
router.startCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/pending/fake");
});
});
describe("#_setupWebSocketAndCallView", function() {
// This is tested separately to ease testing, although it isn't really a
// public API. This will probably be refactored soon anyway.
describe("#_setupWebSocket", function() {
beforeEach(function() {
conversation.setOutgoingSessionData({
sessionId: "sessionId",
@ -157,7 +119,7 @@ describe("loop.webapp", function() {
});
it("should create a CallConnectionWebSocket", function(done) {
router._setupWebSocketAndCallView("fake");
ocView._setupWebSocket();
promise.then(function () {
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
@ -190,12 +152,12 @@ describe("loop.webapp", function() {
it("should display an error", function(done) {
sandbox.stub(notifications, "errorL10n");
router._setupWebSocketAndCallView();
ocView._setupWebSocket();
promise.then(function() {
}, function () {
sinon.assert.calledOnce(router._notifications.errorL10n);
sinon.assert.calledWithExactly(router._notifications.errorL10n,
sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifications.errorL10n,
"cannot_start_call_session_not_ready");
done();
});
@ -218,28 +180,28 @@ describe("loop.webapp", function() {
then: sandbox.spy()
});
router._setupWebSocketAndCallView();
ocView._setupWebSocket();
});
describe("Progress", function() {
describe("state: terminate, reason: reject", function() {
beforeEach(function() {
sandbox.stub(router, "endCall");
sandbox.stub(notifications, "errorL10n");
});
it("should end the call", function() {
router._websocket.trigger("progress", {
it("should display the StartConversationView", function() {
ocView._websocket.trigger("progress", {
state: "terminated",
reason: "reject"
});
sinon.assert.calledOnce(router.endCall);
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
});
it("should display an error message if the reason is not 'cancel'",
function() {
router._websocket.trigger("progress", {
ocView._websocket.trigger("progress", {
state: "terminated",
reason: "reject"
});
@ -251,7 +213,7 @@ describe("loop.webapp", function() {
it("should not display an error message if the reason is 'cancel'",
function() {
router._websocket.trigger("progress", {
ocView._websocket.trigger("progress", {
state: "terminated",
reason: "cancel"
});
@ -261,233 +223,133 @@ describe("loop.webapp", function() {
});
describe("state: connecting", function() {
it("should navigate to the ongoing view", function() {
it("should set display the ConversationView", function() {
// Prevent the conversation trying to start the session for
// this test.
sandbox.stub(conversation, "startSession");
conversation.set({"loopToken": "fakeToken"});
router._websocket.trigger("progress", {
ocView._websocket.trigger("progress", {
state: "connecting"
});
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
TestUtils.findRenderedComponentWithType(ocView,
sharedViews.ConversationView);
});
});
});
});
});
describe("#endCall", function() {
it("should navigate to home if session token is unset", function() {
router.endCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "home");
});
it("should navigate to call/:token if session token is set", function() {
conversation.set("loopToken", "fake");
router.endCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fake");
});
});
describe("Routes", function() {
beforeEach(function() {
// In the router's constructor, it loads the home view, we don't
// need to test it here, so reset the stub.
router.loadReactComponent.reset();
});
describe("#home", function() {
it("should load the HomeView", function() {
router.home();
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWith(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.webapp.HomeView);
}));
});
});
describe("#expired", function() {
it("should load the CallUrlExpiredView view", function() {
router.expired();
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWith(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.webapp.CallUrlExpiredView);
}));
});
});
describe("#initiate", function() {
it("should set the token on the conversation model", function() {
router.initiate("fakeToken");
expect(conversation.get("loopToken")).eql("fakeToken");
});
it("should load the StartConversationView", function() {
router.initiate("fakeToken");
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWithExactly(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.webapp.StartConversationView);
}));
});
// https://bugzilla.mozilla.org/show_bug.cgi?id=991118
it("should terminate any ongoing call session", function() {
sinon.stub(conversation, "endSession");
conversation.set("ongoing", true);
router.initiate("fakeToken");
sinon.assert.calledOnce(conversation.endSession);
});
});
describe("#pendingConversation", function() {
beforeEach(function() {
sandbox.stub(router, "_setupWebSocketAndCallView");
conversation.setOutgoingSessionData({
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
});
});
it("should setup the websocket", function() {
router.pendingConversation();
sinon.assert.calledOnce(router._setupWebSocketAndCallView);
sinon.assert.calledWithExactly(router._setupWebSocketAndCallView);
});
it("should load the PendingConversationView", function() {
router.pendingConversation();
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWith(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.webapp.PendingConversationView);
}));
});
});
describe("#loadConversation", function() {
it("should load the ConversationView if session is set", function() {
conversation.set("sessionId", "fakeSessionId");
router.loadConversation();
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWith(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.shared.views.ConversationView);
}));
});
it("should navigate to #call/{token} if session isn't ready",
function() {
router.loadConversation("fakeToken");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
});
});
describe("#unsupportedDevice", function() {
it("should load the UnsupportedDeviceView", function() {
router.unsupportedDevice();
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWith(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.webapp.UnsupportedDeviceView);
}));
});
});
describe("#unsupportedBrowser", function() {
it("should load the UnsupportedBrowserView", function() {
router.unsupportedBrowser();
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWith(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.webapp.UnsupportedBrowserView);
}));
});
});
});
describe("Events", function() {
var fakeSessionData;
var fakeSessionData, promiseConnectStub;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
websocketToken: 123
websocketToken: 123,
progressURL: "fakeUrl",
callId: "fakeCallId"
};
conversation.set(fakeSessionData);
conversation.set("loopToken", "fakeToken");
sandbox.stub(router, "startCall");
sandbox.stub(notifications, "errorL10n");
sandbox.stub(notifications, "warnL10n");
promiseConnectStub =
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect");
promiseConnectStub.returns(new Promise(function(resolve, reject) {}));
});
it("should attempt to start the call once call session is ready",
function() {
router.setupOutgoingCall();
conversation.outgoing(fakeSessionData);
describe("call:outgoing", function() {
it("should set display the StartConversationView if session token is missing",
function() {
conversation.set("loopToken", "");
sinon.assert.calledOnce(router.startCall);
ocView.startCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
});
it("should notify the user if session token is missing", function() {
conversation.set("loopToken", "");
ocView.startCall();
sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifications.errorL10n,
"missing_conversation_info");
});
it("should navigate to call/{token} when conversation ended", function() {
conversation.trigger("session:ended");
it("should setup the websocket if session token is available",
function() {
ocView.startCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
sinon.assert.calledOnce(promiseConnectStub);
});
it("should show the PendingConversationView if session token is available",
function() {
ocView.startCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.PendingConversationView);
});
});
it("should navigate to call/{token} when peer hangs up", function() {
conversation.trigger("session:peer-hungup");
describe("session:ended", function() {
it("should set display the StartConversationView", function() {
conversation.trigger("session:ended");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
});
});
it("should navigate to call/{token} when network disconnects",
function() {
describe("session:peer-hungup", function() {
it("should set display the StartConversationView", function() {
conversation.trigger("session:peer-hungup");
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
});
it("should notify the user", function() {
conversation.trigger("session:peer-hungup");
sinon.assert.calledOnce(notifications.warnL10n);
sinon.assert.calledWithExactly(notifications.warnL10n,
"peer_ended_conversation2");
});
});
describe("session:network-disconnected", function() {
it("should display the StartConversationView",
function() {
conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
});
it("should notify the user", function() {
conversation.trigger("session:network-disconnected");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
sinon.assert.calledOnce(notifications.warnL10n);
sinon.assert.calledWithExactly(notifications.warnL10n,
"network_disconnected");
});
});
describe("Published and Subscribed Streams", function() {
beforeEach(function() {
router._websocket = {
ocView._websocket = {
mediaUp: sinon.spy()
};
router.initiate();
});
describe("publishStream", function() {
@ -495,7 +357,7 @@ describe("loop.webapp", function() {
function() {
conversation.set("publishedStream", true);
sinon.assert.notCalled(router._websocket.mediaUp);
sinon.assert.notCalled(ocView._websocket.mediaUp);
});
it("should notify the websocket that media is up if both streams" +
@ -503,7 +365,7 @@ describe("loop.webapp", function() {
conversation.set("subscribedStream", true);
conversation.set("publishedStream", true);
sinon.assert.calledOnce(router._websocket.mediaUp);
sinon.assert.calledOnce(ocView._websocket.mediaUp);
});
});
@ -512,7 +374,7 @@ describe("loop.webapp", function() {
function() {
conversation.set("subscribedStream", true);
sinon.assert.notCalled(router._websocket.mediaUp);
sinon.assert.notCalled(ocView._websocket.mediaUp);
});
it("should notify the websocket that media is up if both streams" +
@ -520,26 +382,25 @@ describe("loop.webapp", function() {
conversation.set("publishedStream", true);
conversation.set("subscribedStream", true);
sinon.assert.calledOnce(router._websocket.mediaUp);
sinon.assert.calledOnce(ocView._websocket.mediaUp);
});
});
});
describe("#setupOutgoingCall", function() {
beforeEach(function() {
router.initiate();
});
describe("No loop token", function() {
it("should navigate to home", function() {
beforeEach(function() {
conversation.set("loopToken", "");
});
it("should set display the StartConversationView", function() {
conversation.setupOutgoingCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "home");
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
});
it("should display an error", function() {
sandbox.stub(notifications, "errorL10n");
conversation.setupOutgoingCall();
sinon.assert.calledOnce(notifications.errorL10n);
@ -548,7 +409,6 @@ describe("loop.webapp", function() {
describe("Has loop token", function() {
beforeEach(function() {
conversation.set("loopToken", "fakeToken");
conversation.set("selectedCallType", "audio-video");
sandbox.stub(conversation, "outgoing");
});
@ -563,26 +423,29 @@ describe("loop.webapp", function() {
});
describe("requestCallInfo response handling", function() {
it("should navigate to call/expired when a session has expired",
it("should set display the CallUrlExpiredView if the call has expired",
function() {
client.requestCallInfo.callsArgWith(2, {errno: 105});
conversation.setupOutgoingCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "/call/expired");
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.CallUrlExpiredView);
});
it("should navigate to home on any other error", function() {
client.requestCallInfo.callsArgWith(2, {errno: 104});
conversation.setupOutgoingCall();
it("should set display the StartConversationView on any other error",
function() {
client.requestCallInfo.callsArgWith(2, {errno: 104});
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "home");
conversation.setupOutgoingCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
});
it("should notify the user on any other error", function() {
sandbox.stub(notifications, "errorL10n");
client.requestCallInfo.callsArgWith(2, {errno: 104});
conversation.setupOutgoingCall();
sinon.assert.calledOnce(notifications.errorL10n);
@ -591,6 +454,7 @@ describe("loop.webapp", function() {
it("should call outgoing on the conversation model when details " +
"are successfully received", function() {
client.requestCallInfo.callsArgWith(2, null, fakeSessionData);
conversation.setupOutgoingCall();
sinon.assert.calledOnce(conversation.outgoing);
@ -602,6 +466,75 @@ describe("loop.webapp", function() {
});
});
describe("WebappRootView", function() {
var webappHelper, sdk, conversationModel, client, props;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.webapp.WebappRootView({
client: client,
helper: webappHelper,
sdk: sdk,
conversation: conversationModel
}));
}
beforeEach(function() {
webappHelper = new loop.webapp.WebappHelper();
sdk = {
checkSystemRequirements: function() { return true; }
};
conversationModel = new sharedModels.ConversationModel({}, {
sdk: sdk
});
client = new loop.StandaloneClient({
baseServerUrl: "fakeUrl"
});
// Stub this to stop the StartConversationView kicking in the request and
// follow-ups.
sandbox.stub(client, "requestCallUrlInfo");
});
it("should mount the unsupportedDevice view if the device is running iOS",
function() {
sandbox.stub(webappHelper, "isIOS").returns(true);
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.webapp.UnsupportedDeviceView);
});
it("should mount the unsupportedBrowser view if the sdk detects " +
"the browser is unsupported", function() {
sdk.checkSystemRequirements = function() {
return false;
};
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.webapp.UnsupportedBrowserView);
});
it("should mount the OutgoingConversationView view if there is a loopToken",
function() {
conversationModel.set("loopToken", "fakeToken");
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.webapp.OutgoingConversationView);
});
it("should mount the Home view there is no loopToken", function() {
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.webapp.HomeView);
});
});
describe("PendingConversationView", function() {
var view, websocket;
@ -648,8 +581,7 @@ describe("loop.webapp", function() {
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000
sdk: {}
});
fakeSubmitEvent = {preventDefault: sinon.spy()};
@ -747,8 +679,7 @@ describe("loop.webapp", function() {
conversation = new sharedModels.ConversationModel({
loopToken: "fake"
}, {
sdk: {},
pendingCallTimeout: 1000
sdk: {}
});
sandbox.spy(conversation, "listenTo");
@ -797,8 +728,7 @@ describe("loop.webapp", function() {
conversation = new sharedModels.ConversationModel({
loopToken: "fake"
}, {
sdk: {},
pendingCallTimeout: 1000
sdk: {}
});
requestCallUrlInfo = sandbox.stub();
@ -895,5 +825,4 @@ describe("loop.webapp", function() {
});
});
});
});

View File

@ -772,7 +772,7 @@ ResponsiveUI.prototype = {
if (!filename) {
let date = new Date();
let month = ("0" + (date.getMonth() + 1)).substr(-2, 2);
let day = ("0" + (date.getDay() + 1)).substr(-2, 2);
let day = ("0" + date.getDate()).substr(-2, 2);
let dateString = [date.getFullYear(), month, day].join("-");
let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
filename = this.strings.formatStringFromName("responsiveUI.screenshotGeneratedFilename", [dateString, timeString], 2);

View File

@ -0,0 +1,992 @@
# -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"use strict";
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
this.EXPORTED_SYMBOLS = [ "PluginContent" ];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
const url = "chrome://browser/locale/browser.properties";
return Services.strings.createBundle(url);
});
this.PluginContent = function (global) {
this.init(global);
}
PluginContent.prototype = {
init: function (global) {
this.global = global;
// Need to hold onto the content window or else it'll get destroyed
this.content = this.global.content;
// Cache of plugin actions for the current page.
this.pluginData = new Map();
// Note that the XBL binding is untrusted
global.addEventListener("PluginBindingAttached", this, true, true);
global.addEventListener("PluginCrashed", this, true);
global.addEventListener("PluginOutdated", this, true);
global.addEventListener("PluginInstantiated", this, true);
global.addEventListener("PluginRemoved", this, true);
global.addEventListener("unload", this);
global.addEventListener("pageshow", (event) => this.onPageShow(event), true);
global.addMessageListener("BrowserPlugins:ActivatePlugins", this);
global.addMessageListener("BrowserPlugins:NotificationRemoved", this);
global.addMessageListener("BrowserPlugins:NotificationShown", this);
global.addMessageListener("BrowserPlugins:ContextMenuCommand", this);
},
uninit: function() {
delete this.global;
delete this.content;
},
receiveMessage: function (msg) {
switch (msg.name) {
case "BrowserPlugins:ActivatePlugins":
this.activatePlugins(msg.data.pluginInfo, msg.data.newState);
break;
case "BrowserPlugins:NotificationRemoved":
this.clearPluginDataCache();
break;
case "BrowserPlugins:NotificationShown":
setTimeout(() => this.updateNotificationUI(), 0);
break;
case "BrowserPlugins:ContextMenuCommand":
switch (msg.data.command) {
case "play":
this._showClickToPlayNotification(msg.objects.plugin, true);
break;
case "hide":
this.hideClickToPlayOverlay(msg.objects.plugin);
break;
}
break;
}
},
onPageShow: function (event) {
// Ignore events that aren't from the main document.
if (this.global.content && event.target != this.global.content.document) {
return;
}
// The PluginClickToPlay events are not fired when navigating using the
// BF cache. |persisted| is true when the page is loaded from the
// BF cache, so this code reshows the notification if necessary.
if (event.persisted) {
this.reshowClickToPlayNotification();
}
},
getPluginUI: function (plugin, anonid) {
return plugin.ownerDocument.
getAnonymousElementByAttribute(plugin, "anonid", anonid);
},
_getPluginInfo: function (pluginElement) {
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
pluginElement.QueryInterface(Ci.nsIObjectLoadingContent);
let tagMimetype;
let pluginName = gNavigatorBundle.GetStringFromName("pluginInfo.unknownPlugin");
let pluginTag = null;
let permissionString = null;
let fallbackType = null;
let blocklistState = null;
tagMimetype = pluginElement.actualType;
if (tagMimetype == "") {
tagMimetype = pluginElement.type;
}
if (this.isKnownPlugin(pluginElement)) {
pluginTag = pluginHost.getPluginTagForType(pluginElement.actualType);
pluginName = this.makeNicePluginName(pluginTag.name);
permissionString = pluginHost.getPermissionStringForType(pluginElement.actualType);
fallbackType = pluginElement.defaultFallbackType;
blocklistState = pluginHost.getBlocklistStateForType(pluginElement.actualType);
// Make state-softblocked == state-notblocked for our purposes,
// they have the same UI. STATE_OUTDATED should not exist for plugin
// items, but let's alias it anyway, just in case.
if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED ||
blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) {
blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
}
}
return { mimetype: tagMimetype,
pluginName: pluginName,
pluginTag: pluginTag,
permissionString: permissionString,
fallbackType: fallbackType,
blocklistState: blocklistState,
};
},
// Map the plugin's name to a filtered version more suitable for user UI.
makeNicePluginName : function (aName) {
if (aName == "Shockwave Flash")
return "Adobe Flash";
// Clean up the plugin name by stripping off parenthetical clauses,
// trailing version numbers or "plugin".
// EG, "Foo Bar (Linux) Plugin 1.23_02" --> "Foo Bar"
// Do this by first stripping the numbers, etc. off the end, and then
// removing "Plugin" (and then trimming to get rid of any whitespace).
// (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled)
let newName = aName.replace(/\(.*?\)/g, "").
replace(/[\s\d\.\-\_\(\)]+$/, "").
replace(/\bplug-?in\b/i, "").trim();
return newName;
},
/**
* Update the visibility of the plugin overlay.
*/
setVisibility : function (plugin, overlay, shouldShow) {
overlay.classList.toggle("visible", shouldShow);
},
/**
* Check whether the plugin should be visible on the page. A plugin should
* not be visible if the overlay is too big, or if any other page content
* overlays it.
*
* This function will handle showing or hiding the overlay.
* @returns true if the plugin is invisible.
*/
shouldShowOverlay : function (plugin, overlay) {
// If the overlay size is 0, we haven't done layout yet. Presume that
// plugins are visible until we know otherwise.
if (overlay.scrollWidth == 0) {
return true;
}
// Is the <object>'s size too small to hold what we want to show?
let pluginRect = plugin.getBoundingClientRect();
// XXX bug 446693. The text-shadow on the submitted-report text at
// the bottom causes scrollHeight to be larger than it should be.
let overflows = (overlay.scrollWidth > Math.ceil(pluginRect.width)) ||
(overlay.scrollHeight - 5 > Math.ceil(pluginRect.height));
if (overflows) {
return false;
}
// Is the plugin covered up by other content so that it is not clickable?
// Floating point can confuse .elementFromPoint, so inset just a bit
let left = pluginRect.left + 2;
let right = pluginRect.right - 2;
let top = pluginRect.top + 2;
let bottom = pluginRect.bottom - 2;
let centerX = left + (right - left) / 2;
let centerY = top + (bottom - top) / 2;
let points = [[left, top],
[left, bottom],
[right, top],
[right, bottom],
[centerX, centerY]];
if (right <= 0 || top <= 0) {
return false;
}
let contentWindow = plugin.ownerDocument.defaultView;
let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
for (let [x, y] of points) {
let el = cwu.elementFromPoint(x, y, true, true);
if (el !== plugin) {
return false;
}
}
return true;
},
addLinkClickCallback: function (linkNode, callbackName /*callbackArgs...*/) {
// XXX just doing (callback)(arg) was giving a same-origin error. bug?
let self = this;
let callbackArgs = Array.prototype.slice.call(arguments).slice(2);
linkNode.addEventListener("click",
function(evt) {
if (!evt.isTrusted)
return;
evt.preventDefault();
if (callbackArgs.length == 0)
callbackArgs = [ evt ];
(self[callbackName]).apply(self, callbackArgs);
},
true);
linkNode.addEventListener("keydown",
function(evt) {
if (!evt.isTrusted)
return;
if (evt.keyCode == evt.DOM_VK_RETURN) {
evt.preventDefault();
if (callbackArgs.length == 0)
callbackArgs = [ evt ];
evt.preventDefault();
(self[callbackName]).apply(self, callbackArgs);
}
},
true);
},
// Helper to get the binding handler type from a plugin object
_getBindingType : function(plugin) {
if (!(plugin instanceof Ci.nsIObjectLoadingContent))
return null;
switch (plugin.pluginFallbackType) {
case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED:
return "PluginNotFound";
case Ci.nsIObjectLoadingContent.PLUGIN_DISABLED:
return "PluginDisabled";
case Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED:
return "PluginBlocklisted";
case Ci.nsIObjectLoadingContent.PLUGIN_OUTDATED:
return "PluginOutdated";
case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
return "PluginClickToPlay";
case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
return "PluginVulnerableUpdatable";
case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
return "PluginVulnerableNoUpdate";
case Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW:
return "PluginPlayPreview";
default:
// Not all states map to a handler
return null;
}
},
handleEvent: function (event) {
let eventType = event.type;
if (eventType == "unload") {
this.uninit();
return;
}
if (eventType == "PluginRemoved") {
this.updateNotificationUI();
return;
}
if (eventType == "click") {
this.onOverlayClick(event);
return;
}
if (eventType == "PluginCrashed" &&
!(event.target instanceof Ci.nsIObjectLoadingContent)) {
// If the event target is not a plugin object (i.e., an <object> or
// <embed> element), this call is for a window-global plugin.
this.pluginInstanceCrashed(event.target, event);
return;
}
let plugin = event.target;
let doc = plugin.ownerDocument;
if (!(plugin instanceof Ci.nsIObjectLoadingContent))
return;
if (eventType == "PluginBindingAttached") {
// The plugin binding fires this event when it is created.
// As an untrusted event, ensure that this object actually has a binding
// and make sure we don't handle it twice
let overlay = this.getPluginUI(plugin, "main");
if (!overlay || overlay._bindingHandled) {
return;
}
overlay._bindingHandled = true;
// Lookup the handler for this binding
eventType = this._getBindingType(plugin);
if (!eventType) {
// Not all bindings have handlers
return;
}
}
let shouldShowNotification = false;
switch (eventType) {
case "PluginCrashed":
this.pluginInstanceCrashed(plugin, event);
break;
case "PluginNotFound": {
let installable = this.showInstallNotification(plugin, eventType);
let contentWindow = plugin.ownerDocument.defaultView;
// For non-object plugin tags, register a click handler to install the
// plugin. Object tags can, and often do, deal with that themselves,
// so don't stomp on the page developers toes.
if (installable && !(plugin instanceof contentWindow.HTMLObjectElement)) {
let installStatus = this.getPluginUI(plugin, "installStatus");
installStatus.setAttribute("installable", "true");
let iconStatus = this.getPluginUI(plugin, "icon");
iconStatus.setAttribute("installable", "true");
let installLink = this.getPluginUI(plugin, "installPluginLink");
this.addLinkClickCallback(installLink, "installSinglePlugin", plugin);
}
break;
}
case "PluginBlocklisted":
case "PluginOutdated":
shouldShowNotification = true;
break;
case "PluginVulnerableUpdatable":
let updateLink = this.getPluginUI(plugin, "checkForUpdatesLink");
this.addLinkClickCallback(updateLink, "forwardCallback", "openPluginUpdatePage");
/* FALLTHRU */
case "PluginVulnerableNoUpdate":
case "PluginClickToPlay":
this._handleClickToPlayEvent(plugin);
let overlay = this.getPluginUI(plugin, "main");
let pluginName = this._getPluginInfo(plugin).pluginName;
let messageString = gNavigatorBundle.formatStringFromName("PluginClickToActivate", [pluginName], 1);
let overlayText = this.getPluginUI(plugin, "clickToPlay");
overlayText.textContent = messageString;
if (eventType == "PluginVulnerableUpdatable" ||
eventType == "PluginVulnerableNoUpdate") {
let vulnerabilityString = gNavigatorBundle.GetStringFromName(eventType);
let vulnerabilityText = this.getPluginUI(plugin, "vulnerabilityStatus");
vulnerabilityText.textContent = vulnerabilityString;
}
shouldShowNotification = true;
break;
case "PluginPlayPreview":
this._handlePlayPreviewEvent(plugin);
break;
case "PluginDisabled":
let manageLink = this.getPluginUI(plugin, "managePluginsLink");
this.addLinkClickCallback(manageLink, "forwardCallback", "managePlugins");
shouldShowNotification = true;
break;
case "PluginInstantiated":
shouldShowNotification = true;
break;
}
// Show the in-content UI if it's not too big. The crashed plugin handler already did this.
if (eventType != "PluginCrashed") {
let overlay = this.getPluginUI(plugin, "main");
if (overlay != null) {
this.setVisibility(plugin, overlay,
this.shouldShowOverlay(plugin, overlay));
let resizeListener = (event) => {
this.setVisibility(plugin, overlay,
this.shouldShowOverlay(plugin, overlay));
this.updateNotificationUI();
};
plugin.addEventListener("overflow", resizeListener);
plugin.addEventListener("underflow", resizeListener);
}
}
let closeIcon = this.getPluginUI(plugin, "closeIcon");
if (closeIcon) {
closeIcon.addEventListener("click", event => {
if (event.button == 0 && event.isTrusted)
this.hideClickToPlayOverlay(plugin);
}, true);
}
if (shouldShowNotification) {
this._showClickToPlayNotification(plugin, false);
}
},
isKnownPlugin: function (objLoadingContent) {
return (objLoadingContent.getContentTypeForMIMEType(objLoadingContent.actualType) ==
Ci.nsIObjectLoadingContent.TYPE_PLUGIN);
},
canActivatePlugin: function (objLoadingContent) {
// if this isn't a known plugin, we can't activate it
// (this also guards pluginHost.getPermissionStringForType against
// unexpected input)
if (!this.isKnownPlugin(objLoadingContent))
return false;
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType);
let principal = objLoadingContent.ownerDocument.defaultView.top.document.nodePrincipal;
let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString);
let isFallbackTypeValid =
objLoadingContent.pluginFallbackType >= Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
objLoadingContent.pluginFallbackType <= Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE;
if (objLoadingContent.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) {
// checking if play preview is subject to CTP rules
let playPreviewInfo = pluginHost.getPlayPreviewInfo(objLoadingContent.actualType);
isFallbackTypeValid = !playPreviewInfo.ignoreCTP;
}
return !objLoadingContent.activated &&
pluginPermission != Ci.nsIPermissionManager.DENY_ACTION &&
isFallbackTypeValid;
},
hideClickToPlayOverlay: function (plugin) {
let overlay = this.getPluginUI(plugin, "main");
if (overlay) {
overlay.classList.remove("visible");
}
},
stopPlayPreview: function (plugin, playPlugin) {
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
if (objLoadingContent.activated)
return;
if (playPlugin)
objLoadingContent.playPlugin();
else
objLoadingContent.cancelPlayPreview();
},
// Callback for user clicking on a missing (unsupported) plugin.
installSinglePlugin: function (plugin) {
this.global.sendAsyncMessage("PluginContent:InstallSinglePlugin", {
pluginInfo: this._getPluginInfo(plugin),
});
},
// Forward a link click callback to the chrome process.
forwardCallback: function (name) {
this.global.sendAsyncMessage("PluginContent:LinkClickCallback", { name: name });
},
#ifdef MOZ_CRASHREPORTER
submitReport: function submitReport(pluginDumpID, browserDumpID, plugin) {
let keyVals = {};
if (plugin) {
let userComment = this.getPluginUI(plugin, "submitComment").value.trim();
if (userComment)
keyVals.PluginUserComment = userComment;
if (this.getPluginUI(plugin, "submitURLOptIn").checked)
keyVals.PluginContentURL = plugin.ownerDocument.URL;
}
this.global.sendAsyncMessage("PluginContent:SubmitReport", {
pluginDumpID: pluginDumpID,
browserDumpID: browserDumpID,
keyVals: keyVals,
});
},
#endif
reloadPage: function () {
this.global.content.location.reload();
},
showInstallNotification: function (plugin) {
let [shown] = this.global.sendSyncMessage("PluginContent:ShowInstallNotification", {
pluginInfo: this._getPluginInfo(plugin),
});
return shown;
},
// Event listener for click-to-play plugins.
_handleClickToPlayEvent: function (plugin) {
let doc = plugin.ownerDocument;
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
// guard against giving pluginHost.getPermissionStringForType a type
// not associated with any known plugin
if (!this.isKnownPlugin(objLoadingContent))
return;
let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType);
let principal = doc.defaultView.top.document.nodePrincipal;
let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString);
let overlay = this.getPluginUI(plugin, "main");
if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) {
if (overlay) {
overlay.classList.remove("visible");
}
return;
}
if (overlay) {
overlay.addEventListener("click", this, true);
}
},
onOverlayClick: function (event) {
let document = event.target.ownerDocument;
let plugin = document.getBindingParent(event.target);
let contentWindow = plugin.ownerDocument.defaultView.top;
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
// Have to check that the target is not the link to update the plugin
if (!(event.originalTarget instanceof contentWindow.HTMLAnchorElement) &&
(event.originalTarget.getAttribute('anonid') != 'closeIcon') &&
event.button == 0 && event.isTrusted) {
this._showClickToPlayNotification(plugin, true);
event.stopPropagation();
event.preventDefault();
}
},
_handlePlayPreviewEvent: function (plugin) {
let doc = plugin.ownerDocument;
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
let pluginInfo = this._getPluginInfo(plugin);
let playPreviewInfo = pluginHost.getPlayPreviewInfo(pluginInfo.mimetype);
let previewContent = this.getPluginUI(plugin, "previewPluginContent");
let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
if (!iframe) {
// lazy initialization of the iframe
iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
iframe.className = "previewPluginContentFrame";
previewContent.appendChild(iframe);
// Force a style flush, so that we ensure our binding is attached.
plugin.clientTop;
}
iframe.src = playPreviewInfo.redirectURL;
// MozPlayPlugin event can be dispatched from the extension chrome
// code to replace the preview content with the native plugin
let playPluginHandler = (event) => {
if (!event.isTrusted)
return;
previewContent.removeEventListener("MozPlayPlugin", playPluginHandler, true);
let playPlugin = !event.detail;
this.stopPlayPreview(plugin, playPlugin);
// cleaning up: removes overlay iframe from the DOM
let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
if (iframe)
previewContent.removeChild(iframe);
};
previewContent.addEventListener("MozPlayPlugin", playPluginHandler, true);
if (!playPreviewInfo.ignoreCTP) {
this._showClickToPlayNotification(plugin, false);
}
},
reshowClickToPlayNotification: function () {
let contentWindow = this.global.content;
let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let plugins = cwu.plugins;
for (let plugin of plugins) {
let overlay = this.getPluginUI(plugin, "main");
if (overlay)
overlay.removeEventListener("click", this, true);
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
if (this.canActivatePlugin(objLoadingContent))
this._handleClickToPlayEvent(plugin);
}
this._showClickToPlayNotification(null, false);
},
// Match the behaviour of nsPermissionManager
_getHostFromPrincipal: function (principal) {
if (!principal.URI || principal.URI.schemeIs("moz-nullprincipal")) {
return "(null)";
}
try {
if (principal.URI.host)
return principal.URI.host;
} catch (e) {}
return principal.origin;
},
/**
* Activate the plugins that the user has specified.
*/
activatePlugins: function (pluginInfo, newState) {
let contentWindow = this.global.content;
let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let plugins = cwu.plugins;
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
let pluginFound = false;
for (let plugin of plugins) {
plugin.QueryInterface(Ci.nsIObjectLoadingContent);
if (!this.isKnownPlugin(plugin)) {
continue;
}
if (pluginInfo.permissionString == pluginHost.getPermissionStringForType(plugin.actualType)) {
pluginFound = true;
if (newState == "block") {
plugin.reload(true);
} else {
if (this.canActivatePlugin(plugin)) {
let overlay = this.getPluginUI(plugin, "main");
if (overlay) {
overlay.removeEventListener("click", this, true);
}
plugin.playPlugin();
}
}
}
}
// If there are no instances of the plugin on the page any more, what the
// user probably needs is for us to allow and then refresh.
if (newState != "block" && !pluginFound) {
this.reloadPage();
}
this.updateNotificationUI();
},
_showClickToPlayNotification: function (plugin, showNow) {
let plugins = [];
// If plugin is null, that means the user has navigated back to a page with
// plugins, and we need to collect all the plugins.
if (plugin === null) {
let contentWindow = this.global.content;
let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
// cwu.plugins may contain non-plugin <object>s, filter them out
plugins = cwu.plugins.filter((plugin) =>
plugin.getContentTypeForMIMEType(plugin.actualType) == Ci.nsIObjectLoadingContent.TYPE_PLUGIN);
if (plugins.length == 0) {
this.removeNotification("click-to-play-plugins");
return;
}
} else {
plugins = [plugin];
}
let pluginData = this.pluginData;
let principal = this.global.content.document.nodePrincipal;
let principalHost = this._getHostFromPrincipal(principal);
for (let p of plugins) {
let pluginInfo = this._getPluginInfo(p);
if (pluginInfo.permissionString === null) {
Cu.reportError("No permission string for active plugin.");
continue;
}
if (pluginData.has(pluginInfo.permissionString)) {
continue;
}
let permissionObj = Services.perms.
getPermissionObject(principal, pluginInfo.permissionString, false);
if (permissionObj) {
pluginInfo.pluginPermissionHost = permissionObj.host;
pluginInfo.pluginPermissionType = permissionObj.expireType;
}
else {
pluginInfo.pluginPermissionHost = principalHost;
pluginInfo.pluginPermissionType = undefined;
}
this.pluginData.set(pluginInfo.permissionString, pluginInfo);
}
this.global.sendAsyncMessage("PluginContent:ShowClickToPlayNotification", {
plugins: [... this.pluginData.values()],
showNow: showNow,
host: principalHost,
}, null, principal);
},
updateNotificationUI: function () {
// Make a copy of the actions from the last popup notification.
let haveInsecure = false;
let actions = new Map();
for (let action of this.pluginData.values()) {
switch (action.fallbackType) {
// haveInsecure will trigger the red flashing icon and the infobar
// styling below
case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
haveInsecure = true;
// fall through
case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
actions.set(action.permissionString, action);
continue;
}
}
// Remove plugins that are already active, or large enough to show an overlay.
let contentWindow = this.global.content;
let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
for (let plugin of cwu.plugins) {
let info = this._getPluginInfo(plugin);
if (!actions.has(info.permissionString)) {
continue;
}
let fallbackType = info.fallbackType;
if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
actions.delete(info.permissionString);
if (actions.size == 0) {
break;
}
continue;
}
if (fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE &&
fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE) {
continue;
}
let overlay = this.getPluginUI(plugin, "main");
if (!overlay) {
continue;
}
let shouldShow = this.shouldShowOverlay(plugin, overlay);
this.setVisibility(plugin, overlay, shouldShow);
if (shouldShow) {
actions.delete(info.permissionString);
if (actions.size == 0) {
break;
}
}
}
// If there are any items remaining in `actions` now, they are hidden
// plugins that need a notification bar.
let principal = contentWindow.document.nodePrincipal;
this.global.sendAsyncMessage("PluginContent:UpdateHiddenPluginUI", {
haveInsecure: haveInsecure,
actions: [... actions.values()],
host: this._getHostFromPrincipal(principal),
}, null, principal);
},
removeNotification: function (name) {
this.global.sendAsyncMessage("PluginContent:RemoveNotification", { name: name });
},
clearPluginDataCache: function () {
this.pluginData.clear();
},
hideNotificationBar: function (name) {
this.global.sendAsyncMessage("PluginContent:HideNotificationBar", { name: name });
},
// Crashed-plugin event listener. Called for every instance of a
// plugin in content.
pluginInstanceCrashed: function (target, aEvent) {
// Ensure the plugin and event are of the right type.
if (!(aEvent instanceof Ci.nsIDOMCustomEvent))
return;
let propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2);
let submittedReport = propBag.getPropertyAsBool("submittedCrashReport");
let doPrompt = true; // XXX followup for .getPropertyAsBool("doPrompt");
let submitReports = true; // XXX followup for .getPropertyAsBool("submitReports");
let pluginName = propBag.getPropertyAsAString("pluginName");
let pluginDumpID = propBag.getPropertyAsAString("pluginDumpID");
let browserDumpID = null;
let gmpPlugin = false;
try {
browserDumpID = propBag.getPropertyAsAString("browserDumpID");
} catch (e) {
// For GMP crashes we don't get a browser dump.
}
try {
gmpPlugin = propBag.getPropertyAsBool("gmpPlugin");
} catch (e) {
// This property is only set for GMP plugins.
}
// For non-GMP plugins, remap the plugin name to a more user-presentable form.
if (!gmpPlugin) {
pluginName = this.makeNicePluginName(pluginName);
}
let messageString = gNavigatorBundle.formatStringFromName("crashedpluginsMessage.title", [pluginName], 1);
let plugin = null, doc;
if (target instanceof Ci.nsIObjectLoadingContent) {
plugin = target;
doc = plugin.ownerDocument;
} else {
doc = target.document;
if (!doc) {
return;
}
// doPrompt is specific to the crashed plugin overlay, and
// therefore is not applicable for window-global plugins.
doPrompt = false;
}
let status;
#ifdef MOZ_CRASHREPORTER
// Determine which message to show regarding crash reports.
if (submittedReport) { // submitReports && !doPrompt, handled in observer
status = "submitted";
}
else if (!submitReports && !doPrompt) {
status = "noSubmit";
}
else if (!pluginDumpID) {
// If we don't have a minidumpID, we can't (or didn't) submit anything.
// This can happen if the plugin is killed from the task manager.
status = "noReport";
}
else {
status = "please";
}
// If we don't have a minidumpID, we can't (or didn't) submit anything.
// This can happen if the plugin is killed from the task manager.
if (!pluginDumpID) {
status = "noReport";
}
// If we're showing the link to manually trigger report submission, we'll
// want to be able to update all the instances of the UI for this crash to
// show an updated message when a report is submitted.
if (doPrompt) {
let observer = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
observe : (subject, topic, data) => {
let propertyBag = subject;
if (!(propertyBag instanceof Ci.nsIPropertyBag2))
return;
// Ignore notifications for other crashes.
if (propertyBag.get("minidumpID") != pluginDumpID)
return;
let statusDiv = this.getPluginUI(plugin, "submitStatus");
statusDiv.setAttribute("status", data);
},
handleEvent : function(event) {
// Not expected to be called, just here for the closure.
}
}
// Use a weak reference, so we don't have to remove it...
Services.obs.addObserver(observer, "crash-report-status", true);
// ...alas, now we need something to hold a strong reference to prevent
// it from being GC. But I don't want to manually manage the reference's
// lifetime (which should be no greater than the page).
// Clever solution? Use a closue with an event listener on the document.
// When the doc goes away, so do the listener references and the closure.
doc.addEventListener("mozCleverClosureHack", observer, false);
}
#endif
let isShowing = false;
if (plugin) {
// If there's no plugin (an <object> or <embed> element), this call is
// for a window-global plugin. In this case, there's no overlay to show.
isShowing = _setUpPluginOverlay.call(this, plugin, doPrompt);
}
if (isShowing) {
// If a previous plugin on the page was too small and resulted in adding a
// notification bar, then remove it because this plugin instance it big
// enough to serve as in-content notification.
this.hideNotificationBar("plugin-crashed");
doc.mozNoPluginCrashedNotification = true;
} else {
// If another plugin on the page was large enough to show our UI, we don't
// want to show a notification bar.
if (!doc.mozNoPluginCrashedNotification) {
this.global.sendAsyncMessage("PluginContent:ShowPluginCrashedNotification", {
messageString: messageString,
pluginDumpID: pluginDumpID,
browserDumpID: browserDumpID,
});
// Remove the notification when the page is reloaded.
doc.defaultView.top.addEventListener("unload", event => {
this.hideNotificationBar("plugin-crashed");
}, false);
}
}
// Configure the crashed-plugin placeholder.
// Returns true if the plugin overlay is visible.
function _setUpPluginOverlay(plugin, doPromptSubmit) {
if (!plugin) {
return false;
}
// Force a layout flush so the binding is attached.
plugin.clientTop;
let overlay = this.getPluginUI(plugin, "main");
let statusDiv = this.getPluginUI(plugin, "submitStatus");
if (doPromptSubmit) {
this.getPluginUI(plugin, "submitButton").addEventListener("click",
function (event) {
if (event.button != 0 || !event.isTrusted)
return;
this.submitReport(pluginDumpID, browserDumpID, plugin);
pref.setBoolPref("", optInCB.checked);
}.bind(this));
let optInCB = this.getPluginUI(plugin, "submitURLOptIn");
let pref = Services.prefs.getBranch("dom.ipc.plugins.reportCrashURL");
optInCB.checked = pref.getBoolPref("");
}
statusDiv.setAttribute("status", status);
let helpIcon = this.getPluginUI(plugin, "helpIcon");
this.addLinkClickCallback(helpIcon, "openHelpPage");
let crashText = this.getPluginUI(plugin, "crashedText");
crashText.textContent = messageString;
let link = this.getPluginUI(plugin, "reloadLink");
this.addLinkClickCallback(link, "reloadPage");
let isShowing = this.shouldShowOverlay(plugin, overlay);
// Is the <object>'s size too small to hold what we want to show?
if (!isShowing) {
// First try hiding the crash report submission UI.
statusDiv.removeAttribute("status");
isShowing = this.shouldShowOverlay(plugin, overlay);
}
this.setVisibility(plugin, overlay, isShowing);
return isShowing;
}
}
};

View File

@ -60,10 +60,11 @@ this.TabCrashReporter = {
if (CrashSubmit.submit(dumpID, { recordSubmission: true })) {
this.childMap.set(childID, null); // Avoid resubmission.
this.removeSubmitCheckboxesForSameCrash(childID);
}
},
reloadCrashedTabs: function () {
removeSubmitCheckboxesForSameCrash: function(childID) {
let enumerator = Services.wm.getEnumerator("navigator:browser");
while (enumerator.hasMoreElements()) {
let window = enumerator.getNext();
@ -78,13 +79,27 @@ this.TabCrashReporter = {
if (!doc.documentURI.startsWith("about:tabcrashed"))
continue;
let url = browser.currentURI.spec;
window.gBrowser.updateBrowserRemotenessByURL(browser, url);
browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
if (this.browserMap.get(browser) == childID) {
this.browserMap.delete(browser);
browser.contentDocument.documentElement.classList.remove("crashDumpAvailable");
}
}
}
},
reloadCrashedTab: function (browser) {
if (browser.isRemoteBrowser)
return;
let doc = browser.contentDocument;
if (!doc.documentURI.startsWith("about:tabcrashed"))
return;
let url = browser.currentURI.spec;
browser.getTabBrowser().updateBrowserRemotenessByURL(browser, url);
browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
},
onAboutTabCrashedLoad: function (aBrowser) {
if (!this.childMap)
return;
@ -93,6 +108,6 @@ this.TabCrashReporter = {
if (!dumpID)
return;
aBrowser.contentDocument.documentElement.classList.add("crashDumpAvaible");
aBrowser.contentDocument.documentElement.classList.add("crashDumpAvailable");
}
}

View File

@ -44,6 +44,7 @@ if CONFIG['NIGHTLY_BUILD']:
EXTRA_PP_JS_MODULES += [
'AboutHome.jsm',
'PluginContent.jsm',
'RecentWindow.jsm',
'UITour.jsm',
'webrtcUI.jsm',

View File

@ -30,7 +30,7 @@ p {
display: none;
}
.crashDumpAvaible #report-box {
.crashDumpAvailable #report-box {
display: block
}

View File

@ -30,7 +30,7 @@ p {
display: none;
}
.crashDumpAvaible #report-box {
.crashDumpAvailable #report-box {
display: block
}

View File

@ -30,7 +30,7 @@ p {
display: none;
}
.crashDumpAvaible #report-box {
.crashDumpAvailable #report-box {
display: block
}

View File

@ -33,11 +33,11 @@ import com.jayway.android.robotium.solo.Solo;
public class FennecNativeDriver implements Driver {
private static final int FRAME_TIME_THRESHOLD = 25; // allow 25ms per frame (40fps)
private Activity mActivity;
private Solo mSolo;
private String mRootPath;
private final Activity mActivity;
private final Solo mSolo;
private final String mRootPath;
private static String mLogFile = null;
private static String mLogFile;
private static LogLevel mLogLevel = LogLevel.INFO;
public enum LogLevel {
@ -46,7 +46,7 @@ public class FennecNativeDriver implements Driver {
WARN(3),
ERROR(4);
private int mValue;
private final int mValue;
LogLevel(int value) {
mValue = value;
}
@ -86,6 +86,7 @@ public class FennecNativeDriver implements Driver {
}
}
@Override
public int getGeckoTop() {
if (!mGeckoInfo) {
getGeckoInfo();
@ -93,6 +94,7 @@ public class FennecNativeDriver implements Driver {
return mGeckoTop;
}
@Override
public int getGeckoLeft() {
if (!mGeckoInfo) {
getGeckoInfo();
@ -100,6 +102,7 @@ public class FennecNativeDriver implements Driver {
return mGeckoLeft;
}
@Override
public int getGeckoHeight() {
if (!mGeckoInfo) {
getGeckoInfo();
@ -107,6 +110,7 @@ public class FennecNativeDriver implements Driver {
return mGeckoHeight;
}
@Override
public int getGeckoWidth() {
if (!mGeckoInfo) {
getGeckoInfo();
@ -118,14 +122,17 @@ public class FennecNativeDriver implements Driver {
*
* @return An Element representing the view, or null if the view is not found.
*/
@Override
public Element findElement(Activity activity, int id) {
return new FennecNativeElement(id, activity);
}
@Override
public void startFrameRecording() {
PanningPerfAPI.startFrameTimeRecording();
}
@Override
public int stopFrameRecording() {
final List<Long> frames = PanningPerfAPI.stopFrameTimeRecording();
int badness = 0;
@ -144,10 +151,12 @@ public class FennecNativeDriver implements Driver {
return badness;
}
@Override
public void startCheckerboardRecording() {
PanningPerfAPI.startCheckerboardRecording();
}
@Override
public float stopCheckerboardRecording() {
final List<Float> checkerboard = PanningPerfAPI.stopCheckerboardRecording();
float total = 0;
@ -169,6 +178,7 @@ public class FennecNativeDriver implements Driver {
return layerView;
}
@Override
public PaintedSurface getPaintedSurface() {
final LayerView view = getSurfaceView();
if (view == null) {
@ -223,16 +233,20 @@ public class FennecNativeDriver implements Driver {
public int mScrollHeight=0;
public int mPageHeight=10;
@Override
public int getScrollHeight() {
return mScrollHeight;
}
@Override
public int getPageHeight() {
return mPageHeight;
}
@Override
public int getHeight() {
return mHeight;
}
@Override
public void setupScrollHandling() {
EventDispatcher.getInstance().registerGeckoThreadListener(new GeckoEventListener() {
@Override

View File

@ -13,25 +13,28 @@ import android.widget.TextView;
public class FennecNativeElement implements Element {
private final Activity mActivity;
private Integer mId;
private final Integer mId;
public FennecNativeElement(Integer id, Activity activity) {
mId = id;
mActivity = activity;
}
@Override
public Integer getId() {
return mId;
}
private boolean mClickSuccess;
@Override
public boolean click() {
mClickSuccess = false;
RobocopUtils.runOnUiThreadSync(mActivity,
new Runnable() {
@Override
public void run() {
View view = (View)mActivity.findViewById(mId);
View view = mActivity.findViewById(mId);
if (view != null) {
if (view.performClick()) {
mClickSuccess = true;
@ -50,10 +53,12 @@ public class FennecNativeElement implements Element {
private Object mText;
@Override
public String getText() {
mText = null;
RobocopUtils.runOnUiThreadSync(mActivity,
new Runnable() {
@Override
public void run() {
View v = mActivity.findViewById(mId);
if (v instanceof EditText) {
@ -92,12 +97,14 @@ public class FennecNativeElement implements Element {
private boolean mDisplayed;
@Override
public boolean isDisplayed() {
mDisplayed = false;
RobocopUtils.runOnUiThreadSync(mActivity,
new Runnable() {
@Override
public void run() {
View view = (View)mActivity.findViewById(mId);
View view = mActivity.findViewById(mId);
if (view != null) {
mDisplayed = true;
}

View File

@ -4,8 +4,7 @@
package org.mozilla.gecko;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import android.app.Activity;
@ -15,27 +14,45 @@ public final class RobocopUtils {
private RobocopUtils() {}
public static void runOnUiThreadSync(Activity activity, final Runnable runnable) {
final SynchronousQueue syncQueue = new SynchronousQueue();
final AtomicBoolean sentinel = new AtomicBoolean(false);
// On the UI thread, run the Runnable, then set sentinel to true and wake this thread.
activity.runOnUiThread(
new Runnable() {
@Override
public void run() {
runnable.run();
try {
syncQueue.put(new Object());
} catch (InterruptedException e) {
FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
synchronized (sentinel) {
sentinel.set(true);
sentinel.notifyAll();
}
}
});
try {
// Wait for the UiThread code to finish running
if (syncQueue.poll(MAX_WAIT_MS, TimeUnit.MILLISECONDS) == null) {
FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
"time-out waiting for UI thread");
FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
}
} catch (InterruptedException e) {
FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
);
// Suspend this thread, until the other thread completes its work or until a timeout is
// reached.
long startTimestamp = System.currentTimeMillis();
synchronized (sentinel) {
while (!sentinel.get()) {
try {
sentinel.wait(MAX_WAIT_MS);
} catch (InterruptedException e) {
FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
}
// Abort if we woke up due to timeout (instead of spuriously).
if (System.currentTimeMillis() - startTimestamp >= MAX_WAIT_MS) {
FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
"time-out waiting for UI thread");
FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
return;
}
}
}
}
}

View File

@ -1259,6 +1259,7 @@ void MediaPipelineTransmit::PipelineListener::ProcessVideoChunk(
MOZ_ASSERT(PR_FALSE);
}
conduit->SendVideoFrame(yuv, buffer_size, size.width, size.height, mozilla::kVideoI420, 0);
free(yuv);
} else {
MOZ_MTLOG(ML_ERROR, "Unsupported video format");
MOZ_ASSERT(PR_FALSE);

View File

@ -730,10 +730,6 @@ public class BrowserApp extends GeckoApp
super.onResume();
EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
"Prompt:ShowTop");
if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) {
// Starts or pings the stumbler lib, see also usage in handleMessage(): Gecko:DelayedStartup.
GeckoPreferences.broadcastStumblerPref(this);
}
}
@Override
@ -1509,7 +1505,14 @@ public class BrowserApp extends GeckoApp
if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) {
// Start (this acts as ping if started already) the stumbler lib; if the stumbler has queued data it will upload it.
// Stumbler operates on its own thread, and startup impact is further minimized by delaying work (such as upload) a few seconds.
GeckoPreferences.broadcastStumblerPref(this);
// Avoid any potential startup CPU/thread contention by delaying the pref broadcast.
final long oneSecondInMillis = 1000;
ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
@Override
public void run() {
GeckoPreferences.broadcastStumblerPref(BrowserApp.this);
}
}, oneSecondInMillis);
}
super.handleMessage(event, message);
} else if (event.equals("Gecko:Ready")) {

View File

@ -891,7 +891,7 @@ OnSharedPreferenceChangeListener
* <code>PREFS_STUMBLER_ENABLED</code> pref.
*/
public static void broadcastStumblerPref(final Context context) {
final boolean value = getBooleanPref(context, PREFS_GEO_REPORTING, true);
final boolean value = getBooleanPref(context, PREFS_GEO_REPORTING, false);
broadcastStumblerPref(context, value);
}

View File

@ -195,7 +195,7 @@ abstract class AboutHomeTest extends PixelTest {
boolean correctTab = waitForCondition(new Condition() {
@Override
public boolean isSatisfied() {
ViewPager pager = (ViewPager)mSolo.getView(ViewPager.class, 0);
ViewPager pager = mSolo.getView(ViewPager.class, 0);
return (pager.getCurrentItem() == tabIndex);
}
}, MAX_WAIT_MS);
@ -237,7 +237,7 @@ abstract class AboutHomeTest extends PixelTest {
*/
protected void openAboutHomeTab(AboutHomeTabs tab) {
focusUrlBar();
ViewPager pager = (ViewPager)mSolo.getView(ViewPager.class, 0);
ViewPager pager = mSolo.getView(ViewPager.class, 0);
final int currentTabIndex = pager.getCurrentItem();
int tabOffset;

View File

@ -18,6 +18,7 @@ import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
@SuppressWarnings("unchecked")
public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
public enum Type {
MOCHITEST,
@ -60,7 +61,6 @@ public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<A
* specify a different activity class to the one-argument constructor. To do
* as little as possible, specify <code>Activity.class</code>.
*/
@SuppressWarnings("unchecked")
public BaseRobocopTest() {
this((Class<Activity>) BROWSER_INTENT_CLASS);
}
@ -96,7 +96,7 @@ public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<A
}
String configFile = FennecNativeDriver.getFile(mRootPath + "/robotium.config");
mConfig = FennecNativeDriver.convertTextToTable(configFile);
mLogFile = (String) mConfig.get("logfile");
mLogFile = mConfig.get("logfile");
// Initialize the asserter.
if (getTestType() == Type.TALOS) {
@ -105,6 +105,6 @@ public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<A
mAsserter = new FennecMochitestAssert();
}
mAsserter.setLogFile(mLogFile);
mAsserter.setTestName(this.getClass().getName());
mAsserter.setTestName(getClass().getName());
}
}

View File

@ -78,10 +78,9 @@ abstract class BaseTest extends BaseRobocopTest {
protected String mProfile;
public Device mDevice;
protected DatabaseHelper mDatabaseHelper;
protected StringHelper mStringHelper;
protected int mScreenMidWidth;
protected int mScreenMidHeight;
private HashSet<Integer> mKnownTabIDs = new HashSet<Integer>();
private final HashSet<Integer> mKnownTabIDs = new HashSet<Integer>();
protected void blockForGeckoReady() {
try {
@ -100,12 +99,12 @@ abstract class BaseTest extends BaseRobocopTest {
super.setUp();
// Create the intent to be used with all the important arguments.
mBaseUrl = ((String) mConfig.get("host")).replaceAll("(/$)", "");
mRawBaseUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", "");
mBaseUrl = mConfig.get("host").replaceAll("(/$)", "");
mRawBaseUrl = mConfig.get("rawhost").replaceAll("(/$)", "");
Intent i = new Intent(Intent.ACTION_MAIN);
mProfile = (String) mConfig.get("profile");
mProfile = mConfig.get("profile");
i.putExtra("args", "-no-remote -profile " + mProfile);
String envString = (String) mConfig.get("envvars");
String envString = mConfig.get("envvars");
if (envString != "") {
String[] envStrings = envString.split(",");
for (int iter = 0; iter < envStrings.length; iter++) {
@ -122,7 +121,6 @@ abstract class BaseTest extends BaseRobocopTest {
mActions = new FennecNativeActions(mActivity, mSolo, getInstrumentation(), mAsserter);
mDevice = new Device();
mDatabaseHelper = new DatabaseHelper(mActivity, mAsserter);
mStringHelper = new StringHelper();
}
protected void initializeProfile() {
@ -275,8 +273,8 @@ abstract class BaseTest extends BaseRobocopTest {
}
class VerifyTextViewText implements Condition {
private TextView mTextView;
private String mExpected;
private final TextView mTextView;
private final String mExpected;
public VerifyTextViewText(TextView textView, String expected) {
mTextView = textView;
mExpected = expected;
@ -304,7 +302,7 @@ abstract class BaseTest extends BaseRobocopTest {
boolean result = mSolo.waitForCondition(condition, timeout);
if (!result) {
// Log timeout failure for diagnostic purposes only; a failed wait may
// be normal and does not necessarily warrant a test asssertion/failure.
// be normal and does not necessarily warrant a test assertion/failure.
mAsserter.dumpLog("waitForCondition timeout after " + timeout + " ms.");
}
return result;
@ -757,8 +755,8 @@ abstract class BaseTest extends BaseRobocopTest {
}
class Navigation {
private String devType;
private String osVersion;
private final String devType;
private final String osVersion;
public Navigation(Device mDevice) {
devType = mDevice.type;
@ -862,8 +860,8 @@ abstract class BaseTest extends BaseRobocopTest {
*/
private class DescriptionCondition<T extends View> implements Condition {
public T mView;
private String mDescr;
private Class<T> mCls;
private final String mDescr;
private final Class<T> mCls;
public DescriptionCondition(Class<T> cls, String descr) {
mDescr = descr;

View File

@ -2,6 +2,8 @@ package org.mozilla.gecko.tests;
public class StringHelper {
private StringHelper() {}
public static final String OK = "OK";
// Note: DEFAULT_BOOKMARKS_TITLES.length == DEFAULT_BOOKMARKS_URLS.length

View File

@ -68,8 +68,8 @@ abstract class UITest extends BaseRobocopTest
mDriver = new FennecNativeDriver(activity, mSolo, mRootPath);
mActions = new FennecNativeActions(activity, mSolo, getInstrumentation(), mAsserter);
mBaseHostnameUrl = ((String) mConfig.get("host")).replaceAll("(/$)", "");
mBaseIpUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", "");
mBaseHostnameUrl = mConfig.get("host").replaceAll("(/$)", "");
mBaseIpUrl = mConfig.get("rawhost").replaceAll("(/$)", "");
// Helpers depend on components so initialize them first.
initComponents();
@ -158,6 +158,7 @@ abstract class UITest extends BaseRobocopTest
* Returns the test type. By default this returns MOCHITEST, but tests can override this
* method in order to change the type of the test.
*/
@Override
protected Type getTestType() {
return Type.MOCHITEST;
}
@ -179,10 +180,10 @@ abstract class UITest extends BaseRobocopTest
private static Intent createActivityIntent(final Map<String, String> config) {
final Intent intent = new Intent(Intent.ACTION_MAIN);
final String profile = (String) config.get("profile");
final String profile = config.get("profile");
intent.putExtra("args", "-no-remote -profile " + profile);
final String envString = (String) config.get("envvars");
final String envString = config.get("envvars");
if (!TextUtils.isEmpty(envString)) {
final String[] envStrings = envString.split(",");

View File

@ -18,6 +18,10 @@ import android.widget.TextView;
import com.jayway.android.robotium.solo.Condition;
import com.jayway.android.robotium.solo.Solo;
import org.mozilla.gecko.util.HardwareUtils;
import java.util.Arrays;
import java.util.Enumeration;
/**
* A class representing any interactions that take place on the Awesomescreen.
@ -37,22 +41,21 @@ public class AboutHomeComponent extends BaseComponent {
// TODO: Having a specific ordering of panels is prone to fail and thus temporary.
// Hopefully the work in bug 940565 will alleviate the need for these enums.
// Explicit ordering of HomePager panels on a phone.
private enum PhonePanel {
RECENT_TABS,
HISTORY,
TOP_SITES,
BOOKMARKS,
READING_LIST
}
private static final PanelType[] PANEL_ORDERING_PHONE = {
PanelType.RECENT_TABS,
PanelType.HISTORY,
PanelType.TOP_SITES,
PanelType.BOOKMARKS,
PanelType.READING_LIST
};
// Explicit ordering of HomePager panels on a tablet.
private enum TabletPanel {
TOP_SITES,
BOOKMARKS,
READING_LIST,
HISTORY,
RECENT_TABS
}
private static final PanelType[] PANEL_ORDERING_TABLET = {
PanelType.TOP_SITES,
PanelType.BOOKMARKS,
PanelType.READING_LIST,
PanelType.HISTORY,
PanelType.RECENT_TABS
};
// The percentage of the panel to swipe between 0 and 1. This value was set through
// testing: 0.55f was tested on try and fails on armv6 devices.
@ -77,7 +80,7 @@ public class AboutHomeComponent extends BaseComponent {
public AboutHomeComponent assertCurrentPanel(final PanelType expectedPanel) {
assertVisible();
final int expectedPanelIndex = getPanelIndexForDevice(expectedPanel.ordinal());
final int expectedPanelIndex = getPanelIndexForDevice(expectedPanel);
fAssertEquals("The current HomePager panel is " + expectedPanel,
expectedPanelIndex, getHomePagerView().getCurrentItem());
return this;
@ -161,8 +164,7 @@ public class AboutHomeComponent extends BaseComponent {
// The panel on the left is a lower index and vice versa.
final int unboundedPanelIndex = panelIndex + (panelDirection == Solo.LEFT ? -1 : 1);
final int panelCount = DeviceHelper.isTablet() ?
TabletPanel.values().length : PhonePanel.values().length;
final int panelCount = getPanelOrderingForDevice().length;
final int maxPanelIndex = panelCount - 1;
final int expectedPanelIndex = Math.min(Math.max(0, unboundedPanelIndex), maxPanelIndex);
@ -170,12 +172,7 @@ public class AboutHomeComponent extends BaseComponent {
}
private void waitForPanelIndex(final int expectedIndex) {
final String panelName;
if (DeviceHelper.isTablet()) {
panelName = TabletPanel.values()[expectedIndex].name();
} else {
panelName = PhonePanel.values()[expectedIndex].name();
}
final String panelName = getPanelOrderingForDevice()[expectedIndex].name();
WaitHelper.waitFor("HomePager " + panelName + " panel", new Condition() {
@Override
@ -186,13 +183,19 @@ public class AboutHomeComponent extends BaseComponent {
}
/**
* Gets the panel index in the device specific Panel enum for the given index in the
* PanelType enum.
* Get the expected panel index for the given PanelType on this device. Different panel
* orderings are expected on tables vs. phones.
*/
private int getPanelIndexForDevice(final int panelIndex) {
final String panelName = PanelType.values()[panelIndex].name();
final Class devicePanelEnum =
DeviceHelper.isTablet() ? TabletPanel.class : PhonePanel.class;
return Enum.valueOf(devicePanelEnum, panelName).ordinal();
private int getPanelIndexForDevice(final PanelType panelType) {
PanelType[] panelOrdering = getPanelOrderingForDevice();
return Arrays.asList(panelOrdering).indexOf(panelType);
}
/**
* Get an array of PanelType objects ordered as we want the panels to be ordered on this device.
*/
public static PanelType[] getPanelOrderingForDevice() {
return HardwareUtils.isTablet() ? PANEL_ORDERING_TABLET : PANEL_ORDERING_PHONE;
}
}

View File

@ -84,9 +84,9 @@ public class testEventDispatcher extends UITest
} else if (GECKO_RESPONSE_EVENT.equals(event)) {
final String response = message.getString("response");
if ("success".equals(response)) {
EventDispatcher.getInstance().sendResponse(message, response);
EventDispatcher.sendResponse(message, response);
} else if ("error".equals(response)) {
EventDispatcher.getInstance().sendError(message, response);
EventDispatcher.sendError(message, response);
} else {
fFail("Response type should be valid: " + response);
}

View File

@ -163,13 +163,12 @@ public class testImportFromAndroid extends AboutHomeTest {
// Return bookmarks or history depending on what the user asks for
ArrayList<String> urls = new ArrayList<String>();
ContentResolver resolver = getActivity().getContentResolver();
Browser mBrowser = new Browser();
Cursor cursor = null;
try {
if (data.equals("history")) {
cursor = mBrowser.getAllVisitedUrls(resolver);
cursor = Browser.getAllVisitedUrls(resolver);
} else if (data.equals("bookmarks")) {
cursor = mBrowser.getAllBookmarks(resolver);
cursor = Browser.getAllBookmarks(resolver);
}
if (cursor != null) {
cursor.moveToFirst();

View File

@ -34,25 +34,25 @@ public class testJavascriptBridge extends UITest {
GeckoHelper.blockForReady();
NavigationHelper.enterAndLoadUrl(StringHelper.ROBOCOP_JS_HARNESS_URL +
"?path=" + TEST_JS);
js.syncCall("check_js_int_arg", (int) 1);
js.syncCall("check_js_int_arg", 1);
}
public void checkJavaIntArg(final int int2) {
// Async call from JS
fAssertEquals("Integer argument matches", 2, int2);
js.syncCall("check_js_double_arg", (double) 3.0);
js.syncCall("check_js_double_arg", 3.0D);
}
public void checkJavaDoubleArg(final double double4) {
// Async call from JS
fAssertEquals("Double argument matches", 4.0, double4);
js.syncCall("check_js_boolean_arg", (boolean) false);
js.syncCall("check_js_boolean_arg", false);
}
public void checkJavaBooleanArg(final boolean booltrue) {
// Async call from JS
fAssertEquals("Boolean argument matches", true, booltrue);
js.syncCall("check_js_string_arg", (String) "foo");
js.syncCall("check_js_string_arg", "foo");
}
public void checkJavaStringArg(final String stringbar) throws JSONException {

View File

@ -156,8 +156,8 @@ public class testShareLink extends AboutHomeTest {
}
// Create a SEND intent and get the possible activities offered
public ArrayList getShareOptions() {
ArrayList<String> shareOptions = new ArrayList();
public ArrayList<String> getShareOptions() {
ArrayList<String> shareOptions = new ArrayList<>();
Activity currentActivity = getActivity();
final Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, url);
@ -209,14 +209,14 @@ public class testShareLink extends AboutHomeTest {
}
public ArrayList<String> getSharePopupOption() {
ArrayList<String> displayedOptions = new ArrayList();
ArrayList<String> displayedOptions = new ArrayList<>();
AbsListView shareMenu = getDisplayedShareList();
getGroupTextViews(shareMenu, displayedOptions);
return displayedOptions;
}
public ArrayList<String> getShareSubMenuOption() {
ArrayList<String> displayedOptions = new ArrayList();
ArrayList<String> displayedOptions = new ArrayList<>();
AbsListView shareMenu = getDisplayedShareList();
getGroupTextViews(shareMenu, displayedOptions);
return displayedOptions;

View File

@ -138,4 +138,9 @@ class BrowserToolbarNewTablet extends BrowserToolbarTabletBase {
urlDisplayLayout.prepareForwardAnimation(anim, animation, width);
}
@Override
public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
// Do nothing.
}
}

View File

@ -19,6 +19,8 @@ import android.content.res.Resources;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.RelativeLayout;
/**
@ -37,6 +39,8 @@ class BrowserToolbarTablet extends BrowserToolbarTabletBase {
private final int urlBarViewOffset;
private final int defaultForwardMargin;
private final Interpolator buttonsInterpolator = new AccelerateInterpolator();
public BrowserToolbarTablet(final Context context, final AttributeSet attrs) {
super(context, attrs);
@ -257,4 +261,21 @@ class BrowserToolbarTablet extends BrowserToolbarTabletBase {
urlDisplayLayout.prepareForwardAnimation(anim, animation, width);
}
@Override
public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
if (areTabsShown) {
ViewHelper.setAlpha(tabsCounter, 0.0f);
return;
}
final PropertyAnimator buttonsAnimator =
new PropertyAnimator(animator.getDuration(), buttonsInterpolator);
buttonsAnimator.attach(tabsCounter,
PropertyAnimator.Property.ALPHA,
1.0f);
buttonsAnimator.start();
}
}

View File

@ -10,15 +10,11 @@ import java.util.Arrays;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
@ -39,8 +35,6 @@ abstract class BrowserToolbarTabletBase extends BrowserToolbar {
protected final BackButton backButton;
protected final ForwardButton forwardButton;
private final Interpolator buttonsInterpolator = new AccelerateInterpolator();
protected abstract void animateForwardButton(ForwardButtonAnimation animation);
public BrowserToolbarTabletBase(final Context context, final AttributeSet attrs) {
@ -131,23 +125,6 @@ abstract class BrowserToolbarTabletBase extends BrowserToolbar {
forwardButton.setPrivateMode(isPrivate);
}
@Override
public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
if (areTabsShown) {
ViewHelper.setAlpha(tabsCounter, 0.0f);
return;
}
final PropertyAnimator buttonsAnimator =
new PropertyAnimator(animator.getDuration(), buttonsInterpolator);
buttonsAnimator.attach(tabsCounter,
PropertyAnimator.Property.ALPHA,
1.0f);
buttonsAnimator.start();
}
protected boolean canDoBack(final Tab tab) {
return (tab.canDoBack() && !isEditing());
}

View File

@ -398,8 +398,6 @@ this.LightweightThemeManager = {
function AddonWrapper(aTheme) {
this.__defineGetter__("id", function AddonWrapper_idGetter() aTheme.id + ID_SUFFIX);
this.__defineGetter__("type", function AddonWrapper_typeGetter() ADDON_TYPE);
this.__defineGetter__("headerURL", function AddonWrapper_headerURLGetter() aTheme.headerURL);
this.__defineGetter__("footerURL", function AddonWrapper_footerURLGetter() aTheme.footerURL);
this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
let current = LightweightThemeManager.currentTheme;
if (current)

View File

@ -75,19 +75,21 @@ function part3(aTestPlugin) {
}
function part4() {
ok(PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser), "part4: should have a click-to-play notification");
gPluginBrowser.removeEventListener("PluginBindingAttached", part4);
gBrowser.removeCurrentTab();
let condition = () => PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser);
waitForCondition(condition, () => {
gPluginBrowser.removeEventListener("PluginBindingAttached", part4);
gBrowser.removeCurrentTab();
let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
let menu = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "state-menulist");
let alwaysActivateItem = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "always-activate-menuitem");
menu.selectedItem = alwaysActivateItem;
alwaysActivateItem.doCommand();
gBrowser.selectedTab = gBrowser.addTab();
gPluginBrowser = gBrowser.selectedBrowser;
gPluginBrowser.addEventListener("load", part5, true);
gPluginBrowser.contentWindow.location = gHttpTestRoot + "plugin_test.html";
let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
let menu = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "state-menulist");
let alwaysActivateItem = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "always-activate-menuitem");
menu.selectedItem = alwaysActivateItem;
alwaysActivateItem.doCommand();
gBrowser.selectedTab = gBrowser.addTab();
gPluginBrowser = gBrowser.selectedBrowser;
gPluginBrowser.addEventListener("load", part5, true);
gPluginBrowser.contentWindow.location = gHttpTestRoot + "plugin_test.html";
}, "part4: should have a click-to-play notification");
}
function part5() {
@ -118,20 +120,22 @@ function part6() {
}
function part7() {
ok(PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser), "part7: disabled plugins still show a notification");
let testPlugin = gPluginBrowser.contentDocument.getElementById("test");
ok(testPlugin, "part7: should have a plugin element in the page");
let objLoadingContent = testPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
ok(!objLoadingContent.activated, "part7: plugin should not be activated");
let condition = () => PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser);
waitForCondition(condition, () => {
let testPlugin = gPluginBrowser.contentDocument.getElementById("test");
ok(testPlugin, "part7: should have a plugin element in the page");
let objLoadingContent = testPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
ok(!objLoadingContent.activated, "part7: plugin should not be activated");
gPluginBrowser.removeEventListener("PluginBindingAttached", part7);
gBrowser.removeCurrentTab();
gPluginBrowser.removeEventListener("PluginBindingAttached", part7);
gBrowser.removeCurrentTab();
let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
let details = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "details-btn");
is_element_visible(details, "part7: details link should be visible");
EventUtils.synthesizeMouseAtCenter(details, {}, gManagerWindow);
wait_for_view_load(gManagerWindow, part8);
let pluginEl = get_addon_element(gManagerWindow, gTestPluginId);
let details = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "details-btn");
is_element_visible(details, "part7: details link should be visible");
EventUtils.synthesizeMouseAtCenter(details, {}, gManagerWindow);
wait_for_view_load(gManagerWindow, part8);
}, "part7: disabled plugins still show a notification");
}
function part8() {
@ -180,19 +184,21 @@ function part10() {
}
function part11() {
ok(PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser), "part11: should have a click-to-play notification");
gPluginBrowser.removeEventListener("PluginBindingAttached", part11);
gBrowser.removeCurrentTab();
let condition = () => PopupNotifications.getNotification("click-to-play-plugins", gPluginBrowser);
waitForCondition(condition, () => {
gPluginBrowser.removeEventListener("PluginBindingAttached", part11);
gBrowser.removeCurrentTab();
let pluginTag = getTestPluginTag();
let pluginTag = getTestPluginTag();
// causes appDisabled to be set
setAndUpdateBlocklist(gHttpTestRoot + "blockPluginHard.xml",
function() {
close_manager(gManagerWindow, function() {
open_manager("addons://list/plugin", part12);
// causes appDisabled to be set
setAndUpdateBlocklist(gHttpTestRoot + "blockPluginHard.xml",
function() {
close_manager(gManagerWindow, function() {
open_manager("addons://list/plugin", part12);
});
});
});
}, "part11: should have a click-to-play notification");
}
function part12(aWindow) {