зеркало из https://github.com/mozilla/gecko-dev.git
Merge m-c to mozilla-inbound
This commit is contained in:
Коммит
cb2752ed21
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="4ace9aaee0e048dfda11bb787646c59982a3dc80"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="31a7849fe9a8b743d6f5e5facc212f0ef9d57499"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="0c28789b9957913be975eb002a22323f93585d4c"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="4ace9aaee0e048dfda11bb787646c59982a3dc80"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"git": {
|
||||
"git_revision": "a87f947366c2e044bd6336e1982419ac45378969",
|
||||
"git_revision": "3491a49da4d4ce45b8a682e95d0ce68892c2b8ea",
|
||||
"remote": "https://git.mozilla.org/releases/gaia.git",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "6878d4020e3c1a3b020031cce83c4b437ad42669",
|
||||
"revision": "6458b8861154bf7e28971ce85d49e404ac96da29",
|
||||
"repo_path": "integration/gaia-central"
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="31a7849fe9a8b743d6f5e5facc212f0ef9d57499"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="0c28789b9957913be975eb002a22323f93585d4c"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="a87f947366c2e044bd6336e1982419ac45378969"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3491a49da4d4ce45b8a682e95d0ce68892c2b8ea"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c1bbb66f52f9e2d76ce97e7b3aa0cb29957cd7d8"/>
|
||||
|
|
|
@ -257,6 +257,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent",
|
||||
"resource:///modules/ReaderParent.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
|
||||
"resource://gre/modules/LoginManagerParent.jsm");
|
||||
|
||||
var gInitialPages = [
|
||||
"about:blank",
|
||||
"about:newtab",
|
||||
|
@ -1194,6 +1197,10 @@ var gBrowserInit = {
|
|||
}
|
||||
}, false, true);
|
||||
|
||||
gBrowser.addEventListener("InsecureLoginFormsStateChange", function() {
|
||||
gIdentityHandler.refreshForInsecureLoginForms();
|
||||
});
|
||||
|
||||
let uriToLoad = this._getUriToLoad();
|
||||
if (uriToLoad && uriToLoad != "about:blank") {
|
||||
if (uriToLoad instanceof Ci.nsISupportsArray) {
|
||||
|
@ -7056,9 +7063,7 @@ var gIdentityHandler = {
|
|||
}
|
||||
|
||||
// Then, update the user interface with the available data.
|
||||
if (this._identityBox) {
|
||||
this.refreshIdentityBlock();
|
||||
}
|
||||
this.refreshIdentityBlock();
|
||||
// Handle a location change while the Control Center is focused
|
||||
// by closing the popup (bug 1207542)
|
||||
if (shouldHidePopup) {
|
||||
|
@ -7071,6 +7076,20 @@ var gIdentityHandler = {
|
|||
// information we don't want to suddenly change the panel contents.
|
||||
},
|
||||
|
||||
/**
|
||||
* This is called asynchronously when requested by the Logins module, after
|
||||
* the insecure login forms state for the page has been updated.
|
||||
*/
|
||||
refreshForInsecureLoginForms() {
|
||||
// Check this._uri because we don't want to refresh the user interface if
|
||||
// this is called before the first page load in the window for any reason.
|
||||
if (!this._uri) {
|
||||
Cu.reportError("Unexpected early call to refreshForInsecureLoginForms.");
|
||||
return;
|
||||
}
|
||||
this.refreshIdentityBlock();
|
||||
},
|
||||
|
||||
/**
|
||||
* Attempt to provide proper IDN treatment for host names
|
||||
*/
|
||||
|
@ -7107,6 +7126,10 @@ var gIdentityHandler = {
|
|||
* Updates the identity block user interface with the data from this object.
|
||||
*/
|
||||
refreshIdentityBlock() {
|
||||
if (!this._identityBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
let icon_label = "";
|
||||
let tooltip = "";
|
||||
let icon_country_label = "";
|
||||
|
@ -7175,6 +7198,11 @@ var gIdentityHandler = {
|
|||
this._identityBox.classList.add("weakCipher");
|
||||
}
|
||||
}
|
||||
if (LoginManagerParent.hasInsecureLoginForms(gBrowser.selectedBrowser)) {
|
||||
// Insecure login forms can only be present on "unknown identity"
|
||||
// pages, either already insecure or with mixed active content loaded.
|
||||
this._identityBox.classList.add("insecureLoginForms");
|
||||
}
|
||||
tooltip = gNavigatorBundle.getString("identity.unknown.tooltip");
|
||||
}
|
||||
|
||||
|
@ -7212,6 +7240,12 @@ var gIdentityHandler = {
|
|||
connection = "secure";
|
||||
}
|
||||
|
||||
// Determine if there are insecure login forms.
|
||||
let loginforms = "secure";
|
||||
if (LoginManagerParent.hasInsecureLoginForms(gBrowser.selectedBrowser)) {
|
||||
loginforms = "insecure";
|
||||
}
|
||||
|
||||
// Determine the mixed content state.
|
||||
let mixedcontent = [];
|
||||
if (this._isMixedPassiveContentLoaded) {
|
||||
|
@ -7249,6 +7283,7 @@ var gIdentityHandler = {
|
|||
for (let id of elementIDs) {
|
||||
let element = document.getElementById(id);
|
||||
updateAttribute(element, "connection", connection);
|
||||
updateAttribute(element, "loginforms", loginforms);
|
||||
updateAttribute(element, "ciphers", ciphers);
|
||||
updateAttribute(element, "mixedcontent", mixedcontent);
|
||||
updateAttribute(element, "isbroken", this._isBroken);
|
||||
|
|
|
@ -268,7 +268,7 @@ tags = mcb
|
|||
tags = mcb
|
||||
[browser_bug906190.js]
|
||||
tags = mcb
|
||||
skip-if = buildapp == "mulet" || e10s # Bug 1093642 - test manipulates content and relies on content focus
|
||||
skip-if = buildapp == "mulet" || e10s || os == "linux" # Bug 1093642 - test manipulates content and relies on content focus, Bug 1212520 - Re-enable on Linux
|
||||
[browser_mixedContentFromOnunload.js]
|
||||
tags = mcb
|
||||
[browser_mixedContentFramesOnHttp.js]
|
||||
|
@ -322,6 +322,7 @@ skip-if = e10s # Bug 863514 - no gesture support.
|
|||
[browser_homeDrop.js]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[browser_identity_UI.js]
|
||||
[browser_insecureLoginForms.js]
|
||||
[browser_keywordBookmarklets.js]
|
||||
skip-if = e10s # Bug 1102025 - different principals for the bookmarklet only in e10s mode (unclear if test or 'real' issue)
|
||||
[browser_keywordSearch.js]
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Load directly from the browser-chrome support files of login tests.
|
||||
const testUrlPath =
|
||||
"://example.com/browser/toolkit/components/passwordmgr/test/browser/";
|
||||
|
||||
/**
|
||||
* Waits for the given number of occurrences of InsecureLoginFormsStateChange
|
||||
* on the given browser element.
|
||||
*/
|
||||
function waitForInsecureLoginFormsStateChange(browser, count) {
|
||||
return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
|
||||
false, () => --count == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the insecure login forms logic for the identity block.
|
||||
*/
|
||||
add_task(function* test_simple() {
|
||||
for (let scheme of ["http", "https"]) {
|
||||
let tab = gBrowser.addTab(scheme + testUrlPath + "form_basic.html");
|
||||
let browser = tab.linkedBrowser;
|
||||
yield Promise.all([
|
||||
BrowserTestUtils.switchTab(gBrowser, tab),
|
||||
BrowserTestUtils.browserLoaded(browser),
|
||||
// One event is triggered by pageshow and one by DOMFormHasPassword.
|
||||
waitForInsecureLoginFormsStateChange(browser, 2),
|
||||
]);
|
||||
|
||||
let { gIdentityHandler } = gBrowser.ownerGlobal;
|
||||
gIdentityHandler._identityBox.click();
|
||||
document.getElementById("identity-popup-security-expander").click();
|
||||
|
||||
if (scheme == "http") {
|
||||
let identityBoxImage = gBrowser.ownerGlobal
|
||||
.getComputedStyle(document.getElementById("page-proxy-favicon"), "")
|
||||
.getPropertyValue("list-style-image");
|
||||
let securityViewBG = gBrowser.ownerGlobal
|
||||
.getComputedStyle(document.getElementById("identity-popup-securityView"), "")
|
||||
.getPropertyValue("background-image");
|
||||
let securityContentBG = gBrowser.ownerGlobal
|
||||
.getComputedStyle(document.getElementById("identity-popup-security-content"), "")
|
||||
.getPropertyValue("background-image");
|
||||
is(identityBoxImage,
|
||||
"url(\"chrome://browser/skin/identity-mixed-active-loaded.svg\")",
|
||||
"Using expected icon image in the identity block");
|
||||
is(securityViewBG,
|
||||
"url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
|
||||
"Using expected icon image in the Control Center main view");
|
||||
is(securityContentBG,
|
||||
"url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
|
||||
"Using expected icon image in the Control Center subview");
|
||||
}
|
||||
|
||||
// Messages should be visible when the scheme is HTTP, and invisible when
|
||||
// the scheme is HTTPS.
|
||||
is(Array.every(document.querySelectorAll("[when-loginforms=insecure]"),
|
||||
element => !is_hidden(element)),
|
||||
scheme == "http",
|
||||
"The relevant messages should visible or hidden.");
|
||||
|
||||
gIdentityHandler._identityPopup.hidden = true;
|
||||
gBrowser.removeTab(tab);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that the insecure login forms logic does not regress mixed content
|
||||
* blocking messages when mixed active content is loaded.
|
||||
*/
|
||||
add_task(function* test_mixedcontent() {
|
||||
yield new Promise(resolve => SpecialPowers.pushPrefEnv({
|
||||
"set": [["security.mixed_content.block_active_content", false]],
|
||||
}, resolve));
|
||||
|
||||
// Load the page with the subframe in a new tab.
|
||||
let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html");
|
||||
let browser = tab.linkedBrowser;
|
||||
yield Promise.all([
|
||||
BrowserTestUtils.switchTab(gBrowser, tab),
|
||||
BrowserTestUtils.browserLoaded(browser),
|
||||
// Two events are triggered by pageshow and one by DOMFormHasPassword.
|
||||
waitForInsecureLoginFormsStateChange(browser, 3),
|
||||
]);
|
||||
|
||||
assertMixedContentBlockingState(browser, { activeLoaded: true,
|
||||
activeBlocked: false,
|
||||
passiveLoaded: false });
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
|
@ -890,6 +890,13 @@ function assertMixedContentBlockingState(tabbrowser, states = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
if (activeLoaded || activeBlocked || passiveLoaded) {
|
||||
doc.getElementById("identity-popup-security-expander").click();
|
||||
is(Array.filter(doc.querySelectorAll("[observes=identity-popup-mcb-learn-more]"),
|
||||
element => !is_hidden(element)).length, 1,
|
||||
"The 'Learn more' link should be visible once.");
|
||||
}
|
||||
|
||||
gIdentityHandler._identityPopup.hidden = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
<description when-mixedcontent="active-loaded">&identity.activeLoaded;</description>
|
||||
<description class="identity-popup-warning-yellow"
|
||||
when-ciphers="weak">&identity.weakEncryption;</description>
|
||||
<description when-loginforms="insecure">&identity.insecureLoginForms;</description>
|
||||
</vbox>
|
||||
</vbox>
|
||||
<button id="identity-popup-security-expander"
|
||||
|
@ -116,7 +117,11 @@
|
|||
when-connection="secure secure-ev"/>
|
||||
|
||||
<!-- Connection is Not Secure -->
|
||||
<description when-connection="not-secure">&identity.description.insecure;</description>
|
||||
<description when-connection="not-secure"
|
||||
and-when-loginforms="secure">&identity.description.insecure;</description>
|
||||
|
||||
<!-- Insecure login forms -->
|
||||
<description when-loginforms="insecure">&identity.description.insecureLoginForms;</description>
|
||||
|
||||
<!-- Weak Cipher -->
|
||||
<description when-ciphers="weak">&identity.description.weakCipher;</description>
|
||||
|
@ -138,8 +143,14 @@
|
|||
class="identity-popup-warning-yellow">&identity.description.passiveLoaded3; <label observes="identity-popup-mcb-learn-more"/></description>
|
||||
|
||||
<!-- Active Mixed Content Blocking Disabled -->
|
||||
<description when-mixedcontent="active-loaded">&identity.description.activeLoaded;</description>
|
||||
<description when-mixedcontent="active-loaded">&identity.description.activeLoaded2; <label observes="identity-popup-mcb-learn-more"/></description>
|
||||
<description when-mixedcontent="active-loaded"
|
||||
and-when-loginforms="secure">&identity.description.activeLoaded;</description>
|
||||
<description when-mixedcontent="active-loaded"
|
||||
and-when-loginforms="secure">&identity.description.activeLoaded2; <label observes="identity-popup-mcb-learn-more"/></description>
|
||||
<!-- Show only the first message when there are insecure login forms,
|
||||
and make sure the Learn More link is included. -->
|
||||
<description when-mixedcontent="active-loaded"
|
||||
and-when-loginforms="insecure">&identity.description.activeLoaded; <label observes="identity-popup-mcb-learn-more"/></description>
|
||||
|
||||
<!-- Buttons to enable/disable mixed content blocking. -->
|
||||
<button when-mixedcontent="active-blocked"
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
|
||||
"resource:///modules/CustomizableUI.jsm");
|
||||
|
||||
|
@ -14,22 +18,11 @@ var {
|
|||
// WeakMap[Extension -> BrowserAction]
|
||||
var browserActionMap = new WeakMap();
|
||||
|
||||
// WeakMap[Extension -> docshell]
|
||||
// This map is a cache of the windowless browser that's used to render ImageData
|
||||
// for the browser_action icon.
|
||||
var imageRendererMap = new WeakMap();
|
||||
|
||||
function browserActionOf(extension)
|
||||
{
|
||||
return browserActionMap.get(extension);
|
||||
}
|
||||
|
||||
function makeWidgetId(id)
|
||||
{
|
||||
id = id.toLowerCase();
|
||||
return id.replace(/[^a-z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
var nextActionId = 0;
|
||||
|
||||
// Responsible for the browser_action section of the manifest as well
|
||||
|
@ -40,13 +33,25 @@ function BrowserAction(options, extension)
|
|||
this.id = makeWidgetId(extension.id) + "-browser-action";
|
||||
this.widget = null;
|
||||
|
||||
this.title = new DefaultWeakMap(extension.localize(options.default_title));
|
||||
this.badgeText = new DefaultWeakMap();
|
||||
this.badgeBackgroundColor = new DefaultWeakMap();
|
||||
this.icon = new DefaultWeakMap(options.default_icon);
|
||||
this.popup = new DefaultWeakMap(options.default_popup);
|
||||
let title = extension.localize(options.default_title || "");
|
||||
let popup = extension.localize(options.default_popup || "");
|
||||
if (popup) {
|
||||
popup = extension.baseURI.resolve(popup);
|
||||
}
|
||||
|
||||
this.context = null;
|
||||
this.defaults = {
|
||||
title: title,
|
||||
badgeText: "",
|
||||
badgeBackgroundColor: null,
|
||||
icon: IconDetails.normalize({ path: options.default_icon }, extension,
|
||||
null, true),
|
||||
popup: popup,
|
||||
};
|
||||
|
||||
this.tabContext = new TabContext(tab => Object.create(this.defaults),
|
||||
extension);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
BrowserAction.prototype = {
|
||||
|
@ -62,10 +67,9 @@ BrowserAction.prototype = {
|
|||
node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional badged-button");
|
||||
node.setAttribute("constrain-size", "true");
|
||||
|
||||
this.updateTab(null, node);
|
||||
this.updateButton(node, this.defaults);
|
||||
|
||||
let tabbrowser = document.defaultView.gBrowser;
|
||||
tabbrowser.tabContainer.addEventListener("TabSelect", this);
|
||||
|
||||
node.addEventListener("command", event => {
|
||||
let tab = tabbrowser.selectedTab;
|
||||
|
@ -80,155 +84,58 @@ BrowserAction.prototype = {
|
|||
return node;
|
||||
},
|
||||
});
|
||||
|
||||
this.tabContext.on("tab-select",
|
||||
(evt, tab) => { this.updateWindow(tab.ownerDocument.defaultView); })
|
||||
|
||||
this.widget = widget;
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
if (event.type == "TabSelect") {
|
||||
let window = event.target.ownerDocument.defaultView;
|
||||
let tabbrowser = window.gBrowser;
|
||||
let instance = CustomizableUI.getWidget(this.id).forWindow(window);
|
||||
if (instance) {
|
||||
this.updateTab(tabbrowser.selectedTab, instance.node);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
togglePopup(node, popupResource) {
|
||||
let popupURL = this.extension.baseURI.resolve(popupResource);
|
||||
|
||||
let document = node.ownerDocument;
|
||||
let panel = document.createElement("panel");
|
||||
panel.setAttribute("class", "browser-action-panel");
|
||||
panel.setAttribute("type", "arrow");
|
||||
panel.setAttribute("flip", "slide");
|
||||
node.appendChild(panel);
|
||||
|
||||
panel.addEventListener("popuphidden", () => {
|
||||
this.context.unload();
|
||||
this.context = null;
|
||||
panel.remove();
|
||||
});
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
let browser = document.createElementNS(XUL_NS, "browser");
|
||||
browser.setAttribute("type", "content");
|
||||
browser.setAttribute("disableglobalhistory", "true");
|
||||
panel.appendChild(browser);
|
||||
|
||||
let loadListener = () => {
|
||||
panel.removeEventListener("load", loadListener);
|
||||
|
||||
this.context = new ExtensionPage(this.extension, {
|
||||
type: "popup",
|
||||
contentWindow: browser.contentWindow,
|
||||
uri: Services.io.newURI(popupURL, null, null),
|
||||
docShell: browser.docShell,
|
||||
});
|
||||
GlobalManager.injectInDocShell(browser.docShell, this.extension, this.context);
|
||||
browser.setAttribute("src", popupURL);
|
||||
|
||||
let contentLoadListener = () => {
|
||||
browser.removeEventListener("load", contentLoadListener);
|
||||
|
||||
let contentViewer = browser.docShell.contentViewer;
|
||||
let width = {}, height = {};
|
||||
try {
|
||||
contentViewer.getContentSize(width, height);
|
||||
[width, height] = [width.value, height.value];
|
||||
} catch (e) {
|
||||
// getContentSize can throw
|
||||
[width, height] = [400, 400];
|
||||
}
|
||||
|
||||
let window = document.defaultView;
|
||||
width /= window.devicePixelRatio;
|
||||
height /= window.devicePixelRatio;
|
||||
width = Math.min(width, 800);
|
||||
height = Math.min(height, 800);
|
||||
|
||||
browser.setAttribute("width", width);
|
||||
browser.setAttribute("height", height);
|
||||
|
||||
let anchor = document.getAnonymousElementByAttribute(node, "class", "toolbarbutton-icon");
|
||||
panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
|
||||
};
|
||||
browser.addEventListener("load", contentLoadListener, true);
|
||||
};
|
||||
panel.addEventListener("load", loadListener);
|
||||
openPanel(node, popupResource, this.extension);
|
||||
},
|
||||
|
||||
// Initialize the toolbar icon and popup given that |tab| is the
|
||||
// current tab and |node| is the CustomizableUI node. Note: |tab|
|
||||
// will be null if we don't know the current tab yet (during
|
||||
// initialization).
|
||||
updateTab(tab, node) {
|
||||
let window = node.ownerDocument.defaultView;
|
||||
|
||||
let title = this.getProperty(tab, "title");
|
||||
if (title) {
|
||||
node.setAttribute("tooltiptext", title);
|
||||
node.setAttribute("label", title);
|
||||
// Update the toolbar button |node| with the tab context data
|
||||
// in |tabData|.
|
||||
updateButton(node, tabData) {
|
||||
if (tabData.title) {
|
||||
node.setAttribute("tooltiptext", tabData.title);
|
||||
node.setAttribute("label", tabData.title);
|
||||
node.setAttribute("aria-label", tabData.title);
|
||||
} else {
|
||||
node.removeAttribute("tooltiptext");
|
||||
node.removeAttribute("label");
|
||||
node.removeAttribute("aria-label");
|
||||
}
|
||||
|
||||
let badgeText = this.badgeText.get(tab);
|
||||
if (badgeText) {
|
||||
node.setAttribute("badge", badgeText);
|
||||
if (tabData.badgeText) {
|
||||
node.setAttribute("badge", tabData.badgeText);
|
||||
} else {
|
||||
node.removeAttribute("badge");
|
||||
}
|
||||
|
||||
function toHex(n) {
|
||||
return Math.floor(n / 16).toString(16) + (n % 16).toString(16);
|
||||
}
|
||||
|
||||
let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
|
||||
'class', 'toolbarbutton-badge');
|
||||
if (badgeNode) {
|
||||
let color = this.badgeBackgroundColor.get(tab);
|
||||
let color = tabData.badgeBackgroundColor;
|
||||
if (Array.isArray(color)) {
|
||||
color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
||||
}
|
||||
badgeNode.style.backgroundColor = color;
|
||||
badgeNode.style.backgroundColor = color || "";
|
||||
}
|
||||
|
||||
let iconURL = this.getIcon(tab, node);
|
||||
let iconURL = IconDetails.getURL(
|
||||
tabData.icon, node.ownerDocument.defaultView, this.extension);
|
||||
node.setAttribute("image", iconURL);
|
||||
},
|
||||
|
||||
// Note: tab is allowed to be null here.
|
||||
getIcon(tab, node) {
|
||||
let icon = this.icon.get(tab);
|
||||
|
||||
let url;
|
||||
if (typeof(icon) != "object") {
|
||||
url = icon;
|
||||
} else {
|
||||
let window = node.ownerDocument.defaultView;
|
||||
let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindowUtils);
|
||||
let res = {value: 1}
|
||||
utils.getResolution(res);
|
||||
|
||||
let size = res.value == 1 ? 19 : 38;
|
||||
url = icon[size];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
return this.extension.baseURI.resolve(url);
|
||||
} else {
|
||||
return "chrome://browser/content/extension.svg";
|
||||
}
|
||||
},
|
||||
|
||||
// Update the toolbar button for a given window.
|
||||
updateWindow(window) {
|
||||
let tab = window.gBrowser ? window.gBrowser.selectedTab : null;
|
||||
let node = CustomizableUI.getWidget(this.id).forWindow(window).node;
|
||||
this.updateTab(tab, node);
|
||||
let widget = this.widget.forWindow(window);
|
||||
if (widget) {
|
||||
let tab = window.gBrowser.selectedTab;
|
||||
this.updateButton(widget.node, this.tabContext.get(tab));
|
||||
}
|
||||
},
|
||||
|
||||
// Update the toolbar button when the extension changes the icon,
|
||||
|
@ -240,12 +147,8 @@ BrowserAction.prototype = {
|
|||
this.updateWindow(tab.ownerDocument.defaultView);
|
||||
}
|
||||
} else {
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
if (window.gBrowser) {
|
||||
this.updateWindow(window);
|
||||
}
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
this.updateWindow(window);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -253,30 +156,31 @@ BrowserAction.prototype = {
|
|||
// tab is allowed to be null.
|
||||
// prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
|
||||
setProperty(tab, prop, value) {
|
||||
this[prop].set(tab, value);
|
||||
if (tab == null) {
|
||||
this.defaults[prop] = value;
|
||||
} else {
|
||||
this.tabContext.get(tab)[prop] = value;
|
||||
}
|
||||
|
||||
this.updateOnChange(tab);
|
||||
},
|
||||
|
||||
// tab is allowed to be null.
|
||||
// prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
|
||||
getProperty(tab, prop) {
|
||||
return this[prop].get(tab);
|
||||
if (tab == null) {
|
||||
return this.defaults[prop];
|
||||
} else {
|
||||
return this.tabContext.get(tab)[prop];
|
||||
}
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
let widget = CustomizableUI.getWidget(this.id);
|
||||
for (let instance of widget.instances) {
|
||||
let window = instance.node.ownerDocument.defaultView;
|
||||
let tabbrowser = window.gBrowser;
|
||||
tabbrowser.tabContainer.removeEventListener("TabSelect", this);
|
||||
}
|
||||
|
||||
this.tabContext.shutdown();
|
||||
CustomizableUI.destroyWidget(this.id);
|
||||
},
|
||||
};
|
||||
|
||||
EventEmitter.decorate(BrowserAction.prototype);
|
||||
|
||||
extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
|
||||
let browserAction = new BrowserAction(manifest.browser_action, extension);
|
||||
browserAction.build();
|
||||
|
@ -288,37 +192,8 @@ extensions.on("shutdown", (type, extension) => {
|
|||
browserActionMap.get(extension).shutdown();
|
||||
browserActionMap.delete(extension);
|
||||
}
|
||||
imageRendererMap.delete(extension);
|
||||
});
|
||||
|
||||
function convertImageDataToPNG(extension, imageData)
|
||||
{
|
||||
let webNav = imageRendererMap.get(extension);
|
||||
if (!webNav) {
|
||||
webNav = Services.appShell.createWindowlessBrowser(false);
|
||||
let principal = Services.scriptSecurityManager.createCodebasePrincipal(extension.baseURI,
|
||||
{addonId: extension.id});
|
||||
let interfaceRequestor = webNav.QueryInterface(Ci.nsIInterfaceRequestor);
|
||||
let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
|
||||
|
||||
GlobalManager.injectInDocShell(docShell, extension, null);
|
||||
|
||||
docShell.createAboutBlankContentViewer(principal);
|
||||
}
|
||||
|
||||
let document = webNav.document;
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
canvas.getContext("2d").putImageData(imageData, 0, 0);
|
||||
|
||||
let url = canvas.toDataURL("image/png");
|
||||
|
||||
canvas.remove();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
browserAction: {
|
||||
|
@ -346,12 +221,8 @@ extensions.registerAPI((extension, context) => {
|
|||
|
||||
setIcon: function(details, callback) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
if (details.imageData) {
|
||||
let url = convertImageDataToPNG(extension, details.imageData);
|
||||
browserActionOf(extension).setProperty(tab, "icon", url);
|
||||
} else {
|
||||
browserActionOf(extension).setProperty(tab, "icon", details.path);
|
||||
}
|
||||
let icon = IconDetails.normalize(details, extension, context);
|
||||
browserActionOf(extension).setProperty(tab, "icon", icon);
|
||||
},
|
||||
|
||||
setBadgeText: function(details) {
|
||||
|
@ -367,7 +238,13 @@ extensions.registerAPI((extension, context) => {
|
|||
|
||||
setPopup: function(details) {
|
||||
let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
|
||||
browserActionOf(extension).setProperty(tab, "popup", details.popup);
|
||||
// Note: Chrome resolves arguments to setIcon relative to the calling
|
||||
// context, but resolves arguments to setPopup relative to the extension
|
||||
// root.
|
||||
// For internal consistency, we currently resolve both relative to the
|
||||
// calling context.
|
||||
let url = details.popup && context.uri.resolve(details.popup);
|
||||
browserActionOf(extension).setProperty(tab, "popup", url);
|
||||
},
|
||||
|
||||
getPopup: function(details, callback) {
|
||||
|
|
|
@ -0,0 +1,246 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
var {
|
||||
EventManager,
|
||||
DefaultWeakMap,
|
||||
ignoreEvent,
|
||||
runSafe,
|
||||
} = ExtensionUtils;
|
||||
|
||||
// WeakMap[Extension -> PageAction]
|
||||
var pageActionMap = new WeakMap();
|
||||
|
||||
|
||||
// Handles URL bar icons, including the |page_action| manifest entry
|
||||
// and associated API.
|
||||
function PageAction(options, extension)
|
||||
{
|
||||
this.extension = extension;
|
||||
this.id = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
let title = extension.localize(options.default_title || "");
|
||||
let popup = extension.localize(options.default_popup || "");
|
||||
if (popup) {
|
||||
popup = extension.baseURI.resolve(popup);
|
||||
}
|
||||
|
||||
this.defaults = {
|
||||
show: false,
|
||||
title: title,
|
||||
icon: IconDetails.normalize({ path: options.default_icon }, extension,
|
||||
null, true),
|
||||
popup: popup && extension.baseURI.resolve(popup),
|
||||
};
|
||||
|
||||
this.tabContext = new TabContext(tab => Object.create(this.defaults),
|
||||
extension);
|
||||
|
||||
this.tabContext.on("location-change", this.handleLocationChange.bind(this));
|
||||
|
||||
// WeakMap[ChromeWindow -> <xul:image>]
|
||||
this.buttons = new WeakMap();
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
PageAction.prototype = {
|
||||
// Returns the value of the property |prop| for the given tab, where
|
||||
// |prop| is one of "show", "title", "icon", "popup".
|
||||
getProperty(tab, prop) {
|
||||
return this.tabContext.get(tab)[prop];
|
||||
},
|
||||
|
||||
// Sets the value of the property |prop| for the given tab to the
|
||||
// given value, symmetrically to |getProperty|.
|
||||
//
|
||||
// If |tab| is currently selected, updates the page action button to
|
||||
// reflect the new value.
|
||||
setProperty(tab, prop, value) {
|
||||
this.tabContext.get(tab)[prop] = value;
|
||||
if (tab.selected) {
|
||||
this.updateButton(tab.ownerDocument.defaultView);
|
||||
}
|
||||
},
|
||||
|
||||
// Updates the page action button in the given window to reflect the
|
||||
// properties of the currently selected tab:
|
||||
//
|
||||
// Updates "tooltiptext" and "aria-label" to match "title" property.
|
||||
// Updates "image" to match the "icon" property.
|
||||
// Shows or hides the icon, based on the "show" property.
|
||||
updateButton(window) {
|
||||
let tabData = this.tabContext.get(window.gBrowser.selectedTab);
|
||||
|
||||
if (!(tabData.show || this.buttons.has(window))) {
|
||||
// Don't bother creating a button for a window until it actually
|
||||
// needs to be shown.
|
||||
return;
|
||||
}
|
||||
|
||||
let button = this.getButton(window);
|
||||
|
||||
if (tabData.show) {
|
||||
// Update the title and icon only if the button is visible.
|
||||
|
||||
if (tabData.title) {
|
||||
button.setAttribute("tooltiptext", tabData.title);
|
||||
button.setAttribute("aria-label", tabData.title);
|
||||
} else {
|
||||
button.removeAttribute("tooltiptext");
|
||||
button.removeAttribute("aria-label");
|
||||
}
|
||||
|
||||
let icon = IconDetails.getURL(tabData.icon, window, this.extension);
|
||||
button.setAttribute("src", icon);
|
||||
}
|
||||
|
||||
button.hidden = !tabData.show;
|
||||
},
|
||||
|
||||
// Create an |image| node and add it to the |urlbar-icons|
|
||||
// container in the given window.
|
||||
addButton(window) {
|
||||
let document = window.document;
|
||||
|
||||
let button = document.createElement("image");
|
||||
button.id = this.id;
|
||||
button.setAttribute("class", "urlbar-icon");
|
||||
|
||||
button.addEventListener("click", event => {
|
||||
if (event.button == 0) {
|
||||
this.handleClick(window);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("urlbar-icons").appendChild(button);
|
||||
|
||||
return button;
|
||||
},
|
||||
|
||||
// Returns the page action button for the given window, creating it if
|
||||
// it doesn't already exist.
|
||||
getButton(window) {
|
||||
if (!this.buttons.has(window)) {
|
||||
let button = this.addButton(window);
|
||||
this.buttons.set(window, button);
|
||||
}
|
||||
|
||||
return this.buttons.get(window);
|
||||
},
|
||||
|
||||
// Handles a click event on the page action button for the given
|
||||
// window.
|
||||
// If the page action has a |popup| property, a panel is opened to
|
||||
// that URL. Otherwise, a "click" event is emitted, and dispatched to
|
||||
// the any click listeners in the add-on.
|
||||
handleClick(window) {
|
||||
let tab = window.gBrowser.selectedTab;
|
||||
let popup = this.tabContext.get(tab).popup;
|
||||
|
||||
if (popup) {
|
||||
openPanel(this.getButton(window), popup, this.extension);
|
||||
} else {
|
||||
this.emit("click", tab);
|
||||
}
|
||||
},
|
||||
|
||||
handleLocationChange(eventType, tab, fromBrowse) {
|
||||
if (fromBrowse) {
|
||||
this.tabContext.clear(tab);
|
||||
}
|
||||
this.updateButton(tab.ownerDocument.defaultView);
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
this.tabContext.shutdown();
|
||||
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
if (this.buttons.has(window)) {
|
||||
this.buttons.get(window).remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
PageAction.for = extension => {
|
||||
return pageActionMap.get(extension);
|
||||
};
|
||||
|
||||
|
||||
extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
|
||||
let pageAction = new PageAction(manifest.page_action, extension);
|
||||
pageActionMap.set(extension, pageAction);
|
||||
});
|
||||
|
||||
extensions.on("shutdown", (type, extension) => {
|
||||
if (pageActionMap.has(extension)) {
|
||||
pageActionMap.get(extension).shutdown();
|
||||
pageActionMap.delete(extension);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
extensions.registerAPI((extension, context) => {
|
||||
return {
|
||||
pageAction: {
|
||||
onClicked: new EventManager(context, "pageAction.onClicked", fire => {
|
||||
let listener = (evt, tab) => {
|
||||
fire(TabManager.convert(extension, tab));
|
||||
};
|
||||
let pageAction = PageAction.for(extension);
|
||||
|
||||
pageAction.on("click", listener);
|
||||
return () => {
|
||||
pageAction.off("click", listener);
|
||||
};
|
||||
}).api(),
|
||||
|
||||
show(tabId) {
|
||||
let tab = TabManager.getTab(tabId);
|
||||
PageAction.for(extension).setProperty(tab, "show", true);
|
||||
},
|
||||
|
||||
hide(tabId) {
|
||||
let tab = TabManager.getTab(tabId);
|
||||
PageAction.for(extension).setProperty(tab, "show", false);
|
||||
},
|
||||
|
||||
setTitle(details) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
PageAction.for(extension).setProperty(tab, "title", details.title);
|
||||
},
|
||||
|
||||
getTitle(details, callback) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
let title = PageAction.for(extension).getProperty(tab, "title");
|
||||
runSafe(context, callback, title);
|
||||
},
|
||||
|
||||
setIcon(details, callback) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
let icon = IconDetails.normalize(details, extension, context);
|
||||
PageAction.for(extension).setProperty(tab, "icon", icon);
|
||||
},
|
||||
|
||||
setPopup(details) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
// Note: Chrome resolves arguments to setIcon relative to the calling
|
||||
// context, but resolves arguments to setPopup relative to the extension
|
||||
// root.
|
||||
// For internal consistency, we currently resolve both relative to the
|
||||
// calling context.
|
||||
let url = details.popup && context.uri.resolve(details.popup);
|
||||
PageAction.for(extension).setProperty(tab, "popup", url);
|
||||
},
|
||||
|
||||
getPopup(details, callback) {
|
||||
let tab = TabManager.getTab(details.tabId);
|
||||
let popup = PageAction.for(extension).getProperty(tab, "popup");
|
||||
runSafe(context, callback, popup);
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,3 +1,7 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
|
@ -10,6 +14,239 @@ var {
|
|||
// modules. All of the code is installed on |global|, which is a scope
|
||||
// shared among the different ext-*.js scripts.
|
||||
|
||||
|
||||
// Manages icon details for toolbar buttons in the |pageAction| and
|
||||
// |browserAction| APIs.
|
||||
global.IconDetails = {
|
||||
// Accepted icon sizes.
|
||||
SIZES: ["19", "38"],
|
||||
|
||||
// Normalizes the various acceptable input formats into an object
|
||||
// with two properties, "19" and "38", containing icon URLs.
|
||||
normalize(details, extension, context=null, localize=false) {
|
||||
let result = {};
|
||||
|
||||
if (details.imageData) {
|
||||
let imageData = details.imageData;
|
||||
|
||||
if (imageData instanceof Cu.getGlobalForObject(imageData).ImageData) {
|
||||
imageData = {"19": imageData};
|
||||
}
|
||||
|
||||
for (let size of this.SIZES) {
|
||||
if (size in imageData) {
|
||||
result[size] = this.convertImageDataToPNG(imageData[size], context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (details.path) {
|
||||
let path = details.path;
|
||||
if (typeof path != "object") {
|
||||
path = {"19": path};
|
||||
}
|
||||
|
||||
let baseURI = context ? context.uri : extension.baseURI;
|
||||
|
||||
for (let size of this.SIZES) {
|
||||
if (size in path) {
|
||||
let url = path[size];
|
||||
if (localize) {
|
||||
url = extension.localize(url);
|
||||
}
|
||||
|
||||
url = baseURI.resolve(path[size]);
|
||||
|
||||
// The Chrome documentation specifies these parameters as
|
||||
// relative paths. We currently accept absolute URLs as well,
|
||||
// which means we need to check that the extension is allowed
|
||||
// to load them.
|
||||
try {
|
||||
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
|
||||
extension.principal, url,
|
||||
Services.scriptSecurityManager.DISALLOW_SCRIPT);
|
||||
} catch (e if !context) {
|
||||
// If there's no context, it's because we're handling this
|
||||
// as a manifest directive. Log a warning rather than
|
||||
// raising an error, but don't accept the URL in any case.
|
||||
extension.manifestError(`Access to URL '${url}' denied`);
|
||||
continue;
|
||||
}
|
||||
|
||||
result[size] = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// Returns the appropriate icon URL for the given icons object and the
|
||||
// screen resolution of the given window.
|
||||
getURL(icons, window, extension) {
|
||||
const DEFAULT = "chrome://browser/content/extension.svg";
|
||||
|
||||
// Use the higher resolution image if we're doing any up-scaling
|
||||
// for high resolution monitors.
|
||||
let res = window.devicePixelRatio;
|
||||
let size = res > 1 ? "38" : "19";
|
||||
|
||||
return icons[size] || icons["19"] || icons["38"] || DEFAULT;
|
||||
},
|
||||
|
||||
convertImageDataToPNG(imageData, context) {
|
||||
let document = context.contentWindow.document;
|
||||
let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
canvas.getContext("2d").putImageData(imageData, 0, 0);
|
||||
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
};
|
||||
|
||||
global.makeWidgetId = id => {
|
||||
id = id.toLowerCase();
|
||||
// FIXME: This allows for collisions.
|
||||
return id.replace(/[^a-z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
// Open a panel anchored to the given node, containing a browser opened
|
||||
// to the given URL, owned by the given extension. If |popupURL| is not
|
||||
// an absolute URL, it is resolved relative to the given extension's
|
||||
// base URL.
|
||||
global.openPanel = (node, popupURL, extension) => {
|
||||
let document = node.ownerDocument;
|
||||
|
||||
let popupURI = Services.io.newURI(popupURL, null, extension.baseURI);
|
||||
|
||||
Services.scriptSecurityManager.checkLoadURIWithPrincipal(
|
||||
extension.principal, popupURI,
|
||||
Services.scriptSecurityManager.DISALLOW_SCRIPT);
|
||||
|
||||
let panel = document.createElement("panel");
|
||||
panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
|
||||
panel.setAttribute("class", "browser-extension-panel");
|
||||
panel.setAttribute("type", "arrow");
|
||||
panel.setAttribute("flip", "slide");
|
||||
|
||||
let anchor;
|
||||
if (node.localName == "toolbarbutton") {
|
||||
// Toolbar buttons are a special case. The panel becomes a child of
|
||||
// the button, and is anchored to the button's icon.
|
||||
node.appendChild(panel);
|
||||
anchor = document.getAnonymousElementByAttribute(node, "class", "toolbarbutton-icon");
|
||||
} else {
|
||||
// In all other cases, the panel is anchored to the target node
|
||||
// itself, and is a child of a popupset node.
|
||||
document.getElementById("mainPopupSet").appendChild(panel);
|
||||
anchor = node;
|
||||
}
|
||||
|
||||
let context;
|
||||
panel.addEventListener("popuphidden", () => {
|
||||
context.unload();
|
||||
panel.remove();
|
||||
});
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
let browser = document.createElementNS(XUL_NS, "browser");
|
||||
browser.setAttribute("type", "content");
|
||||
browser.setAttribute("disableglobalhistory", "true");
|
||||
panel.appendChild(browser);
|
||||
|
||||
let loadListener = () => {
|
||||
panel.removeEventListener("load", loadListener);
|
||||
|
||||
context = new ExtensionPage(extension, {
|
||||
type: "popup",
|
||||
contentWindow: browser.contentWindow,
|
||||
uri: popupURI,
|
||||
docShell: browser.docShell,
|
||||
});
|
||||
GlobalManager.injectInDocShell(browser.docShell, extension, context);
|
||||
browser.setAttribute("src", context.uri.spec);
|
||||
|
||||
let contentLoadListener = () => {
|
||||
browser.removeEventListener("load", contentLoadListener, true);
|
||||
|
||||
let contentViewer = browser.docShell.contentViewer;
|
||||
let width = {}, height = {};
|
||||
try {
|
||||
contentViewer.getContentSize(width, height);
|
||||
[width, height] = [width.value, height.value];
|
||||
} catch (e) {
|
||||
// getContentSize can throw
|
||||
[width, height] = [400, 400];
|
||||
}
|
||||
|
||||
let window = document.defaultView;
|
||||
width /= window.devicePixelRatio;
|
||||
height /= window.devicePixelRatio;
|
||||
width = Math.min(width, 800);
|
||||
height = Math.min(height, 800);
|
||||
|
||||
browser.setAttribute("width", width);
|
||||
browser.setAttribute("height", height);
|
||||
|
||||
panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
|
||||
};
|
||||
browser.addEventListener("load", contentLoadListener, true);
|
||||
};
|
||||
panel.addEventListener("load", loadListener);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
// Manages tab-specific context data, and dispatching tab select events
|
||||
// across all windows.
|
||||
global.TabContext = function TabContext(getDefaults, extension) {
|
||||
this.extension = extension;
|
||||
this.getDefaults = getDefaults;
|
||||
|
||||
this.tabData = new WeakMap();
|
||||
|
||||
AllWindowEvents.addListener("progress", this);
|
||||
AllWindowEvents.addListener("TabSelect", this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
TabContext.prototype = {
|
||||
get(tab) {
|
||||
if (!this.tabData.has(tab)) {
|
||||
this.tabData.set(tab, this.getDefaults(tab));
|
||||
}
|
||||
|
||||
return this.tabData.get(tab);
|
||||
},
|
||||
|
||||
clear(tab) {
|
||||
this.tabData.delete(tab);
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
if (event.type == "TabSelect") {
|
||||
let tab = event.target;
|
||||
this.emit("tab-select", tab);
|
||||
this.emit("location-change", tab);
|
||||
}
|
||||
},
|
||||
|
||||
onLocationChange(browser, webProgress, request, locationURI, flags) {
|
||||
let gBrowser = browser.ownerDocument.defaultView.gBrowser;
|
||||
if (browser === gBrowser.selectedBrowser) {
|
||||
let tab = gBrowser.getTabForBrowser(browser);
|
||||
this.emit("location-change", tab, true);
|
||||
}
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
AllWindowEvents.removeListener("progress", this);
|
||||
AllWindowEvents.removeListener("TabSelect", this);
|
||||
},
|
||||
};
|
||||
|
||||
// Manages mapping between XUL tabs and extension tab IDs.
|
||||
global.TabManager = {
|
||||
_tabs: new WeakMap(),
|
||||
|
@ -39,9 +276,7 @@ global.TabManager = {
|
|||
|
||||
getTab(tabId) {
|
||||
// FIXME: Speed this up without leaking memory somehow.
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
if (!window.gBrowser) {
|
||||
continue;
|
||||
}
|
||||
|
@ -132,9 +367,7 @@ global.WindowManager = {
|
|||
},
|
||||
|
||||
getWindow(id) {
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
for (let window of WindowListManager.browserWindows(true)) {
|
||||
if (this.getId(window) == id) {
|
||||
return window;
|
||||
}
|
||||
|
@ -172,15 +405,25 @@ global.WindowListManager = {
|
|||
_openListeners: new Set(),
|
||||
_closeListeners: new Set(),
|
||||
|
||||
// Returns an iterator for all browser windows. Unless |includeIncomplete| is
|
||||
// true, only fully-loaded windows are returned.
|
||||
*browserWindows(includeIncomplete = false) {
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
if (includeIncomplete || window.document.readyState == "complete") {
|
||||
yield window;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addOpenListener(listener, fireOnExisting = true) {
|
||||
if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
|
||||
Services.ww.registerNotification(this);
|
||||
}
|
||||
this._openListeners.add(listener);
|
||||
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
for (let window of this.browserWindows(true)) {
|
||||
if (window.document.readyState != "complete") {
|
||||
window.addEventListener("load", this);
|
||||
} else if (fireOnExisting) {
|
||||
|
@ -263,7 +506,11 @@ global.AllWindowEvents = {
|
|||
list.add(listener);
|
||||
|
||||
if (needOpenListener) {
|
||||
WindowListManager.addOpenListener(this.openListener);
|
||||
WindowListManager.addOpenListener(this.openListener, false);
|
||||
}
|
||||
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
this.addWindowListener(window, type, listener);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -283,9 +530,7 @@ global.AllWindowEvents = {
|
|||
}
|
||||
}
|
||||
|
||||
let e = Services.wm.getEnumerator("navigator:browser");
|
||||
while (e.hasMoreElements()) {
|
||||
let window = e.getNext();
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
if (type == "progress") {
|
||||
window.gBrowser.removeTabsProgressListener(listener);
|
||||
} else {
|
||||
|
@ -294,15 +539,19 @@ global.AllWindowEvents = {
|
|||
}
|
||||
},
|
||||
|
||||
addWindowListener(window, eventType, listener) {
|
||||
if (eventType == "progress") {
|
||||
window.gBrowser.addTabsProgressListener(listener);
|
||||
} else {
|
||||
window.addEventListener(eventType, listener);
|
||||
}
|
||||
},
|
||||
|
||||
// Runs whenever the "load" event fires for a new window.
|
||||
openListener(window) {
|
||||
for (let [eventType, listeners] of AllWindowEvents._listeners) {
|
||||
for (let listener of listeners) {
|
||||
if (eventType == "progress") {
|
||||
window.gBrowser.addTabsProgressListener(listener);
|
||||
} else {
|
||||
window.addEventListener(eventType, listener);
|
||||
}
|
||||
this.addWindowListener(window, eventType, listener);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@ browser.jar:
|
|||
content/browser/ext-utils.js
|
||||
content/browser/ext-contextMenus.js
|
||||
content/browser/ext-browserAction.js
|
||||
content/browser/ext-pageAction.js
|
||||
content/browser/ext-tabs.js
|
||||
content/browser/ext-windows.js
|
||||
content/browser/ext-bookmarks.js
|
||||
|
|
|
@ -7,7 +7,10 @@ support-files =
|
|||
[browser_ext_simple.js]
|
||||
[browser_ext_currentWindow.js]
|
||||
[browser_ext_browserAction_simple.js]
|
||||
[browser_ext_browserAction_icon.js]
|
||||
[browser_ext_browserAction_pageAction_icon.js]
|
||||
[browser_ext_browserAction_context.js]
|
||||
[browser_ext_pageAction_context.js]
|
||||
[browser_ext_pageAction_popup.js]
|
||||
[browser_ext_contextMenus.js]
|
||||
[browser_ext_getViews.js]
|
||||
[browser_ext_tabs_executeScript.js]
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
add_task(function* testTabSwitchContext() {
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {
|
||||
"default_icon": "default.png",
|
||||
"default_popup": "default.html",
|
||||
"default_title": "Default Title",
|
||||
},
|
||||
"permissions": ["tabs"],
|
||||
},
|
||||
|
||||
background: function () {
|
||||
var details = [
|
||||
{ "icon": browser.runtime.getURL("default.png"),
|
||||
"popup": browser.runtime.getURL("default.html"),
|
||||
"title": "Default Title",
|
||||
"badge": "",
|
||||
"badgeBackgroundColor": null },
|
||||
{ "icon": browser.runtime.getURL("1.png"),
|
||||
"popup": browser.runtime.getURL("default.html"),
|
||||
"title": "Default Title",
|
||||
"badge": "",
|
||||
"badgeBackgroundColor": null },
|
||||
{ "icon": browser.runtime.getURL("2.png"),
|
||||
"popup": browser.runtime.getURL("2.html"),
|
||||
"title": "Title 2",
|
||||
"badge": "2",
|
||||
"badgeBackgroundColor": [0xff, 0, 0] },
|
||||
{ "icon": browser.runtime.getURL("1.png"),
|
||||
"popup": browser.runtime.getURL("default-2.html"),
|
||||
"title": "Default Title 2",
|
||||
"badge": "d2",
|
||||
"badgeBackgroundColor": [0, 0xff, 0] },
|
||||
{ "icon": browser.runtime.getURL("default-2.png"),
|
||||
"popup": browser.runtime.getURL("default-2.html"),
|
||||
"title": "Default Title 2",
|
||||
"badge": "d2",
|
||||
"badgeBackgroundColor": [0, 0xff, 0] },
|
||||
];
|
||||
|
||||
var tabs = [];
|
||||
|
||||
var tests = [
|
||||
expect => {
|
||||
browser.test.log("Initial state, expect default properties.");
|
||||
expect(details[0]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Change the icon in the current tab. Expect default properties excluding the icon.");
|
||||
browser.browserAction.setIcon({ tabId: tabs[0], path: "1.png" });
|
||||
expect(details[1]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Create a new tab. Expect default properties.");
|
||||
browser.tabs.create({ active: true, url: "about:blank?0" }, tab => {
|
||||
tabs.push(tab.id);
|
||||
expect(details[0]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Change properties. Expect new properties.");
|
||||
var tabId = tabs[1];
|
||||
browser.browserAction.setIcon({ tabId, path: "2.png" });
|
||||
browser.browserAction.setPopup({ tabId, popup: "2.html" });
|
||||
browser.browserAction.setTitle({ tabId, title: "Title 2" });
|
||||
browser.browserAction.setBadgeText({ tabId, text: "2" });
|
||||
browser.browserAction.setBadgeBackgroundColor({ tabId, color: [0xff, 0, 0] });
|
||||
|
||||
expect(details[2]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Navigate to a new page. Expect no changes.");
|
||||
|
||||
// TODO: This listener should not be necessary, but the |tabs.update|
|
||||
// callback currently fires too early in e10s windows.
|
||||
browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
|
||||
if (tabId == tabs[1] && changed.url) {
|
||||
browser.tabs.onUpdated.removeListener(listener);
|
||||
expect(details[2]);
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.update(tabs[1], { url: "about:blank?1" });
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Switch back to the first tab. Expect previously set properties.");
|
||||
browser.tabs.update(tabs[0], { active: true }, () => {
|
||||
expect(details[1]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Change default values, expect those changes reflected.");
|
||||
browser.browserAction.setIcon({ path: "default-2.png" });
|
||||
browser.browserAction.setPopup({ popup: "default-2.html" });
|
||||
browser.browserAction.setTitle({ title: "Default Title 2" });
|
||||
browser.browserAction.setBadgeText({ text: "d2" });
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: [0, 0xff, 0] });
|
||||
expect(details[3]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Switch back to tab 2. Expect former value, unaffected by changes to defaults in previous step.");
|
||||
browser.tabs.update(tabs[1], { active: true }, () => {
|
||||
expect(details[2]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Delete tab, switch back to tab 1. Expect previous results again.");
|
||||
browser.tabs.remove(tabs[1], () => {
|
||||
expect(details[3]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Create a new tab. Expect new default properties.");
|
||||
browser.tabs.create({ active: true, url: "about:blank?2" }, tab => {
|
||||
tabs.push(tab.id);
|
||||
expect(details[4]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Delete tab.");
|
||||
browser.tabs.remove(tabs[2], () => {
|
||||
expect(details[3]);
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
// Gets the current details of the browser action, and returns a
|
||||
// promise that resolves to an object containing them.
|
||||
function getDetails() {
|
||||
return new Promise(resolve => {
|
||||
return browser.tabs.query({ active: true, currentWindow: true }, resolve);
|
||||
}).then(tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
return Promise.all([
|
||||
new Promise(resolve => browser.browserAction.getTitle({tabId}, resolve)),
|
||||
new Promise(resolve => browser.browserAction.getPopup({tabId}, resolve)),
|
||||
new Promise(resolve => browser.browserAction.getBadgeText({tabId}, resolve)),
|
||||
new Promise(resolve => browser.browserAction.getBadgeBackgroundColor({tabId}, resolve))])
|
||||
}).then(details => {
|
||||
return Promise.resolve({ title: details[0],
|
||||
popup: details[1],
|
||||
badge: details[2],
|
||||
badgeBackgroundColor: details[3] });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Runs the next test in the `tests` array, checks the results,
|
||||
// and passes control back to the outer test scope.
|
||||
function nextTest() {
|
||||
var test = tests.shift();
|
||||
|
||||
test(expecting => {
|
||||
// Check that the API returns the expected values, and then
|
||||
// run the next test.
|
||||
getDetails().then(details => {
|
||||
browser.test.assertEq(expecting.title, details.title,
|
||||
"expected value from getTitle");
|
||||
|
||||
browser.test.assertEq(expecting.popup, details.popup,
|
||||
"expected value from getPopup");
|
||||
|
||||
browser.test.assertEq(expecting.badge, details.badge,
|
||||
"expected value from getBadge");
|
||||
|
||||
browser.test.assertEq(String(expecting.badgeBackgroundColor),
|
||||
String(details.badgeBackgroundColor),
|
||||
"expected value from getBadgeBackgroundColor");
|
||||
|
||||
// Check that the actual icon has the expected values, then
|
||||
// run the next test.
|
||||
browser.test.sendMessage("nextTest", expecting, tests.length);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
browser.test.onMessage.addListener((msg) => {
|
||||
if (msg != "runNextTest") {
|
||||
browser.test.fail("Expecting 'runNextTest' message");
|
||||
}
|
||||
|
||||
nextTest();
|
||||
});
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
|
||||
tabs[0] = resultTabs[0].id;
|
||||
|
||||
nextTest();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
|
||||
|
||||
function checkDetails(details) {
|
||||
let button = document.getElementById(browserActionId);
|
||||
|
||||
ok(button, "button exists");
|
||||
|
||||
is(button.getAttribute("image"), details.icon, "icon URL is correct");
|
||||
is(button.getAttribute("tooltiptext"), details.title, "image title is correct");
|
||||
is(button.getAttribute("label"), details.title, "image label is correct");
|
||||
is(button.getAttribute("aria-label"), details.title, "image aria-label is correct");
|
||||
is(button.getAttribute("badge"), details.badge, "badge text is correct");
|
||||
|
||||
if (details.badge && details.badgeBackgroundColor) {
|
||||
let badge = button.ownerDocument.getAnonymousElementByAttribute(
|
||||
button, 'class', 'toolbarbutton-badge');
|
||||
|
||||
let badgeColor = window.getComputedStyle(badge).backgroundColor;
|
||||
let color = details.badgeBackgroundColor;
|
||||
let expectedColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`
|
||||
|
||||
is(badgeColor, expectedColor, "badge color is correct");
|
||||
}
|
||||
|
||||
|
||||
// TODO: Popup URL.
|
||||
}
|
||||
|
||||
let awaitFinish = new Promise(resolve => {
|
||||
extension.onMessage("nextTest", (expecting, testsRemaining) => {
|
||||
checkDetails(expecting);
|
||||
|
||||
if (testsRemaining) {
|
||||
extension.sendMessage("runNextTest")
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
yield awaitFinish;
|
||||
|
||||
yield extension.unload();
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
add_task(function* () {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {},
|
||||
"background": {
|
||||
"page": "background.html",
|
||||
}
|
||||
},
|
||||
|
||||
files: {
|
||||
"background.html": `<canvas id="canvas" width="2" height="2">
|
||||
<script src="background.js"></script>`,
|
||||
|
||||
"background.js": function() {
|
||||
var canvas = document.getElementById("canvas");
|
||||
var canvasContext = canvas.getContext("2d");
|
||||
|
||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvasContext.fillStyle = "green";
|
||||
canvasContext.fillRect(0, 0, 1, 1);
|
||||
|
||||
var url = canvas.toDataURL("image/png");
|
||||
var imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);
|
||||
browser.browserAction.setIcon({imageData});
|
||||
|
||||
browser.test.sendMessage("imageURL", url);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let [_, url] = yield Promise.all([extension.startup(), extension.awaitMessage("imageURL")]);
|
||||
|
||||
let widgetId = makeWidgetId(extension.id) + "-browser-action";
|
||||
let node = CustomizableUI.getWidget(widgetId).forWindow(window).node;
|
||||
|
||||
let image = node.getAttribute("image");
|
||||
is(image, url, "image is correct");
|
||||
|
||||
yield extension.unload();
|
||||
});
|
|
@ -0,0 +1,345 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
// Test that various combinations of icon details specs, for both paths
|
||||
// and ImageData objects, result in the correct image being displayed in
|
||||
// all display resolutions.
|
||||
add_task(function* testDetailsObjects() {
|
||||
function background() {
|
||||
function getImageData(color) {
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = 2;
|
||||
canvas.height = 2;
|
||||
var canvasContext = canvas.getContext("2d");
|
||||
|
||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvasContext.fillStyle = color;
|
||||
canvasContext.fillRect(0, 0, 1, 1);
|
||||
|
||||
return {
|
||||
url: canvas.toDataURL("image/png"),
|
||||
imageData: canvasContext.getImageData(0, 0, canvas.width, canvas.height),
|
||||
};
|
||||
}
|
||||
|
||||
var imageData = {
|
||||
red: getImageData("red"),
|
||||
green: getImageData("green"),
|
||||
};
|
||||
|
||||
var iconDetails = [
|
||||
// Only paths.
|
||||
{ details: { "path": "a.png" },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
{ details: { "path": "/a.png" },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("a.png"),
|
||||
"2": browser.runtime.getURL("a.png"), } },
|
||||
{ details: { "path": { "19": "a.png" } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
{ details: { "path": { "38": "a.png" } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
{ details: { "path": { "19": "a.png", "38": "a-x2.png" } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": browser.runtime.getURL("data/a-x2.png"), } },
|
||||
|
||||
// Only ImageData objects.
|
||||
{ details: { "imageData": imageData.red.imageData },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": imageData.red.url, } },
|
||||
{ details: { "imageData": { "19": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": imageData.red.url, } },
|
||||
{ details: { "imageData": { "38": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": imageData.red.url, } },
|
||||
{ details: { "imageData": {
|
||||
"19": imageData.red.imageData,
|
||||
"38": imageData.green.imageData } },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": imageData.green.url, } },
|
||||
|
||||
// Mixed path and imageData objects.
|
||||
//
|
||||
// The behavior is currently undefined if both |path| and
|
||||
// |imageData| specify icons of the same size.
|
||||
{ details: {
|
||||
"path": { "19": "a.png" },
|
||||
"imageData": { "38": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": imageData.red.url, } },
|
||||
{ details: {
|
||||
"path": { "38": "a.png" },
|
||||
"imageData": { "19": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
|
||||
// A path or ImageData object by itself is treated as a 19px icon.
|
||||
{ details: {
|
||||
"path": "a.png",
|
||||
"imageData": { "38": imageData.red.imageData } },
|
||||
resolutions: {
|
||||
"1": browser.runtime.getURL("data/a.png"),
|
||||
"2": imageData.red.url, } },
|
||||
{ details: {
|
||||
"path": { "38": "a.png" },
|
||||
"imageData": imageData.red.imageData, },
|
||||
resolutions: {
|
||||
"1": imageData.red.url,
|
||||
"2": browser.runtime.getURL("data/a.png"), } },
|
||||
];
|
||||
|
||||
// Allow serializing ImageData objects for logging.
|
||||
ImageData.prototype.toJSON = () => "<ImageData>";
|
||||
|
||||
var tabId;
|
||||
|
||||
browser.test.onMessage.addListener((msg, test) => {
|
||||
if (msg != "setIcon") {
|
||||
browser.test.fail("expecting 'setIcon' message");
|
||||
}
|
||||
|
||||
var details = iconDetails[test.index];
|
||||
var expectedURL = details.resolutions[test.resolution];
|
||||
|
||||
var detailString = JSON.stringify(details);
|
||||
browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URL ${expectedURL}`)
|
||||
|
||||
browser.browserAction.setIcon(Object.assign({tabId}, details.details));
|
||||
browser.pageAction.setIcon(Object.assign({tabId}, details.details));
|
||||
|
||||
browser.test.sendMessage("imageURL", expectedURL);
|
||||
});
|
||||
|
||||
// Generate a list of tests and resolutions to send back to the test
|
||||
// context.
|
||||
//
|
||||
// This process is a bit convoluted, because the outer test context needs
|
||||
// to handle checking the button nodes and changing the screen resolution,
|
||||
// but it can't pass us icon definitions with ImageData objects. This
|
||||
// shouldn't be a problem, since structured clones should handle ImageData
|
||||
// objects without issue. Unfortunately, |cloneInto| implements a slightly
|
||||
// different algorithm than we use in web APIs, and does not handle them
|
||||
// correctly.
|
||||
var tests = [];
|
||||
for (var [idx, icon] of iconDetails.entries()) {
|
||||
for (var res of Object.keys(icon.resolutions)) {
|
||||
tests.push({ index: idx, resolution: Number(res) });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by resolution, so we don't needlessly switch back and forth
|
||||
// between each test.
|
||||
tests.sort(test => test.resolution);
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
tabId = tabs[0].id;
|
||||
browser.pageAction.show(tabId);
|
||||
|
||||
browser.test.sendMessage("ready", tests);
|
||||
});
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {},
|
||||
"page_action": {},
|
||||
"background": {
|
||||
"page": "data/background.html",
|
||||
}
|
||||
},
|
||||
|
||||
files: {
|
||||
"data/background.html": `<script src="background.js"></script>`,
|
||||
"data/background.js": background,
|
||||
},
|
||||
});
|
||||
|
||||
const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
|
||||
registerCleanupFunction(() => {
|
||||
SpecialPowers.clearUserPref(RESOLUTION_PREF);
|
||||
});
|
||||
|
||||
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
let [, tests] = yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
|
||||
|
||||
for (let test of tests) {
|
||||
SpecialPowers.setCharPref(RESOLUTION_PREF, String(test.resolution));
|
||||
is(window.devicePixelRatio, test.resolution, "window has the required resolution");
|
||||
|
||||
extension.sendMessage("setIcon", test);
|
||||
|
||||
let imageURL = yield extension.awaitMessage("imageURL");
|
||||
|
||||
let browserActionButton = document.getElementById(browserActionId);
|
||||
is(browserActionButton.getAttribute("image"), imageURL, "browser action has the correct image");
|
||||
|
||||
let pageActionImage = document.getElementById(pageActionId);
|
||||
is(pageActionImage.src, imageURL, "page action has the correct image");
|
||||
}
|
||||
|
||||
yield extension.unload();
|
||||
});
|
||||
|
||||
// Test that default icon details in the manifest.json file are handled
|
||||
// correctly.
|
||||
add_task(function *testDefaultDetails() {
|
||||
// TODO: Test localized variants.
|
||||
let icons = [
|
||||
"foo/bar.png",
|
||||
"/foo/bar.png",
|
||||
{ "19": "foo/bar.png" },
|
||||
{ "38": "foo/bar.png" },
|
||||
{ "19": "foo/bar.png", "38": "baz/quux.png" },
|
||||
];
|
||||
|
||||
let expectedURL = new RegExp(String.raw`^moz-extension://[^/]+/foo/bar\.png$`);
|
||||
|
||||
for (let icon of icons) {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": { "default_icon": icon },
|
||||
"page_action": { "default_icon": icon },
|
||||
},
|
||||
|
||||
background: function () {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
browser.pageAction.show(tabId);
|
||||
browser.test.sendMessage("ready");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
|
||||
|
||||
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
let browserActionButton = document.getElementById(browserActionId);
|
||||
let image = browserActionButton.getAttribute("image");
|
||||
|
||||
ok(expectedURL.test(image), `browser action image ${image} matches ${expectedURL}`);
|
||||
|
||||
let pageActionImage = document.getElementById(pageActionId);
|
||||
image = pageActionImage.src;
|
||||
|
||||
ok(expectedURL.test(image), `page action image ${image} matches ${expectedURL}`);
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
let node = document.getElementById(pageActionId);
|
||||
is(node, undefined, "pageAction image removed from document");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Check that attempts to load a privileged URL as an icon image fail.
|
||||
add_task(function* testSecureURLsDenied() {
|
||||
|
||||
// Test URLs passed to setIcon.
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {},
|
||||
"page_action": {},
|
||||
},
|
||||
|
||||
background: function () {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
var urls = ["chrome://browser/content/browser.xul",
|
||||
"javascript:true"];
|
||||
|
||||
for (var url of urls) {
|
||||
for (var api of ["pageAction", "browserAction"]) {
|
||||
try {
|
||||
browser[api].setIcon({tabId, path: url});
|
||||
|
||||
browser.test.fail(`Load of '${url}' succeeded. Expected failure.`);
|
||||
browser.test.notifyFail("setIcon security tests");
|
||||
return;
|
||||
} catch (e) {
|
||||
// We can't actually inspect the error here, since the
|
||||
// error object belongs to the privileged scope of the API,
|
||||
// rather than to the extension scope that calls into it.
|
||||
// Just assume it's the expected security error, for now.
|
||||
browser.test.succeed(`Load of '${url}' failed. Expected failure.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
browser.test.notifyPass("setIcon security tests");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
yield extension.awaitFinish();
|
||||
yield extension.unload();
|
||||
|
||||
|
||||
// Test URLs included in the manifest.
|
||||
|
||||
let urls = ["chrome://browser/content/browser.xul",
|
||||
"javascript:true"];
|
||||
|
||||
let matchURLForbidden = url => ({
|
||||
message: new RegExp(`Loading extension.*Access to.*'${url}' denied`),
|
||||
});
|
||||
|
||||
let messages = [matchURLForbidden(urls[0]),
|
||||
matchURLForbidden(urls[1]),
|
||||
matchURLForbidden(urls[0]),
|
||||
matchURLForbidden(urls[1])];
|
||||
|
||||
let waitForConsole = new Promise(resolve => {
|
||||
// Not necessary in browser-chrome tests, but monitorConsole gripes
|
||||
// if we don't call it.
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
SimpleTest.monitorConsole(resolve, messages);
|
||||
});
|
||||
|
||||
extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"19": urls[0],
|
||||
"38": urls[1],
|
||||
},
|
||||
},
|
||||
"page_action": {
|
||||
"default_icon": {
|
||||
"19": urls[0],
|
||||
"38": urls[1],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
yield extension.unload();
|
||||
|
||||
SimpleTest.endMonitorConsole();
|
||||
yield waitForConsole;
|
||||
});
|
|
@ -0,0 +1,209 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
add_task(function* testTabSwitchContext() {
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"page_action": {
|
||||
"default_icon": "default.png",
|
||||
"default_popup": "default.html",
|
||||
"default_title": "Default Title",
|
||||
},
|
||||
"permissions": ["tabs"],
|
||||
},
|
||||
|
||||
background: function () {
|
||||
var details = [
|
||||
{ "icon": browser.runtime.getURL("default.png"),
|
||||
"popup": browser.runtime.getURL("default.html"),
|
||||
"title": "Default Title" },
|
||||
{ "icon": browser.runtime.getURL("1.png"),
|
||||
"popup": browser.runtime.getURL("default.html"),
|
||||
"title": "Default Title" },
|
||||
{ "icon": browser.runtime.getURL("2.png"),
|
||||
"popup": browser.runtime.getURL("2.html"),
|
||||
"title": "Title 2" },
|
||||
];
|
||||
|
||||
var tabs = [];
|
||||
|
||||
var tests = [
|
||||
expect => {
|
||||
browser.test.log("Initial state. No icon visible.");
|
||||
expect(null);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Show the icon on the first tab, expect default properties.");
|
||||
browser.pageAction.show(tabs[0]);
|
||||
expect(details[0]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Change the icon. Expect default properties excluding the icon.");
|
||||
browser.pageAction.setIcon({ tabId: tabs[0], path: "1.png" });
|
||||
expect(details[1]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Create a new tab. No icon visible.");
|
||||
browser.tabs.create({ active: true, url: "about:blank?0" }, tab => {
|
||||
tabs.push(tab.id);
|
||||
expect(null);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Change properties. Expect new properties.");
|
||||
var tabId = tabs[1];
|
||||
browser.pageAction.show(tabId);
|
||||
browser.pageAction.setIcon({ tabId, path: "2.png" });
|
||||
browser.pageAction.setPopup({ tabId, popup: "2.html" });
|
||||
browser.pageAction.setTitle({ tabId, title: "Title 2" });
|
||||
|
||||
expect(details[2]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Navigate to a new page. Expect icon hidden.");
|
||||
|
||||
// TODO: This listener should not be necessary, but the |tabs.update|
|
||||
// callback currently fires too early in e10s windows.
|
||||
browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
|
||||
if (tabId == tabs[1] && changed.url) {
|
||||
browser.tabs.onUpdated.removeListener(listener);
|
||||
expect(null);
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.update(tabs[1], { url: "about:blank?1" });
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Show the icon. Expect default properties again.");
|
||||
browser.pageAction.show(tabs[1]);
|
||||
expect(details[0]);
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Switch back to the first tab. Expect previously set properties.");
|
||||
browser.tabs.update(tabs[0], { active: true }, () => {
|
||||
expect(details[1]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Hide the icon on tab 2. Switch back, expect hidden.");
|
||||
browser.pageAction.hide(tabs[1]);
|
||||
browser.tabs.update(tabs[1], { active: true }, () => {
|
||||
expect(null);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Switch back to tab 1. Expect previous results again.");
|
||||
browser.tabs.remove(tabs[1], () => {
|
||||
expect(details[1]);
|
||||
});
|
||||
},
|
||||
expect => {
|
||||
browser.test.log("Hide the icon. Expect hidden.");
|
||||
browser.pageAction.hide(tabs[0]);
|
||||
expect(null);
|
||||
},
|
||||
];
|
||||
|
||||
// Gets the current details of the page action, and returns a
|
||||
// promise that resolves to an object containing them.
|
||||
function getDetails() {
|
||||
return new Promise(resolve => {
|
||||
return browser.tabs.query({ active: true, currentWindow: true }, resolve);
|
||||
}).then(tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
return Promise.all([
|
||||
new Promise(resolve => browser.pageAction.getTitle({tabId}, resolve)),
|
||||
new Promise(resolve => browser.pageAction.getPopup({tabId}, resolve))])
|
||||
}).then(details => {
|
||||
return Promise.resolve({ title: details[0],
|
||||
popup: details[1] });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Runs the next test in the `tests` array, checks the results,
|
||||
// and passes control back to the outer test scope.
|
||||
function nextTest() {
|
||||
var test = tests.shift();
|
||||
|
||||
test(expecting => {
|
||||
function finish() {
|
||||
// Check that the actual icon has the expected values, then
|
||||
// run the next test.
|
||||
browser.test.sendMessage("nextTest", expecting, tests.length);
|
||||
}
|
||||
|
||||
if (expecting) {
|
||||
// Check that the API returns the expected values, and then
|
||||
// run the next test.
|
||||
getDetails().then(details => {
|
||||
browser.test.assertEq(expecting.title, details.title,
|
||||
"expected value from getTitle");
|
||||
|
||||
browser.test.assertEq(expecting.popup, details.popup,
|
||||
"expected value from getPopup");
|
||||
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
browser.test.onMessage.addListener((msg) => {
|
||||
if (msg != "runNextTest") {
|
||||
browser.test.fail("Expecting 'runNextTest' message");
|
||||
}
|
||||
|
||||
nextTest();
|
||||
});
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
|
||||
tabs[0] = resultTabs[0].id;
|
||||
|
||||
nextTest();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
function checkDetails(details) {
|
||||
let image = document.getElementById(pageActionId);
|
||||
if (details == null) {
|
||||
ok(image == null || image.hidden, "image is hidden");
|
||||
} else {
|
||||
ok(image, "image exists");
|
||||
|
||||
is(image.src, details.icon, "icon URL is correct");
|
||||
is(image.getAttribute("tooltiptext"), details.title, "image title is correct");
|
||||
is(image.getAttribute("aria-label"), details.title, "image aria-label is correct");
|
||||
// TODO: Popup URL.
|
||||
}
|
||||
}
|
||||
|
||||
let awaitFinish = new Promise(resolve => {
|
||||
extension.onMessage("nextTest", (expecting, testsRemaining) => {
|
||||
checkDetails(expecting);
|
||||
|
||||
if (testsRemaining) {
|
||||
extension.sendMessage("runNextTest")
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
yield awaitFinish;
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
let node = document.getElementById(pageActionId);
|
||||
is(node, undefined, "pageAction image removed from document");
|
||||
});
|
|
@ -0,0 +1,215 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
function promisePopupShown(popup) {
|
||||
return new Promise(resolve => {
|
||||
if (popup.popupOpen) {
|
||||
resolve();
|
||||
} else {
|
||||
let onPopupShown = event => {
|
||||
popup.removeEventListener("popupshown", onPopupShown);
|
||||
resolve();
|
||||
};
|
||||
popup.addEventListener("popupshown", onPopupShown);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* testPageActionPopup() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"background": {
|
||||
"page": "data/background.html"
|
||||
},
|
||||
"page_action": {
|
||||
"default_popup": "popup-a.html"
|
||||
}
|
||||
},
|
||||
|
||||
files: {
|
||||
"popup-a.html": `<script src="popup-a.js"></script>`,
|
||||
"popup-a.js": function() {
|
||||
browser.runtime.sendMessage("from-popup-a");
|
||||
},
|
||||
|
||||
"data/popup-b.html": `<script src="popup-b.js"></script>`,
|
||||
"data/popup-b.js": function() {
|
||||
browser.runtime.sendMessage("from-popup-b");
|
||||
},
|
||||
|
||||
"data/background.html": `<script src="background.js"></script>`,
|
||||
|
||||
"data/background.js": function() {
|
||||
var tabId;
|
||||
|
||||
var tests = [
|
||||
() => {
|
||||
sendClick({ expectEvent: false, expectPopup: "a" });
|
||||
},
|
||||
() => {
|
||||
sendClick({ expectEvent: false, expectPopup: "a" });
|
||||
},
|
||||
() => {
|
||||
browser.pageAction.setPopup({ tabId, popup: "popup-b.html" });
|
||||
sendClick({ expectEvent: false, expectPopup: "b" });
|
||||
},
|
||||
() => {
|
||||
sendClick({ expectEvent: false, expectPopup: "b" });
|
||||
},
|
||||
() => {
|
||||
browser.pageAction.setPopup({ tabId, popup: "" });
|
||||
sendClick({ expectEvent: true, expectPopup: null });
|
||||
},
|
||||
() => {
|
||||
sendClick({ expectEvent: true, expectPopup: null });
|
||||
},
|
||||
() => {
|
||||
browser.pageAction.setPopup({ tabId, popup: "/popup-a.html" });
|
||||
sendClick({ expectEvent: false, expectPopup: "a" });
|
||||
},
|
||||
];
|
||||
|
||||
var expect = {};
|
||||
function sendClick({ expectEvent, expectPopup }) {
|
||||
expect = { event: expectEvent, popup: expectPopup };
|
||||
browser.test.sendMessage("send-click");
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(msg => {
|
||||
if (expect.popup) {
|
||||
browser.test.assertEq(msg, `from-popup-${expect.popup}`,
|
||||
"expected popup opened");
|
||||
} else {
|
||||
browser.test.fail("unexpected popup");
|
||||
}
|
||||
|
||||
expect.popup = null;
|
||||
browser.test.sendMessage("next-test");
|
||||
});
|
||||
|
||||
browser.pageAction.onClicked.addListener(() => {
|
||||
if (expect.event) {
|
||||
browser.test.succeed("expected click event received");
|
||||
} else {
|
||||
browser.test.fail("unexpected click event");
|
||||
}
|
||||
|
||||
expect.event = false;
|
||||
browser.test.sendMessage("next-test");
|
||||
});
|
||||
|
||||
browser.test.onMessage.addListener((msg) => {
|
||||
if (msg != "next-test") {
|
||||
browser.test.fail("Expecting 'next-test' message");
|
||||
}
|
||||
|
||||
if (tests.length) {
|
||||
var test = tests.shift();
|
||||
test();
|
||||
} else {
|
||||
browser.test.notifyPass("pageaction-tests-done");
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
tabId = tabs[0].id;
|
||||
|
||||
browser.pageAction.show(tabId);
|
||||
browser.test.sendMessage("next-test");
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
let panelId = makeWidgetId(extension.id) + "-panel";
|
||||
|
||||
extension.onMessage("send-click", () => {
|
||||
let image = document.getElementById(pageActionId);
|
||||
|
||||
let evt = new MouseEvent("click", {});
|
||||
image.dispatchEvent(evt);
|
||||
});
|
||||
|
||||
extension.onMessage("next-test", Task.async(function* () {
|
||||
let panel = document.getElementById(panelId);
|
||||
if (panel) {
|
||||
yield promisePopupShown(panel);
|
||||
panel.hidePopup();
|
||||
|
||||
panel = document.getElementById(panelId);
|
||||
is(panel, undefined, "panel successfully removed from document after hiding");
|
||||
}
|
||||
|
||||
extension.sendMessage("next-test");
|
||||
}));
|
||||
|
||||
|
||||
yield Promise.all([extension.startup(), extension.awaitFinish("pageaction-tests-done")]);
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
let node = document.getElementById(pageActionId);
|
||||
is(node, undefined, "pageAction image removed from document");
|
||||
|
||||
let panel = document.getElementById(panelId);
|
||||
is(panel, undefined, "pageAction panel removed from document");
|
||||
});
|
||||
|
||||
|
||||
add_task(function* testPageActionSecurity() {
|
||||
const URL = "chrome://browser/content/browser.xul";
|
||||
|
||||
let matchURLForbidden = url => ({
|
||||
message: new RegExp(`Loading extension.*Access to.*'${URL}' denied`),
|
||||
});
|
||||
|
||||
let messages = [/Access to restricted URI denied/,
|
||||
/Access to restricted URI denied/];
|
||||
|
||||
let waitForConsole = new Promise(resolve => {
|
||||
// Not necessary in browser-chrome tests, but monitorConsole gripes
|
||||
// if we don't call it.
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
SimpleTest.monitorConsole(resolve, messages);
|
||||
});
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"browser_action": { "default_popup": URL },
|
||||
"page_action": { "default_popup": URL },
|
||||
},
|
||||
|
||||
background: function () {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
var tabId = tabs[0].id;
|
||||
|
||||
browser.pageAction.show(tabId);
|
||||
browser.test.sendMessage("ready");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
|
||||
|
||||
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
|
||||
let pageActionId = makeWidgetId(extension.id) + "-page-action";
|
||||
|
||||
let browserAction = document.getElementById(browserActionId);
|
||||
let evt = new CustomEvent("command", {});
|
||||
browserAction.dispatchEvent(evt);
|
||||
|
||||
let pageAction = document.getElementById(pageActionId);
|
||||
evt = new MouseEvent("click", {});
|
||||
pageAction.dispatchEvent(evt);
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
let node = document.getElementById(pageActionId);
|
||||
is(node, undefined, "pageAction image removed from document");
|
||||
|
||||
SimpleTest.endMonitorConsole();
|
||||
yield waitForConsole;
|
||||
});
|
|
@ -642,6 +642,7 @@ BrowserGlue.prototype = {
|
|||
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
|
||||
|
|
|
@ -937,6 +937,9 @@ toolbar .toolbarbutton-1:-moz-any(@primaryToolbarButtons@) > :-moz-any(.toolbarb
|
|||
|
||||
.urlbar-icon {
|
||||
padding: 0 3px;
|
||||
/* 16x16 icon with border-box sizing */
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#urlbar-search-footer {
|
||||
|
@ -1942,7 +1945,7 @@ toolbarbutton.chevron > .toolbarbutton-icon {
|
|||
-moz-margin-end: 0 !important;
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -1728,6 +1728,9 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
|
|||
|
||||
.urlbar-icon {
|
||||
padding: 0 3px;
|
||||
/* 16x16 icon with border-box sizing */
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#urlbar-search-footer {
|
||||
|
@ -2015,7 +2018,6 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-
|
|||
#page-report-button {
|
||||
list-style-image: url("chrome://browser/skin/urlbar-popup-blocked@2x.png");
|
||||
-moz-image-region: rect(0, 32px, 32px, 0);
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
#page-report-button:hover:active,
|
||||
|
@ -3617,7 +3619,7 @@ notification[value="loop-sharing-notification"] .messageImage {
|
|||
padding-right: 0;
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
%endif
|
||||
|
||||
/* Hide all conditional elements by default. */
|
||||
:-moz-any([when-connection],[when-mixedcontent],[when-ciphers]) {
|
||||
:-moz-any([when-connection],[when-mixedcontent],[when-ciphers],[when-loginforms]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@
|
|||
#identity-popup[connection=secure] [when-connection~=secure],
|
||||
#identity-popup[connection=chrome] [when-connection~=chrome],
|
||||
#identity-popup[connection=file] [when-connection~=file],
|
||||
/* Show insecure login forms messages when needed. */
|
||||
#identity-popup[loginforms=insecure] [when-loginforms=insecure],
|
||||
/* Show weak cipher messages when needed. */
|
||||
#identity-popup[ciphers=weak] [when-ciphers~=weak],
|
||||
/* Show mixed content warnings when needed */
|
||||
|
@ -28,6 +30,14 @@
|
|||
display: inherit;
|
||||
}
|
||||
|
||||
/* Hide redundant messages based on insecure login forms presence. */
|
||||
#identity-popup[loginforms=secure] [and-when-loginforms=insecure] {
|
||||
display: none;
|
||||
}
|
||||
#identity-popup[loginforms=insecure] [and-when-loginforms=secure] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide 'not secure' message in subview when weak cipher or mixed content messages are shown. */
|
||||
#identity-popup-securityView-body:-moz-any([mixedcontent],[ciphers]) > description[when-connection=not-secure],
|
||||
/* Hide 'passive-loaded (only)' message when there is mixed passive content loaded and active blocked. */
|
||||
|
@ -224,6 +234,8 @@
|
|||
background-image: url(chrome://browser/skin/controlcenter/conn-degraded.svg);
|
||||
}
|
||||
|
||||
#identity-popup[loginforms=insecure] #identity-popup-securityView,
|
||||
#identity-popup[loginforms=insecure] #identity-popup-security-content,
|
||||
#identity-popup[mixedcontent~=active-loaded][isbroken] #identity-popup-securityView,
|
||||
#identity-popup[mixedcontent~=active-loaded][isbroken] #identity-popup-security-content {
|
||||
background-image: url(chrome://browser/skin/controlcenter/mcb-disabled.svg);
|
||||
|
|
|
@ -123,6 +123,7 @@
|
|||
list-style-image: url(chrome://browser/skin/identity-secure.svg);
|
||||
}
|
||||
|
||||
.insecureLoginForms > #identity-icons > #page-proxy-favicon[pageproxystate="valid"],
|
||||
.mixedActiveContent > #identity-icons > #page-proxy-favicon[pageproxystate="valid"] {
|
||||
list-style-image: url(chrome://browser/skin/identity-mixed-active-loaded.svg);
|
||||
}
|
||||
|
|
|
@ -1325,6 +1325,9 @@ html|*.urlbar-input:-moz-lwtheme::-moz-placeholder,
|
|||
|
||||
.urlbar-icon {
|
||||
padding: 0 3px;
|
||||
/* 16x16 icon with border-box sizing */
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.search-go-container {
|
||||
|
@ -2802,7 +2805,7 @@ notification[value="loop-sharing-notification"] .messageImage {
|
|||
%include browser-aero.css
|
||||
}
|
||||
|
||||
.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -480,10 +480,10 @@ Workers.prototype = {
|
|||
this._updateWorkerList();
|
||||
},
|
||||
|
||||
_onWorkerSelect: function (type, workerActor) {
|
||||
_onWorkerSelect: function (workerActor) {
|
||||
DebuggerController.client.attachWorker(workerActor, (response, workerClient) => {
|
||||
gDevTools.showToolbox(devtools.TargetFactory.forWorker(workerClient),
|
||||
"jsdebugger", devtools.Toolbox.HostType.WINDOW);
|
||||
gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
|
||||
"jsdebugger", Toolbox.HostType.WINDOW);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/* 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";
|
||||
|
||||
// @TODO 1215606
|
||||
// Use this assert instead of utils when fixed.
|
||||
// const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { breakdownEquals, createSnapshot, assert } = require("../utils");
|
||||
const { actions, snapshotState: states } = require("../constants");
|
||||
const { takeCensus } = require("./snapshot");
|
||||
|
||||
const setBreakdownAndRefresh = exports.setBreakdownAndRefresh = function (heapWorker, breakdown) {
|
||||
return function *(dispatch, getState) {
|
||||
// Clears out all stored census data and sets
|
||||
// the breakdown
|
||||
dispatch(setBreakdown(breakdown));
|
||||
let snapshot = getState().snapshots.find(s => s.selected);
|
||||
|
||||
// If selected snapshot does not have updated census if the breakdown
|
||||
// changed, retake the census with new breakdown
|
||||
if (snapshot && !breakdownEquals(snapshot.breakdown, breakdown)) {
|
||||
yield dispatch(takeCensus(heapWorker, snapshot));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears out all census data in the snapshots and sets
|
||||
* a new breakdown.
|
||||
*
|
||||
* @param {Breakdown} breakdown
|
||||
*/
|
||||
const setBreakdown = exports.setBreakdown = function (breakdown) {
|
||||
// @TODO 1215606
|
||||
assert(typeof breakdown === "object" && breakdown.by,
|
||||
`Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(breakdown)}`);
|
||||
|
||||
return {
|
||||
type: actions.SET_BREAKDOWN,
|
||||
breakdown,
|
||||
}
|
||||
};
|
|
@ -4,5 +4,6 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'breakdown.js',
|
||||
'snapshot.js',
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
// @TODO 1215606
|
||||
// Use this assert instead of utils when fixed.
|
||||
// const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { createSnapshot, assert } = require("../utils");
|
||||
const { getSnapshot, breakdownEquals, createSnapshot, assert } = require("../utils");
|
||||
const { actions, snapshotState: states } = require("../constants");
|
||||
|
||||
/**
|
||||
|
@ -17,19 +17,36 @@ const { actions, snapshotState: states } = require("../constants");
|
|||
* @param {HeapAnalysesClient}
|
||||
* @param {Object}
|
||||
*/
|
||||
const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function takeSnapshotAndCensus (front, heapWorker) {
|
||||
return function *(dispatch, getStore) {
|
||||
const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, heapWorker) {
|
||||
return function *(dispatch, getState) {
|
||||
let snapshot = yield dispatch(takeSnapshot(front));
|
||||
yield dispatch(readSnapshot(heapWorker, snapshot));
|
||||
yield dispatch(takeCensus(heapWorker, snapshot));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects a snapshot and if the snapshot's census is using a different
|
||||
* breakdown, take a new census.
|
||||
*
|
||||
* @param {HeapAnalysesClient}
|
||||
* @param {Snapshot}
|
||||
*/
|
||||
const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, snapshot) {
|
||||
return function *(dispatch, getState) {
|
||||
dispatch(selectSnapshot(snapshot));
|
||||
|
||||
// Attempt to take another census; if the snapshot already is using
|
||||
// the correct breakdown, this will noop.
|
||||
yield dispatch(takeCensus(heapWorker, snapshot));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {MemoryFront}
|
||||
*/
|
||||
const takeSnapshot = exports.takeSnapshot = function takeSnapshot (front) {
|
||||
return function *(dispatch, getStore) {
|
||||
const takeSnapshot = exports.takeSnapshot = function (front) {
|
||||
return function *(dispatch, getState) {
|
||||
let snapshot = createSnapshot();
|
||||
dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
|
||||
dispatch(selectSnapshot(snapshot));
|
||||
|
@ -49,7 +66,7 @@ const takeSnapshot = exports.takeSnapshot = function takeSnapshot (front) {
|
|||
* @param {Snapshot} snapshot,
|
||||
*/
|
||||
const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, snapshot) {
|
||||
return function *(dispatch, getStore) {
|
||||
return function *(dispatch, getState) {
|
||||
// @TODO 1215606
|
||||
assert(snapshot.state === states.SAVED,
|
||||
"Should only read a snapshot once");
|
||||
|
@ -64,29 +81,42 @@ const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, s
|
|||
* @param {HeapAnalysesClient} heapWorker
|
||||
* @param {Snapshot} snapshot,
|
||||
*
|
||||
* @see {Snapshot} model defined in devtools/client/memory/app.js
|
||||
* @see {Snapshot} model defined in devtools/client/memory/models.js
|
||||
* @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
|
||||
* @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
|
||||
*/
|
||||
const takeCensus = exports.takeCensus = function takeCensus (heapWorker, snapshot) {
|
||||
return function *(dispatch, getStore) {
|
||||
const takeCensus = exports.takeCensus = function (heapWorker, snapshot) {
|
||||
return function *(dispatch, getState) {
|
||||
// @TODO 1215606
|
||||
assert([states.READ, states.SAVED_CENSUS].includes(snapshot.state),
|
||||
"Can only take census of snapshots in READ or SAVED_CENSUS state");
|
||||
|
||||
let breakdown = getStore().breakdown;
|
||||
dispatch({ type: actions.TAKE_CENSUS_START, snapshot, breakdown });
|
||||
let census;
|
||||
let breakdown = getState().breakdown;
|
||||
|
||||
let census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, { asTreeNode: true });
|
||||
dispatch({ type: actions.TAKE_CENSUS_END, snapshot, census });
|
||||
// If breakdown hasn't changed, don't do anything
|
||||
if (breakdownEquals(breakdown, snapshot.breakdown)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep taking a census if the breakdown changes during. Recheck
|
||||
// that the breakdown used for the census is the same as
|
||||
// the state's breakdown.
|
||||
do {
|
||||
breakdown = getState().breakdown;
|
||||
dispatch({ type: actions.TAKE_CENSUS_START, snapshot, breakdown });
|
||||
census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, { asTreeNode: true });
|
||||
} while (!breakdownEquals(breakdown, getState().breakdown));
|
||||
|
||||
dispatch({ type: actions.TAKE_CENSUS_END, snapshot, breakdown, census });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Snapshot}
|
||||
* @see {Snapshot} model defined in devtools/client/memory/app.js
|
||||
* @see {Snapshot} model defined in devtools/client/memory/models.js
|
||||
*/
|
||||
const selectSnapshot = exports.selectSnapshot = function takeSnapshot (snapshot) {
|
||||
const selectSnapshot = exports.selectSnapshot = function (snapshot) {
|
||||
return {
|
||||
type: actions.SELECT_SNAPSHOT,
|
||||
snapshot
|
||||
|
|
|
@ -1,59 +1,18 @@
|
|||
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
const { selectSnapshot, takeSnapshotAndCensus } = require("./actions/snapshot");
|
||||
const { snapshotState } = require("./constants");
|
||||
const { selectSnapshotAndRefresh, takeSnapshotAndCensus } = require("./actions/snapshot");
|
||||
const { setBreakdownAndRefresh } = require("./actions/breakdown");
|
||||
const { breakdownNameToSpec, getBreakdownDisplayData } = require("./utils");
|
||||
const Toolbar = createFactory(require("./components/toolbar"));
|
||||
const List = createFactory(require("./components/list"));
|
||||
const SnapshotListItem = createFactory(require("./components/snapshot-list-item"));
|
||||
const HeapView = createFactory(require("./components/heap"));
|
||||
|
||||
const stateModel = {
|
||||
/**
|
||||
* {MemoryFront}
|
||||
* Used to communicate with the platform.
|
||||
*/
|
||||
front: PropTypes.any,
|
||||
|
||||
/**
|
||||
* {HeapAnalysesClient}
|
||||
* Used to communicate with the worker that performs analyses on heaps.
|
||||
*/
|
||||
heapWorker: PropTypes.any,
|
||||
|
||||
/**
|
||||
* The breakdown object DSL describing how we want
|
||||
* the census data to be.
|
||||
* @see `js/src/doc/Debugger/Debugger.Memory.md`
|
||||
*/
|
||||
breakdown: PropTypes.object.isRequired,
|
||||
|
||||
/**
|
||||
* {Array<Snapshot>}
|
||||
* List of references to all snapshots taken
|
||||
*/
|
||||
snapshots: PropTypes.arrayOf(PropTypes.shape({
|
||||
// Unique ID for a snapshot
|
||||
id: PropTypes.number.isRequired,
|
||||
// fs path to where the snapshot is stored; used to
|
||||
// identify the snapshot for HeapAnalysesClient.
|
||||
path: PropTypes.string,
|
||||
// Whether or not this snapshot is currently selected.
|
||||
selected: PropTypes.bool.isRequired,
|
||||
// Whther or not the snapshot has been read into memory.
|
||||
// Only needed to do once.
|
||||
snapshotRead: PropTypes.bool.isRequired,
|
||||
// State the snapshot is in
|
||||
// @see ./constants.js
|
||||
state: PropTypes.oneOf(Object.keys(snapshotState)).isRequired,
|
||||
// Data of a census breakdown
|
||||
census: PropTypes.any,
|
||||
}))
|
||||
};
|
||||
const { app: appModel } = require("./models");
|
||||
|
||||
const App = createClass({
|
||||
displayName: "memory-tool",
|
||||
|
||||
propTypes: stateModel,
|
||||
propTypes: appModel,
|
||||
|
||||
childContextTypes: {
|
||||
front: PropTypes.any,
|
||||
|
@ -75,17 +34,17 @@ const App = createClass({
|
|||
dom.div({ id: "memory-tool" }, [
|
||||
|
||||
Toolbar({
|
||||
buttons: [{
|
||||
className: "take-snapshot",
|
||||
onClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker))
|
||||
}]
|
||||
breakdowns: getBreakdownDisplayData(),
|
||||
onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
|
||||
onBreakdownChange: breakdown =>
|
||||
dispatch(setBreakdownAndRefresh(heapWorker, breakdownNameToSpec(breakdown))),
|
||||
}),
|
||||
|
||||
dom.div({ id: "memory-tool-container" }, [
|
||||
List({
|
||||
itemComponent: SnapshotListItem,
|
||||
items: snapshots,
|
||||
onClick: snapshot => dispatch(selectSnapshot(snapshot))
|
||||
onClick: snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot))
|
||||
}),
|
||||
|
||||
HeapView({
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { getSnapshotStatusText } = require("../utils");
|
||||
const { snapshotState: states } = require("../constants");
|
||||
const { snapshot: snapshotModel } = require("../models");
|
||||
const TAKE_SNAPSHOT_TEXT = "Take snapshot";
|
||||
|
||||
/**
|
||||
|
@ -14,7 +15,7 @@ const Heap = module.exports = createClass({
|
|||
|
||||
propTypes: {
|
||||
onSnapshotClick: PropTypes.func.isRequired,
|
||||
snapshot: PropTypes.any,
|
||||
snapshot: snapshotModel,
|
||||
},
|
||||
|
||||
render() {
|
||||
|
@ -22,7 +23,6 @@ const Heap = module.exports = createClass({
|
|||
let pane;
|
||||
let census = snapshot ? snapshot.census : null;
|
||||
let state = snapshot ? snapshot.state : "initial";
|
||||
let statusText = getSnapshotStatusText(snapshot);
|
||||
|
||||
switch (state) {
|
||||
case "initial":
|
||||
|
@ -35,7 +35,8 @@ const Heap = module.exports = createClass({
|
|||
case states.READING:
|
||||
case states.READ:
|
||||
case states.SAVING_CENSUS:
|
||||
pane = dom.div({ className: "heap-view-panel", "data-state": state }, statusText);
|
||||
pane = dom.div({ className: "heap-view-panel", "data-state": state },
|
||||
getSnapshotStatusText(snapshot));
|
||||
break;
|
||||
case states.SAVED_CENSUS:
|
||||
pane = dom.div({ className: "heap-view-panel", "data-state": "loaded" }, JSON.stringify(census || {}));
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { getSnapshotStatusText } = require("../utils");
|
||||
const { snapshot: snapshotModel } = require("../models");
|
||||
|
||||
const SnapshotListItem = module.exports = createClass({
|
||||
displayName: "snapshot-list-item",
|
||||
|
||||
propTypes: {
|
||||
onClick: PropTypes.func,
|
||||
item: PropTypes.any.isRequired,
|
||||
item: snapshotModel.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
},
|
||||
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
const { DOM, createClass } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
|
||||
const Toolbar = module.exports = createClass({
|
||||
displayName: "toolbar",
|
||||
propTypes: {
|
||||
breakdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
onTakeSnapshotClick: PropTypes.func.isRequired,
|
||||
onBreakdownChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
let buttons = this.props.buttons;
|
||||
let { onTakeSnapshotClick, onBreakdownChange, breakdowns } = this.props;
|
||||
return (
|
||||
DOM.div({ className: "devtools-toolbar" }, ...buttons.map(spec => {
|
||||
return DOM.button(Object.assign({}, spec, {
|
||||
className: `${spec.className || "" } devtools-button`
|
||||
}));
|
||||
}))
|
||||
DOM.div({ className: "devtools-toolbar" }, [
|
||||
DOM.button({ className: `take-snapshot devtools-button`, onClick: onTakeSnapshotClick }),
|
||||
DOM.select({
|
||||
className: `select-breakdown`,
|
||||
onChange: e => onBreakdownChange(e.target.value),
|
||||
}, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName)))
|
||||
])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,6 +21,39 @@ actions.TAKE_CENSUS_END = "take-census-end";
|
|||
// Fired by UI to select a snapshot to view.
|
||||
actions.SELECT_SNAPSHOT = "select-snapshot";
|
||||
|
||||
const COUNT = { by: "count", count: true, bytes: true };
|
||||
const INTERNAL_TYPE = { by: "internalType", then: COUNT };
|
||||
const ALLOCATION_STACK = { by: "allocationStack", then: COUNT, noStack: COUNT };
|
||||
const OBJECT_CLASS = { by: "objectClass", then: COUNT, other: COUNT };
|
||||
|
||||
const breakdowns = exports.breakdowns = {
|
||||
coarseType: {
|
||||
displayName: "Coarse Type",
|
||||
breakdown: {
|
||||
by: "coarseType",
|
||||
objects: ALLOCATION_STACK,
|
||||
strings: ALLOCATION_STACK,
|
||||
scripts: INTERNAL_TYPE,
|
||||
other: INTERNAL_TYPE,
|
||||
}
|
||||
},
|
||||
|
||||
allocationStack: {
|
||||
displayName: "Allocation Site",
|
||||
breakdown: ALLOCATION_STACK,
|
||||
},
|
||||
|
||||
objectClass: {
|
||||
displayName: "Object Class",
|
||||
breakdown: OBJECT_CLASS,
|
||||
},
|
||||
|
||||
internalType: {
|
||||
displayName: "Internal Type",
|
||||
breakdown: INTERNAL_TYPE,
|
||||
},
|
||||
};
|
||||
|
||||
const snapshotState = exports.snapshotState = {};
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
const { MemoryFront } = require("devtools/server/actors/memory");
|
||||
const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
|
||||
const { PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { snapshotState: states } = require("./constants");
|
||||
|
||||
/**
|
||||
* The breakdown object DSL describing how we want
|
||||
* the census data to be.
|
||||
* @see `js/src/doc/Debugger/Debugger.Memory.md`
|
||||
*/
|
||||
let breakdownModel = exports.breakdown = PropTypes.shape({
|
||||
by: PropTypes.oneOf(["coarseType", "allocationStack", "objectClass", "internalType"]).isRequired,
|
||||
});
|
||||
|
||||
/**
|
||||
* Snapshot model.
|
||||
*/
|
||||
let snapshotModel = exports.snapshot = PropTypes.shape({
|
||||
// Unique ID for a snapshot
|
||||
id: PropTypes.number.isRequired,
|
||||
// Whether or not this snapshot is currently selected.
|
||||
selected: PropTypes.bool.isRequired,
|
||||
// fs path to where the snapshot is stored; used to
|
||||
// identify the snapshot for HeapAnalysesClient.
|
||||
path: PropTypes.string,
|
||||
// Data of a census breakdown
|
||||
census: PropTypes.object,
|
||||
// The breakdown used to generate the current census
|
||||
breakdown: breakdownModel,
|
||||
// State the snapshot is in
|
||||
// @see ./constants.js
|
||||
state: function (props, propName) {
|
||||
let stateNames = Object.keys(states);
|
||||
let current = props.state;
|
||||
let shouldHavePath = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
|
||||
let shouldHaveCensus = [states.SAVED_CENSUS];
|
||||
|
||||
if (!stateNames.contains(current)) {
|
||||
throw new Error(`Snapshot state must be one of ${stateNames}.`);
|
||||
}
|
||||
if (shouldHavePath.contains(current) && !path) {
|
||||
throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
|
||||
}
|
||||
if (shouldHaveCensus.contains(current) && (!props.census || !props.breakdown)) {
|
||||
throw new Error(`Snapshots in state ${current} must have a census and breakdown.`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let appModel = exports.app = {
|
||||
// {MemoryFront} Used to communicate with platform
|
||||
front: PropTypes.instanceOf(MemoryFront),
|
||||
// {HeapAnalysesClient} Used to interface with snapshots
|
||||
heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
|
||||
// The breakdown object DSL describing how we want
|
||||
// the census data to be.
|
||||
// @see `js/src/doc/Debugger/Debugger.Memory.md`
|
||||
breakdown: breakdownModel.isRequired,
|
||||
// List of reference to all snapshots taken
|
||||
snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
|
||||
};
|
|
@ -14,6 +14,7 @@ DevToolsModules(
|
|||
'app.js',
|
||||
'constants.js',
|
||||
'initializer.js',
|
||||
'models.js',
|
||||
'panel.js',
|
||||
'reducers.js',
|
||||
'store.js',
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
const { actions } = require("../constants");
|
||||
const { actions, breakdowns } = require("../constants");
|
||||
const DEFAULT_BREAKDOWN = breakdowns.coarseType.breakdown;
|
||||
|
||||
// Hardcoded breakdown for now
|
||||
const DEFAULT_BREAKDOWN = {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true }
|
||||
let handlers = Object.create(null);
|
||||
|
||||
handlers[actions.SET_BREAKDOWN] = function (_, action) {
|
||||
return Object.assign({}, action.breakdown);
|
||||
};
|
||||
|
||||
/**
|
||||
* Not much to do here yet until we can change breakdowns,
|
||||
* but this gets it in our store.
|
||||
*/
|
||||
module.exports = function (state=DEFAULT_BREAKDOWN, action) {
|
||||
return Object.assign({}, DEFAULT_BREAKDOWN);
|
||||
let handle = handlers[action.type];
|
||||
if (handle) {
|
||||
return handle(state, action);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ handlers[actions.TAKE_CENSUS_START] = function (snapshots, action) {
|
|||
let snapshot = getSnapshot(snapshots, action.snapshot);
|
||||
snapshot.state = states.SAVING_CENSUS;
|
||||
snapshot.census = null;
|
||||
snapshot.breakdown = action.breakdown;
|
||||
return [...snapshots];
|
||||
};
|
||||
|
||||
|
@ -40,6 +41,7 @@ handlers[actions.TAKE_CENSUS_END] = function (snapshots, action) {
|
|||
let snapshot = getSnapshot(snapshots, action.snapshot);
|
||||
snapshot.state = states.SAVED_CENSUS;
|
||||
snapshot.census = action.census;
|
||||
snapshot.breakdown = action.breakdown;
|
||||
return [...snapshots];
|
||||
};
|
||||
|
||||
|
|
|
@ -59,3 +59,32 @@ function waitUntilState (store, predicate) {
|
|||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function waitUntilSnapshotState (store, expected) {
|
||||
let predicate = () => {
|
||||
let snapshots = store.getState().snapshots;
|
||||
do_print(snapshots.map(x => x.state));
|
||||
return snapshots.length === expected.length &&
|
||||
expected.every((state, i) => state === "*" || snapshots[i].state === state);
|
||||
};
|
||||
do_print(`Waiting for snapshots to be of state: ${expected}`);
|
||||
return waitUntilState(store, predicate);
|
||||
}
|
||||
|
||||
function isBreakdownType (census, type) {
|
||||
// Little sanity check, all censuses should have atleast a children array
|
||||
if (!census || !Array.isArray(census.children)) {
|
||||
return false;
|
||||
}
|
||||
switch (type) {
|
||||
case "coarseType":
|
||||
return census.children.find(c => c.name === "objects");
|
||||
case "objectClass":
|
||||
return census.children.find(c => c.name === "Function");
|
||||
case "internalType":
|
||||
return census.children.find(c => c.name === "js::BaseShape") &&
|
||||
!census.children.find(c => c.name === "objects");
|
||||
default:
|
||||
throw new Error(`isBreakdownType does not yet support ${type}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests the task creator `setBreakdownAndRefreshAndRefresh()` for breakdown changing.
|
||||
* We test this rather than `setBreakdownAndRefresh` directly, as we use the refresh action
|
||||
* in the app itself composed from `setBreakdownAndRefresh`
|
||||
*/
|
||||
|
||||
let { breakdowns, snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { breakdownEquals } = require("devtools/client/memory/utils");
|
||||
let { setBreakdownAndRefresh } = require("devtools/client/memory/actions/breakdown");
|
||||
let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function *() {
|
||||
let front = new StubbedMemoryFront();
|
||||
let heapWorker = new HeapAnalysesClient();
|
||||
yield front.attach();
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
// Test default breakdown with no snapshots
|
||||
equal(getState().breakdown.by, "coarseType", "default coarseType breakdown selected at start.");
|
||||
dispatch(setBreakdownAndRefresh(heapWorker, breakdowns.objectClass.breakdown));
|
||||
equal(getState().breakdown.by, "objectClass", "breakdown changed with no snapshots");
|
||||
|
||||
// Test invalid breakdowns
|
||||
ok(getState().errors.length === 0, "No error actions in the queue.");
|
||||
dispatch(setBreakdownAndRefresh(heapWorker, {}));
|
||||
yield waitUntilState(store, () => getState().errors.length === 1);
|
||||
ok(true, "Emits an error action when passing in an invalid breakdown object");
|
||||
|
||||
equal(getState().breakdown.by, "objectClass",
|
||||
"current breakdown unchanged when passing invalid breakdown");
|
||||
|
||||
// Test new snapshots
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
ok(isBreakdownType(getState().snapshots[0].census, "objectClass"),
|
||||
"New snapshots use the current, non-default breakdown");
|
||||
|
||||
|
||||
// Updates when changing breakdown during `SAVING`
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING]);
|
||||
dispatch(setBreakdownAndRefresh(heapWorker, breakdowns.coarseType.breakdown));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
|
||||
ok(isBreakdownType(getState().snapshots[1].census, "coarseType"),
|
||||
"Breakdown can be changed while saving snapshots, uses updated breakdown in census");
|
||||
|
||||
|
||||
// Updates when changing breakdown during `SAVING_CENSUS`
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVING_CENSUS]);
|
||||
dispatch(setBreakdownAndRefresh(heapWorker, breakdowns.objectClass.breakdown));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
|
||||
ok(breakdownEquals(getState().snapshots[2].breakdown, breakdowns.objectClass.breakdown),
|
||||
"Breakdown can be changed while saving census, stores updated breakdown in snapshot");
|
||||
ok(isBreakdownType(getState().snapshots[2].census, "objectClass"),
|
||||
"Breakdown can be changed while saving census, uses updated breakdown in census");
|
||||
|
||||
// Updates census on currently selected snapshot when changing breakdown
|
||||
ok(getState().snapshots[2].selected, "Third snapshot currently selected");
|
||||
dispatch(setBreakdownAndRefresh(heapWorker, breakdowns.internalType.breakdown));
|
||||
yield waitUntilState(store, () => isBreakdownType(getState().snapshots[2].census, "internalType"));
|
||||
ok(isBreakdownType(getState().snapshots[2].census, "internalType"),
|
||||
"Snapshot census updated when changing breakdowns after already generating one census");
|
||||
|
||||
// Does not update unselected censuses
|
||||
ok(!getState().snapshots[1].selected, "Second snapshot unselected currently");
|
||||
ok(breakdownEquals(getState().snapshots[1].breakdown, breakdowns.coarseType.breakdown),
|
||||
"Second snapshot using `coarseType` breakdown still and not yet updated to correct breakdown");
|
||||
ok(isBreakdownType(getState().snapshots[1].census, "coarseType"),
|
||||
"Second snapshot using `coarseType` still for census and not yet updated to correct breakdown");
|
||||
|
||||
// Updates to current breakdown when switching to stale snapshot
|
||||
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1]));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING_CENSUS, states.SAVED_CENSUS]);
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
|
||||
|
||||
ok(getState().snapshots[1].selected, "Second snapshot selected currently");
|
||||
ok(breakdownEquals(getState().snapshots[1].breakdown, breakdowns.internalType.breakdown),
|
||||
"Second snapshot using `internalType` breakdown and updated to correct breakdown");
|
||||
ok(isBreakdownType(getState().snapshots[1].census, "internalType"),
|
||||
"Second snapshot using `internalType` for census and updated to correct breakdown");
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests the task creator `setBreakdownAndRefreshAndRefresh()` for custom
|
||||
* breakdowns.
|
||||
*/
|
||||
|
||||
let { snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { breakdownEquals } = require("devtools/client/memory/utils");
|
||||
let { setBreakdownAndRefresh } = require("devtools/client/memory/actions/breakdown");
|
||||
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
let custom = { by: "internalType", then: { by: "count", bytes: true }};
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function *() {
|
||||
let front = new StubbedMemoryFront();
|
||||
let heapWorker = new HeapAnalysesClient();
|
||||
yield front.attach();
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
dispatch(setBreakdownAndRefresh(heapWorker, custom));
|
||||
ok(breakdownEquals(getState().breakdown, custom),
|
||||
"Custom breakdown stored in breakdown state.");
|
||||
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
|
||||
ok(breakdownEquals(getState().snapshots[0].breakdown, custom),
|
||||
"New snapshot stored custom breakdown when done taking census");
|
||||
ok(getState().snapshots[0].census.children.length, "Census has some children");
|
||||
// Ensure we don't have `count` in any results
|
||||
ok(getState().snapshots[0].census.children.every(c => !c.count), "Census used custom breakdown");
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests the action creator `setBreakdown()` for breakdown changing.
|
||||
* Does not test refreshing the census information, check `setBreakdownAndRefresh` action
|
||||
* for that.
|
||||
*/
|
||||
|
||||
let { breakdowns, snapshotState: states } = require("devtools/client/memory/constants");
|
||||
let { setBreakdown } = require("devtools/client/memory/actions/breakdown");
|
||||
let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function *() {
|
||||
let front = new StubbedMemoryFront();
|
||||
let heapWorker = new HeapAnalysesClient();
|
||||
yield front.attach();
|
||||
let store = Store();
|
||||
let { getState, dispatch } = store;
|
||||
|
||||
// Test default breakdown with no snapshots
|
||||
equal(getState().breakdown.by, "coarseType", "default coarseType breakdown selected at start.");
|
||||
dispatch(setBreakdown(breakdowns.objectClass.breakdown));
|
||||
equal(getState().breakdown.by, "objectClass", "breakdown changed with no snapshots");
|
||||
|
||||
// Test invalid breakdowns
|
||||
try {
|
||||
dispatch(setBreakdown({}));
|
||||
ok(false, "Throws when passing in an invalid breakdown object");
|
||||
} catch (e) {
|
||||
ok(true, "Throws when passing in an invalid breakdown object");
|
||||
}
|
||||
equal(getState().breakdown.by, "objectClass",
|
||||
"current breakdown unchanged when passing invalid breakdown");
|
||||
|
||||
// Test new snapshots
|
||||
dispatch(takeSnapshotAndCensus(front, heapWorker));
|
||||
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
|
||||
ok(isBreakdownType(getState().snapshots[0].census, "objectClass"),
|
||||
"New snapshots use the current, non-default breakdown");
|
||||
});
|
|
@ -5,7 +5,8 @@
|
|||
* Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)`
|
||||
*/
|
||||
|
||||
var { snapshotState: states } = require("devtools/client/memory/constants");
|
||||
var { snapshotState: states, breakdowns } = require("devtools/client/memory/constants");
|
||||
var { breakdownEquals } = require("devtools/client/memory/utils");
|
||||
var { ERROR_TYPE } = require("devtools/client/shared/redux/middleware/task");
|
||||
var actions = require("devtools/client/memory/actions/snapshot");
|
||||
|
||||
|
@ -43,7 +44,9 @@ add_task(function *() {
|
|||
|
||||
snapshot = store.getState().snapshots[0];
|
||||
ok(snapshot.census, "Snapshot has census after saved census");
|
||||
ok(snapshot.census.children.length, "Census is in tree node form with the default breakdown");
|
||||
ok(snapshot.census.children.find(t => t.name === "JSObject"),
|
||||
ok(snapshot.census.children.length, "Census is in tree node form");
|
||||
ok(isBreakdownType(snapshot.census, "coarseType"),
|
||||
"Census is in tree node form with the default breakdown");
|
||||
ok(breakdownEquals(snapshot.breakdown, breakdowns.coarseType.breakdown),
|
||||
"Snapshot stored correct breakdown used for the census");
|
||||
});
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests the task creator `takeSnapshotAndCensus()` for the whole flow of
|
||||
* taking a snapshot, and its sub-actions.
|
||||
*/
|
||||
|
||||
let utils = require("devtools/client/memory/utils");
|
||||
let { snapshotState: states, breakdowns } = require("devtools/client/memory/constants");
|
||||
let { Preferences } = require("resource://gre/modules/Preferences.jsm");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function *() {
|
||||
ok(utils.breakdownEquals(breakdowns.allocationStack.breakdown, {
|
||||
by: "allocationStack",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
noStack: { by: "count", count: true, bytes: true },
|
||||
}), "utils.breakdownEquals() passes with preset"),
|
||||
|
||||
ok(!utils.breakdownEquals(breakdowns.allocationStack.breakdown, {
|
||||
by: "allocationStack",
|
||||
then: { by: "count", count: false, bytes: true },
|
||||
noStack: { by: "count", count: true, bytes: true },
|
||||
}), "utils.breakdownEquals() fails when deep properties do not match");
|
||||
|
||||
ok(!utils.breakdownEquals(breakdowns.allocationStack.breakdown, {
|
||||
by: "allocationStack",
|
||||
then: { by: "count", bytes: true },
|
||||
noStack: { by: "count", count: true, bytes: true },
|
||||
}), "utils.breakdownEquals() fails when deep properties are missing.");
|
||||
|
||||
let s1 = utils.createSnapshot();
|
||||
let s2 = utils.createSnapshot();
|
||||
ok(s1.state, states.SAVING, "utils.createSnapshot() creates snapshot in saving state");
|
||||
ok(s1.id !== s2.id, "utils.createSnapshot() creates snapshot with unique ids");
|
||||
|
||||
ok(utils.breakdownEquals(utils.breakdownNameToSpec("coarseType"), breakdowns.coarseType.breakdown),
|
||||
"utils.breakdownNameToSpec() works for presets");
|
||||
ok(utils.breakdownEquals(utils.breakdownNameToSpec("coarseType"), breakdowns.coarseType.breakdown),
|
||||
"utils.breakdownNameToSpec() works for presets");
|
||||
|
||||
let custom = { by: "internalType", then: { by: "count", bytes: true }};
|
||||
Preferences.set("devtools.memory.custom-breakdowns", JSON.stringify({ "My Breakdown": custom }));
|
||||
|
||||
ok(utils.breakdownEquals(utils.getCustomBreakdowns()["My Breakdown"], custom),
|
||||
"utils.getCustomBreakdowns() returns custom breakdowns");
|
||||
});
|
|
@ -6,6 +6,10 @@ firefox-appdir = browser
|
|||
skip-if = toolkit == 'android' || toolkit == 'gonk'
|
||||
|
||||
[test_action-select-snapshot.js]
|
||||
[test_action-set-breakdown.js]
|
||||
[test_action-set-breakdown-and-refresh-01.js]
|
||||
[test_action-set-breakdown-and-refresh-02.js]
|
||||
[test_action-take-census.js]
|
||||
[test_action-take-snapshot.js]
|
||||
[test_action-take-snapshot-and-census.js]
|
||||
[test_utils.js]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const { Preferences } = require("resource://gre/modules/Preferences.jsm");
|
||||
const CUSTOM_BREAKDOWN_PREF = "devtools.memory.custom-breakdowns";
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
const { snapshotState: states } = require("./constants");
|
||||
const { snapshotState: states, breakdowns } = require("./constants");
|
||||
const SAVING_SNAPSHOT_TEXT = "Saving snapshot...";
|
||||
const READING_SNAPSHOT_TEXT = "Reading snapshot...";
|
||||
const SAVING_CENSUS_TEXT = "Taking heap census...";
|
||||
|
@ -14,6 +16,78 @@ exports.assert = function (condition, message) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of objects with the unique key `name`
|
||||
* and `displayName` for each breakdown.
|
||||
*
|
||||
* @return {Object{name, displayName}}
|
||||
*/
|
||||
exports.getBreakdownDisplayData = function () {
|
||||
return exports.getBreakdownNames().map(name => {
|
||||
// If it's a preset use the display name value
|
||||
let preset = breakdowns[name];
|
||||
let displayName = name;
|
||||
if (preset && preset.displayName) {
|
||||
displayName = preset.displayName;
|
||||
}
|
||||
return { name, displayName };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of the unique names for each breakdown in
|
||||
* presets and custom pref.
|
||||
*
|
||||
* @return {Array<Breakdown>}
|
||||
*/
|
||||
exports.getBreakdownNames = function () {
|
||||
let custom = exports.getCustomBreakdowns();
|
||||
return Object.keys(Object.assign({}, breakdowns, custom));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns custom breakdowns defined in `devtools.memory.custom-breakdowns` pref.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
exports.getCustomBreakdowns = function () {
|
||||
let customBreakdowns = Object.create(null);
|
||||
try {
|
||||
customBreakdowns = JSON.parse(Preferences.get(CUSTOM_BREAKDOWN_PREF)) || Object.create(null);
|
||||
} catch (e) {
|
||||
DevToolsUtils.reportException(
|
||||
`String stored in "${CUSTOM_BREAKDOWN_PREF}" pref cannot be parsed by \`JSON.parse()\`.`);
|
||||
}
|
||||
return customBreakdowns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a breakdown preset name, like "allocationStack", and returns the
|
||||
* spec for the breakdown. Also checks properties of keys in the `devtools.memory.custom-breakdowns`
|
||||
* pref. If not found, returns an empty object.
|
||||
*
|
||||
* @param {String} name
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
exports.breakdownNameToSpec = function (name) {
|
||||
let customBreakdowns = exports.getCustomBreakdowns();
|
||||
|
||||
// If breakdown is already a breakdown, use it
|
||||
if (typeof name === "object") {
|
||||
return name;
|
||||
}
|
||||
// If it's in our custom breakdowns, use it
|
||||
else if (name in customBreakdowns) {
|
||||
return customBreakdowns[name];
|
||||
}
|
||||
// If breakdown name is in our presets, use that
|
||||
else if (name in breakdowns) {
|
||||
return breakdowns[name].breakdown;
|
||||
}
|
||||
return Object.create(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a string representing a readable form of the snapshot's state.
|
||||
*
|
||||
|
@ -21,16 +95,26 @@ exports.assert = function (condition, message) {
|
|||
* @return {String}
|
||||
*/
|
||||
exports.getSnapshotStatusText = function (snapshot) {
|
||||
switch (snapshot && snapshot.state) {
|
||||
exports.assert((snapshot || {}).state,
|
||||
`Snapshot must have expected state, found ${(snapshot || {}).state}.`);
|
||||
|
||||
switch (snapshot.state) {
|
||||
case states.SAVING:
|
||||
return SAVING_SNAPSHOT_TEXT;
|
||||
case states.SAVED:
|
||||
case states.READING:
|
||||
return READING_SNAPSHOT_TEXT;
|
||||
case states.READ:
|
||||
case states.SAVING_CENSUS:
|
||||
return SAVING_CENSUS_TEXT;
|
||||
// If it's read, it shouldn't have any label, as we could've cleared the
|
||||
// census cache by changing the breakdown, and we should lazily
|
||||
// go to SAVING_CENSUS. If it's SAVED_CENSUS, we have no status to display.
|
||||
case states.READ:
|
||||
case states.SAVED_CENSUS:
|
||||
return "";
|
||||
}
|
||||
|
||||
DevToolsUtils.reportException(`Snapshot in unexpected state: ${snapshot.state}`);
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -66,3 +150,43 @@ exports.createSnapshot = function createSnapshot () {
|
|||
path: null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes two objects and compares them deeply, returning
|
||||
* a boolean indicating if they're equal or not. Used for breakdown
|
||||
* comparison.
|
||||
*
|
||||
* @param {Any} obj1
|
||||
* @param {Any} obj2
|
||||
* @return {Boolean}
|
||||
*/
|
||||
exports.breakdownEquals = function (obj1, obj2) {
|
||||
let type1 = typeof obj1;
|
||||
let type2 = typeof obj2;
|
||||
|
||||
// Quick checks
|
||||
if (type1 !== type2 || (Array.isArray(obj1) !== Array.isArray(obj2))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj1 === obj2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj1)) {
|
||||
if (obj1.length !== obj2.length) { return false; }
|
||||
return obj1.every((_, i) => exports.breakdownEquals(obj[1], obj2[i]));
|
||||
}
|
||||
else if (type1 === "object") {
|
||||
let k1 = Object.keys(obj1);
|
||||
let k2 = Object.keys(obj2);
|
||||
|
||||
if (k1.length !== k2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return k1.every(k => exports.breakdownEquals(obj1[k], obj2[k]));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -103,6 +103,8 @@ pref("devtools.debugger.ui.variables-searchbox-visible", false);
|
|||
// Enable the Memory tools
|
||||
pref("devtools.memory.enabled", false);
|
||||
|
||||
pref("devtools.memory.custom-breakdowns", "{}");
|
||||
|
||||
// Enable the Performance tools
|
||||
pref("devtools.performance.enabled", true);
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ CensusTreeNodeBreakdowns.internalType = function (node, breakdown, report) {
|
|||
for (let key of Object.keys(report)) {
|
||||
node.children.push(new CensusTreeNode(breakdown.then, report[key], key));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CensusTreeNodeBreakdowns.objectClass = function (node, breakdown, report) {
|
||||
node.children = [];
|
||||
|
@ -71,14 +71,18 @@ CensusTreeNodeBreakdowns.objectClass = function (node, breakdown, report) {
|
|||
let bd = key === "other" ? breakdown.other : breakdown.then;
|
||||
node.children.push(new CensusTreeNode(bd, report[key], key));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CensusTreeNodeBreakdowns.coarseType = function (node, breakdown, report) {
|
||||
node.children = [];
|
||||
for (let type of Object.keys(breakdown).filter(type => COARSE_TYPES.has(type))) {
|
||||
node.children.push(new CensusTreeNode(breakdown[type], report[type], type));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CensusTreeNodeBreakdowns.allocationStack = function (node, breakdown, report) {
|
||||
node.children = [];
|
||||
};
|
||||
|
||||
function sortByBytes (a, b) {
|
||||
return (b.bytes || 0) - (a.bytes || 0);
|
||||
|
|
|
@ -285,10 +285,15 @@ MaybeInvalidTabContext::MaybeInvalidTabContext(const IPCTabContext& aParams)
|
|||
}
|
||||
}
|
||||
|
||||
nsCOMPtr<mozIApplication> ownApp = GetAppForId(originAttributes.mAppId);
|
||||
if ((ownApp == nullptr) != (originAttributes.mAppId == NO_APP_ID)) {
|
||||
mInvalidReason = "Got an ownAppId that didn't correspond to an app.";
|
||||
return;
|
||||
nsCOMPtr<mozIApplication> ownApp;
|
||||
if (!originAttributes.mInBrowser) {
|
||||
// mAppId corresponds to OwnOrContainingAppId; if mInBrowser is
|
||||
// false then it's ownApp otherwise it's containingApp
|
||||
ownApp = GetAppForId(originAttributes.mAppId);
|
||||
if ((ownApp == nullptr) != (originAttributes.mAppId == NO_APP_ID)) {
|
||||
mInvalidReason = "Got an ownAppId that didn't correspond to an app.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nsCOMPtr<mozIApplication> containingApp = GetAppForId(containingAppId);
|
||||
|
|
|
@ -3362,7 +3362,12 @@ public class BrowserApp extends GeckoApp
|
|||
|
||||
// Track the menu action. We don't know much about the context, but we can use this to determine
|
||||
// the frequency of use for various actions.
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, getResources().getResourceEntryName(itemId));
|
||||
String extras = getResources().getResourceEntryName(itemId);
|
||||
if (TextUtils.equals(extras, "new_private_tab")) {
|
||||
// Mask private browsing
|
||||
extras = "new_tab";
|
||||
}
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
|
||||
|
||||
mBrowserToolbar.cancelEdit();
|
||||
|
||||
|
|
|
@ -25,10 +25,12 @@ import android.text.TextUtils;
|
|||
import android.util.Log;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class IntentHelper implements GeckoEventListener,
|
||||
NativeEventListener {
|
||||
|
@ -50,6 +52,8 @@ public final class IntentHelper implements GeckoEventListener,
|
|||
private static String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url";
|
||||
|
||||
/** A partial URI to an error page - the encoded error URI should be appended before loading. */
|
||||
private static final String GENERIC_URI_PREFIX = "about:neterror?e=generic&u=";
|
||||
private static final String MALFORMED_URI_PREFIX = "about:neterror?e=malformedURI&u=";
|
||||
private static String UNKNOWN_PROTOCOL_URI_PREFIX = "about:neterror?e=unknownProtocolFound&u=";
|
||||
|
||||
private static IntentHelper instance;
|
||||
|
@ -184,7 +188,22 @@ public final class IntentHelper implements GeckoEventListener,
|
|||
// https://developer.chrome.com/multidevice/android/intents
|
||||
if (intent.hasExtra(EXTRA_BROWSER_FALLBACK_URL)) {
|
||||
final String fallbackUrl = intent.getStringExtra(EXTRA_BROWSER_FALLBACK_URL);
|
||||
callback.sendError(fallbackUrl);
|
||||
String urlToLoad;
|
||||
try {
|
||||
final String anyCaseScheme = new URI(fallbackUrl).getScheme();
|
||||
final String scheme = (anyCaseScheme == null) ? null : anyCaseScheme.toLowerCase(Locale.US);
|
||||
if ("http".equals(scheme) || "https".equals(scheme)) {
|
||||
urlToLoad = fallbackUrl;
|
||||
} else {
|
||||
Log.w(LOGTAG, "Fallback URI uses unsupported scheme: " + scheme);
|
||||
urlToLoad = GENERIC_URI_PREFIX + fallbackUrl;
|
||||
}
|
||||
} catch (final URISyntaxException e) {
|
||||
// Do not include Exception to avoid leaking uris.
|
||||
Log.w(LOGTAG, "Exception parsing fallback URI");
|
||||
urlToLoad = MALFORMED_URI_PREFIX + fallbackUrl;
|
||||
}
|
||||
callback.sendError(urlToLoad);
|
||||
|
||||
} else if (intent.getPackage() != null) {
|
||||
// Note on alternative flows: we could get the intent package from a component, however, for
|
||||
|
|
|
@ -36,6 +36,7 @@ import android.content.Intent;
|
|||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
|
@ -179,7 +180,12 @@ public abstract class HomeFragment extends Fragment {
|
|||
|
||||
// Track the menu action. We don't know much about the context, but we can use this to determine
|
||||
// the frequency of use for various actions.
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, getResources().getResourceEntryName(itemId));
|
||||
String extras = getResources().getResourceEntryName(itemId);
|
||||
if (TextUtils.equals(extras, "home_open_private_tab")) {
|
||||
// Mask private browsing
|
||||
extras = "home_open_new_tab";
|
||||
}
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, extras);
|
||||
|
||||
if (itemId == R.id.home_copyurl) {
|
||||
if (info.url == null) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.mozilla.gecko.widget.GeckoPopupMenu;
|
|||
import org.mozilla.gecko.widget.IconTabWidget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
|
@ -74,7 +75,9 @@ public class TabsPanel extends LinearLayout
|
|||
|
||||
|
||||
public static View createTabsLayout(final Context context, final AttributeSet attrs) {
|
||||
if (HardwareUtils.isTablet() || AppConstants.NIGHTLY_BUILD) {
|
||||
final boolean isLandscape = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
|
||||
if (HardwareUtils.isTablet() || isLandscape) {
|
||||
return new TabsGridLayout(context, attrs);
|
||||
} else {
|
||||
return new TabsListLayout(context, attrs);
|
||||
|
@ -189,11 +192,11 @@ public class TabsPanel extends LinearLayout
|
|||
}
|
||||
|
||||
private void addTab() {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_tab");
|
||||
|
||||
if (mCurrentPanel == Panel.NORMAL_TABS) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_tab");
|
||||
mActivity.addTab();
|
||||
} else {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_private_tab");
|
||||
mActivity.addPrivateTab();
|
||||
}
|
||||
|
||||
|
@ -215,8 +218,7 @@ public class TabsPanel extends LinearLayout
|
|||
|
||||
if (itemId == R.id.close_all_tabs) {
|
||||
if (mCurrentPanel == Panel.NORMAL_TABS) {
|
||||
final String extras = getResources().getResourceEntryName(itemId);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "close_all_tabs");
|
||||
|
||||
// Disable the menu button so that the menu won't interfere with the tab close animation.
|
||||
mMenuButton.setEnabled(false);
|
||||
|
@ -229,8 +231,8 @@ public class TabsPanel extends LinearLayout
|
|||
|
||||
if (itemId == R.id.close_private_tabs) {
|
||||
if (mCurrentPanel == Panel.PRIVATE_TABS) {
|
||||
final String extras = getResources().getResourceEntryName(itemId);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
|
||||
// Mask private browsing
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "close_all_tabs");
|
||||
|
||||
((CloseAllPanelView) mPanelPrivate).closeAll();
|
||||
} else {
|
||||
|
|
|
@ -52,6 +52,9 @@ public final class GeckoJarReader {
|
|||
inputStream = getStream(zip, jarUrls, url);
|
||||
if (inputStream != null) {
|
||||
bitmap = new BitmapDrawable(resources, inputStream);
|
||||
// BitmapDrawable created from a stream does not set the correct target density from resources.
|
||||
// In fact it discards the resources https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/graphics/java/android/graphics/drawable/BitmapDrawable.java#191
|
||||
bitmap.setTargetDensity(resources.getDisplayMetrics());
|
||||
}
|
||||
} catch (IOException | URISyntaxException ex) {
|
||||
Log.e(LOGTAG, "Exception ", ex);
|
||||
|
|
|
@ -696,7 +696,7 @@ var BrowserApp = {
|
|||
NativeWindow.contextmenus.add(stringGetter("contextmenu.openInPrivateTab"),
|
||||
NativeWindow.contextmenus.linkOpenableContext,
|
||||
function (aTarget) {
|
||||
UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_private_tab");
|
||||
UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
|
||||
UITelemetry.addEvent("loadurl.1", "contextmenu", null);
|
||||
|
||||
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
|
||||
|
@ -1805,8 +1805,8 @@ var BrowserApp = {
|
|||
PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
|
||||
} else {
|
||||
Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
|
||||
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
|
||||
}
|
||||
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
|
||||
} else {
|
||||
// Remove the current host from the 'trackingprotection' consumer
|
||||
// of the permission manager. This effectively removes this host
|
||||
|
@ -1815,8 +1815,8 @@ var BrowserApp = {
|
|||
PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
|
||||
} else {
|
||||
Services.perms.remove(normalizedUrl, "trackingprotection");
|
||||
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
|
||||
}
|
||||
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6549,7 +6549,7 @@ var IdentityHandler = {
|
|||
|
||||
getTrackingMode: function getTrackingMode(aState, aBrowser) {
|
||||
if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
|
||||
Telemetry.addData("TRACKING_PROTECTION_SHIELD", 2);
|
||||
this.shieldHistogramAdd(aBrowser, 2);
|
||||
return this.TRACKING_MODE_CONTENT_BLOCKED;
|
||||
}
|
||||
|
||||
|
@ -6559,14 +6559,21 @@ var IdentityHandler = {
|
|||
PrivateBrowsingUtils.isBrowserPrivate(aBrowser));
|
||||
|
||||
if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) && tpEnabled) {
|
||||
Telemetry.addData("TRACKING_PROTECTION_SHIELD", 1);
|
||||
this.shieldHistogramAdd(aBrowser, 1);
|
||||
return this.TRACKING_MODE_CONTENT_LOADED;
|
||||
}
|
||||
|
||||
Telemetry.addData("TRACKING_PROTECTION_SHIELD", 0);
|
||||
this.shieldHistogramAdd(aBrowser, 0);
|
||||
return this.TRACKING_MODE_UNKNOWN;
|
||||
},
|
||||
|
||||
shieldHistogramAdd: function(browser, value) {
|
||||
if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
|
||||
return;
|
||||
}
|
||||
Telemetry.addData("TRACKING_PROTECTION_SHIELD", value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine the identity of the page being displayed by examining its SSL cert
|
||||
* (if available). Return the data needed to update the UI.
|
||||
|
|
|
@ -3268,9 +3268,9 @@ pref("ui.osk.enabled", true);
|
|||
pref("ui.osk.detect_physical_keyboard", true);
|
||||
// Path to TabTip.exe on local machine. Cached for performance reasons.
|
||||
pref("ui.osk.on_screen_keyboard_path", "");
|
||||
// Only show the on-screen keyboard when Windows is in Tablet mode. Setting
|
||||
// this pref to false will allow the OSK to show in regular non-tablet mode.
|
||||
pref("ui.osk.require_tablet_mode", true);
|
||||
// Only try to show the on-screen keyboard on Windows 10 and later. Setting
|
||||
// this pref to false will allow the OSK to show on Windows 8 and 8.1.
|
||||
pref("ui.osk.require_win10", false);
|
||||
// This pref stores the "reason" that the on-screen keyboard was either
|
||||
// shown or not shown when focus is moved to an editable text field. It is
|
||||
// used to help debug why the keyboard is either not appearing when expected
|
||||
|
|
|
@ -1125,7 +1125,7 @@ SimpleTest.monitorConsole = function (continuation, msgs, forbidUnexpectedMsgs)
|
|||
}
|
||||
|
||||
function msgMatches(msg, pat) {
|
||||
for (k in pat) {
|
||||
for (var k in pat) {
|
||||
if (!(k in msg)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ Cu.import("resource://gre/modules/devtools/shared/event-emitter.js");
|
|||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
|
||||
"resource://gre/modules/Locale.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Log",
|
||||
"resource://gre/modules/Log.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||
|
@ -59,6 +61,8 @@ var {
|
|||
flushJarCache,
|
||||
} = ExtensionUtils;
|
||||
|
||||
const LOGGER_ID_BASE = "addons.webextension.";
|
||||
|
||||
var scriptScope = this;
|
||||
|
||||
// This object loads the ext-*.js scripts that define the extension API.
|
||||
|
@ -336,8 +340,11 @@ this.Extension = function(addonData)
|
|||
this.addonData = addonData;
|
||||
this.id = addonData.id;
|
||||
this.baseURI = Services.io.newURI("moz-extension://" + uuid, null, null);
|
||||
this.baseURI.QueryInterface(Ci.nsIURL);
|
||||
this.manifest = null;
|
||||
this.localeMessages = null;
|
||||
this.logger = Log.repository.getLogger(LOGGER_ID_BASE + this.id.replace(/\./g, "-"));
|
||||
this.principal = this.createPrincipal();
|
||||
|
||||
this.views = new Set();
|
||||
|
||||
|
@ -433,12 +440,10 @@ this.Extension.generate = function(id, data)
|
|||
let components = filename.split("/");
|
||||
let path = "";
|
||||
for (let component of components.slice(0, -1)) {
|
||||
path += component;
|
||||
path += component + "/";
|
||||
if (!zipW.hasEntry(path)) {
|
||||
zipW.addEntryDirectory(path, time, false);
|
||||
}
|
||||
|
||||
path += "/";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -487,6 +492,24 @@ Extension.prototype = {
|
|||
Management.emit("test-message", this, ...args);
|
||||
},
|
||||
|
||||
createPrincipal(uri = this.baseURI) {
|
||||
return Services.scriptSecurityManager.createCodebasePrincipal(
|
||||
uri, {addonId: this.id});
|
||||
},
|
||||
|
||||
// Checks that the given URL is a child of our baseURI.
|
||||
isExtensionURL(url) {
|
||||
let uri = Services.io.newURI(url, null, null);
|
||||
|
||||
let common = this.baseURI.getCommonBaseSpec(uri);
|
||||
return common == this.baseURI.spec;
|
||||
},
|
||||
|
||||
// Report an error about the extension's manifest file.
|
||||
manifestError(message) {
|
||||
this.logger.error(`Loading extension '${this.id}': ${message}`);
|
||||
},
|
||||
|
||||
// Representation of the extension to send to content
|
||||
// processes. This should include anything the content process might
|
||||
// need.
|
||||
|
|
|
@ -45,7 +45,8 @@ function runSafeSync(context, f, ...args)
|
|||
try {
|
||||
args = Cu.cloneInto(args, context.cloneScope);
|
||||
} catch (e) {
|
||||
dump(`runSafe failure\n${context.cloneScope}\n${Error().stack}`);
|
||||
Cu.reportError(e);
|
||||
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${Error().stack}`);
|
||||
}
|
||||
return runSafeSyncWithoutClone(f, ...args);
|
||||
}
|
||||
|
@ -57,7 +58,8 @@ function runSafe(context, f, ...args)
|
|||
try {
|
||||
args = Cu.cloneInto(args, context.cloneScope);
|
||||
} catch (e) {
|
||||
dump(`runSafe failure\n${context.cloneScope}\n${Error().stack}`);
|
||||
Cu.reportError(e);
|
||||
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${Error().stack}`);
|
||||
}
|
||||
return runSafeWithoutClone(f, ...args);
|
||||
}
|
||||
|
|
|
@ -21,13 +21,26 @@ BackgroundPage.prototype = {
|
|||
let webNav = Services.appShell.createWindowlessBrowser(false);
|
||||
this.webNav = webNav;
|
||||
|
||||
let principal = Services.scriptSecurityManager.createCodebasePrincipal(this.extension.baseURI,
|
||||
{addonId: this.extension.id});
|
||||
let url;
|
||||
if (this.page) {
|
||||
url = this.extension.baseURI.resolve(this.page);
|
||||
} else {
|
||||
// TODO: Chrome uses "_generated_background_page.html" for this.
|
||||
url = this.extension.baseURI.resolve("_blank.html");
|
||||
}
|
||||
|
||||
if (!this.extension.isExtensionURL(url)) {
|
||||
this.extension.manifestError("Background page must be a file within the extension");
|
||||
url = this.extension.baseURI.resolve("_blank.html");
|
||||
}
|
||||
|
||||
let uri = Services.io.newURI(url, null, null);
|
||||
let principal = this.extension.createPrincipal(uri);
|
||||
|
||||
let interfaceRequestor = webNav.QueryInterface(Ci.nsIInterfaceRequestor);
|
||||
let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
|
||||
|
||||
this.context = new ExtensionPage(this.extension, {type: "background", docShell});
|
||||
this.context = new ExtensionPage(this.extension, {type: "background", docShell, uri});
|
||||
GlobalManager.injectInDocShell(docShell, this.extension, this.context);
|
||||
|
||||
docShell.createAboutBlankContentViewer(principal);
|
||||
|
@ -36,12 +49,6 @@ BackgroundPage.prototype = {
|
|||
this.contentWindow = window;
|
||||
this.context.contentWindow = window;
|
||||
|
||||
let url;
|
||||
if (this.page) {
|
||||
url = this.extension.baseURI.resolve(this.page);
|
||||
} else {
|
||||
url = this.extension.baseURI.resolve("_blank.html");
|
||||
}
|
||||
webNav.loadURI(url, 0, null, null, null);
|
||||
|
||||
// TODO: Right now we run onStartup after the background page
|
||||
|
@ -51,6 +58,12 @@ BackgroundPage.prototype = {
|
|||
let doc = window.document;
|
||||
for (let script of this.scripts) {
|
||||
let url = this.extension.baseURI.resolve(script);
|
||||
|
||||
if (!this.extension.isExtensionURL(url)) {
|
||||
this.extension.manifestError("Background scripts must be files within the extension");
|
||||
continue;
|
||||
}
|
||||
|
||||
let tag = doc.createElement("script");
|
||||
tag.setAttribute("src", url);
|
||||
tag.async = false;
|
||||
|
|
|
@ -1237,19 +1237,30 @@ var Impl = {
|
|||
this._log.trace("assemblePayloadWithMeasurements - reason: " + reason +
|
||||
", submitting subsession data: " + isSubsession);
|
||||
|
||||
// This allows wrapping data retrieval calls in a try-catch block so that
|
||||
// failures don't break the rest of the ping assembly.
|
||||
const protect = (fn) => {
|
||||
try {
|
||||
return fn();
|
||||
} catch (ex) {
|
||||
this.log.error("assemblePayloadWithMeasurements - caught exception", ex);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Payload common to chrome and content processes.
|
||||
let payloadObj = {
|
||||
ver: PAYLOAD_VERSION,
|
||||
simpleMeasurements: simpleMeasurements,
|
||||
histograms: this.getHistograms(isSubsession, clearSubsession),
|
||||
keyedHistograms: this.getKeyedHistograms(isSubsession, clearSubsession),
|
||||
histograms: protect(() => this.getHistograms(isSubsession, clearSubsession)),
|
||||
keyedHistograms: protect(() => this.getKeyedHistograms(isSubsession, clearSubsession)),
|
||||
};
|
||||
|
||||
// Add extended set measurements common to chrome & content processes
|
||||
if (Telemetry.canRecordExtended) {
|
||||
payloadObj.chromeHangs = Telemetry.chromeHangs;
|
||||
payloadObj.threadHangStats = this.getThreadHangStats(Telemetry.threadHangStats);
|
||||
payloadObj.log = TelemetryLog.entries();
|
||||
payloadObj.chromeHangs = protect(() => Telemetry.chromeHangs);
|
||||
payloadObj.threadHangStats = protect(() => this.getThreadHangStats(Telemetry.threadHangStats));
|
||||
payloadObj.log = protect(() => TelemetryLog.entries());
|
||||
}
|
||||
|
||||
if (Utils.isContentProcess) {
|
||||
|
@ -1261,20 +1272,21 @@ var Impl = {
|
|||
|
||||
// Add extended set measurements for chrome process.
|
||||
if (Telemetry.canRecordExtended) {
|
||||
payloadObj.slowSQL = Telemetry.slowSQL;
|
||||
payloadObj.fileIOReports = Telemetry.fileIOReports;
|
||||
payloadObj.lateWrites = Telemetry.lateWrites;
|
||||
payloadObj.slowSQL = protect(() => Telemetry.slowSQL);
|
||||
payloadObj.fileIOReports = protect(() => Telemetry.fileIOReports);
|
||||
payloadObj.lateWrites = protect(() => Telemetry.lateWrites);
|
||||
|
||||
// Add the addon histograms if they are present
|
||||
let addonHistograms = this.getAddonHistograms();
|
||||
if (Object.keys(addonHistograms).length > 0) {
|
||||
let addonHistograms = protect(() => this.getAddonHistograms());
|
||||
if (addonHistograms && Object.keys(addonHistograms).length > 0) {
|
||||
payloadObj.addonHistograms = addonHistograms;
|
||||
}
|
||||
|
||||
payloadObj.addonDetails = AddonManagerPrivate.getTelemetryDetails();
|
||||
payloadObj.UIMeasurements = UITelemetry.getUIMeasurements();
|
||||
payloadObj.addonDetails = protect(() => AddonManagerPrivate.getTelemetryDetails());
|
||||
payloadObj.UIMeasurements = protect(() => UITelemetry.getUIMeasurements());
|
||||
|
||||
if (Object.keys(this._slowSQLStartup).length != 0 &&
|
||||
if (this._slowSQLStartup &&
|
||||
Object.keys(this._slowSQLStartup).length != 0 &&
|
||||
(Object.keys(this._slowSQLStartup.mainThread).length ||
|
||||
Object.keys(this._slowSQLStartup.otherThreads).length)) {
|
||||
payloadObj.slowSQLStartup = this._slowSQLStartup;
|
||||
|
@ -1282,7 +1294,7 @@ var Impl = {
|
|||
}
|
||||
|
||||
if (this._childTelemetry.length) {
|
||||
payloadObj.childPayloads = this.getChildPayloads();
|
||||
payloadObj.childPayloads = protect(() => this.getChildPayloads());
|
||||
}
|
||||
|
||||
return payloadObj;
|
||||
|
|
|
@ -43,17 +43,18 @@ Structure::
|
|||
subsessionLength: <number>, // the subsession length in seconds, monotonic
|
||||
},
|
||||
|
||||
childPayloads: {...}, // only present with e10s; a reduced payload from content processes
|
||||
childPayloads: {...}, // only present with e10s; a reduced payload from content processes, null on failure
|
||||
simpleMeasurements: {...},
|
||||
|
||||
simpleMeasurements: { ... },
|
||||
histograms: {},
|
||||
keyedHistograms: {},
|
||||
chromeHangs: {},
|
||||
threadHangStats: {},
|
||||
log: [],
|
||||
// The following properties may all be null if we fail to collect them.
|
||||
histograms: {...},
|
||||
keyedHistograms: {...},
|
||||
chromeHangs: {...},
|
||||
threadHangStats: {...},
|
||||
log: [...],
|
||||
fileIOReports: {...},
|
||||
lateWrites: {...},
|
||||
addonDetails: { ... },
|
||||
addonDetails: {...},
|
||||
addonHistograms: {...},
|
||||
UIMeasurements: {...},
|
||||
slowSQL: {...},
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
const char* kOskPathPrefName = "ui.osk.on_screen_keyboard_path";
|
||||
const char* kOskEnabled = "ui.osk.enabled";
|
||||
const char* kOskDetectPhysicalKeyboard = "ui.osk.detect_physical_keyboard";
|
||||
const char* kOskRequireTabletMode = "ui.osk.require_tablet_mode";
|
||||
const char* kOskRequireWin10 = "ui.osk.require_win10";
|
||||
const char* kOskDebugReason = "ui.osk.debug.keyboardDisplayReason";
|
||||
|
||||
namespace mozilla {
|
||||
|
@ -46,6 +46,9 @@ bool IMEHandler::sShowingOnScreenKeyboard = false;
|
|||
decltype(SetInputScopes)* IMEHandler::sSetInputScopes = nullptr;
|
||||
#endif // #ifdef NS_ENABLE_TSF
|
||||
|
||||
static POWER_PLATFORM_ROLE sPowerPlatformRole = PlatformRoleUnspecified;
|
||||
static bool sDeterminedPowerPlatformRole = false;
|
||||
|
||||
// static
|
||||
void
|
||||
IMEHandler::Initialize()
|
||||
|
@ -536,21 +539,22 @@ void
|
|||
IMEHandler::MaybeShowOnScreenKeyboard()
|
||||
{
|
||||
if (sPluginHasFocus ||
|
||||
!IsWin10OrLater() ||
|
||||
!IsWin8OrLater() ||
|
||||
!Preferences::GetBool(kOskEnabled, true) ||
|
||||
sShowingOnScreenKeyboard ||
|
||||
IMEHandler::IsKeyboardPresentOnSlate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tablet Mode is only supported on Windows 10 and higher.
|
||||
// When touch-event detection within IME is better supported
|
||||
// this check may be removed, and ShowOnScreenKeyboard can
|
||||
// run on Windows 8 and higher (adjusting the IsWin10OrLater
|
||||
// guard above and within MaybeDismissOnScreenKeyboard).
|
||||
if (!IsInTabletMode() &&
|
||||
Preferences::GetBool(kOskRequireTabletMode, true) &&
|
||||
!AutoInvokeOnScreenKeyboardInDesktopMode()) {
|
||||
// On Windows 10 we require tablet mode, unless the user has set the relevant
|
||||
// Windows setting to enable the on-screen keyboard in desktop mode.
|
||||
// We might be disabled specifically on Win8(.1), so we check that afterwards.
|
||||
if (IsWin10OrLater()) {
|
||||
if (!IsInTabletMode() && !AutoInvokeOnScreenKeyboardInDesktopMode()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (Preferences::GetBool(kOskRequireWin10, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -562,7 +566,7 @@ void
|
|||
IMEHandler::MaybeDismissOnScreenKeyboard()
|
||||
{
|
||||
if (sPluginHasFocus ||
|
||||
!IsWin10OrLater() ||
|
||||
!IsWin8OrLater() ||
|
||||
!sShowingOnScreenKeyboard) {
|
||||
return;
|
||||
}
|
||||
|
@ -661,23 +665,33 @@ IMEHandler::IsKeyboardPresentOnSlate()
|
|||
// checked by first checking the role of the device and then the
|
||||
// corresponding system metric (SM_CONVERTIBLESLATEMODE). If it is being
|
||||
// used as a tablet then we want the OSK to show up.
|
||||
typedef POWER_PLATFORM_ROLE (WINAPI* PowerDeterminePlatformRole)();
|
||||
PowerDeterminePlatformRole power_determine_platform_role =
|
||||
reinterpret_cast<PowerDeterminePlatformRole>(::GetProcAddress(
|
||||
::LoadLibraryW(L"PowrProf.dll"), "PowerDeterminePlatformRole"));
|
||||
if (power_determine_platform_role) {
|
||||
POWER_PLATFORM_ROLE role = power_determine_platform_role();
|
||||
if (((role == PlatformRoleMobile) || (role == PlatformRoleSlate)) &&
|
||||
(::GetSystemMetrics(SM_CONVERTIBLESLATEMODE) == 0)) {
|
||||
if (role == PlatformRoleMobile) {
|
||||
Preferences::SetString(kOskDebugReason, L"IKPOS: PlatformRoleMobile.");
|
||||
} else if (role == PlatformRoleSlate) {
|
||||
Preferences::SetString(kOskDebugReason, L"IKPOS: PlatformRoleSlate.");
|
||||
}
|
||||
return false;
|
||||
typedef POWER_PLATFORM_ROLE (WINAPI* PowerDeterminePlatformRoleEx)(ULONG Version);
|
||||
if (!sDeterminedPowerPlatformRole) {
|
||||
sDeterminedPowerPlatformRole = true;
|
||||
PowerDeterminePlatformRoleEx power_determine_platform_role =
|
||||
reinterpret_cast<PowerDeterminePlatformRoleEx>(::GetProcAddress(
|
||||
::LoadLibraryW(L"PowrProf.dll"), "PowerDeterminePlatformRoleEx"));
|
||||
if (power_determine_platform_role) {
|
||||
sPowerPlatformRole = power_determine_platform_role(POWER_PLATFORM_ROLE_V2);
|
||||
} else {
|
||||
sPowerPlatformRole = PlatformRoleUnspecified;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is not a mobile or slate (tablet) device, we don't need to
|
||||
// do anything here.
|
||||
if (sPowerPlatformRole != PlatformRoleMobile &&
|
||||
sPowerPlatformRole != PlatformRoleSlate) {
|
||||
Preferences::SetString(kOskDebugReason, L"IKPOS: PlatformRole is neither Mobile nor Slate.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Likewise, if the tablet/mobile isn't in "slate" mode, we should bail:
|
||||
if (::GetSystemMetrics(SM_CONVERTIBLESLATEMODE) != 0) {
|
||||
Preferences::SetString(kOskDebugReason, L"IKPOS: ConvertibleSlateMode is non-zero");
|
||||
return true;
|
||||
}
|
||||
|
||||
const GUID KEYBOARD_CLASS_GUID =
|
||||
{ 0x4D36E96B, 0xE325, 0x11CE,
|
||||
{ 0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18 } };
|
||||
|
|
Загрузка…
Ссылка в новой задаче