Merge m-c to inbound. a=merge

This commit is contained in:
Ryan VanderMeulen 2015-05-28 10:20:38 -04:00
commit 094299de37
219 changed files with 4979 additions and 2421 deletions

View File

@ -15,6 +15,7 @@ support-files =
[test_controls.xul]
[test_doc.html]
[test_doc_busy.html]
skip-if = os == 'mac' && os_version == '10.6'
[test_docarticle.html]
[test_editablebody.html]
[test_expandable.xul]

View File

@ -27,6 +27,6 @@ exports.getTabForWindow = getTabForWindow;
exports.getTabForRawTab = modelFor;
function getTabForBrowser(browser) {
return modelFor(getRawTabForBrowser(browser));
return modelFor(getRawTabForBrowser(browser)) || null;
}
exports.getTabForBrowser = getTabForBrowser;

View File

@ -9,7 +9,7 @@ const { tabNS, rawTabNS } = require('./namespace');
const { EventTarget } = require('../event/target');
const { activateTab, getTabTitle, setTabTitle, closeTab, getTabURL,
getTabContentWindow, getTabForBrowser, setTabURL, getOwnerWindow,
getTabContentDocument, getTabContentType, getTabId } = require('./utils');
getTabContentDocument, getTabContentType, getTabId, isTab } = require('./utils');
const { emit } = require('../event/core');
const { isPrivate } = require('../private-browsing/utils');
const { isWindowPrivate } = require('../window/utils');
@ -17,6 +17,7 @@ const { when: unload } = require('../system/unload');
const { BLANK } = require('../content/thumbnail');
const { viewFor } = require('../view/core');
const { EVENTS } = require('./events');
const { modelFor } = require('../model/core');
const ERR_FENNEC_MSG = 'This method is not yet supported by Fennec';

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d31f6f08740a46e5315c9279b8f987c9d1fce486"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="04c307a8a8439adf38c0f14d065275f571a7b486"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d31f6f08740a46e5315c9279b8f987c9d1fce486"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="04c307a8a8439adf38c0f14d065275f571a7b486"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>

View File

@ -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="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="22664edc4c73e5fe8f5095ff1d5549db78a2bc10"/>

View File

@ -17,10 +17,10 @@
</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="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d31f6f08740a46e5315c9279b8f987c9d1fce486"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="04c307a8a8439adf38c0f14d065275f571a7b486"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d31f6f08740a46e5315c9279b8f987c9d1fce486"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="04c307a8a8439adf38c0f14d065275f571a7b486"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d31f6f08740a46e5315c9279b8f987c9d1fce486"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="04c307a8a8439adf38c0f14d065275f571a7b486"/>
<!-- Stock Android things -->
<project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="ffc05a232799fe8fcb3e47b7440b52b1fb4244c0"/>
<project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" revision="337e0ef5e40f02a1ae59b90db0548976c70a7226"/>

View File

@ -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="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="22664edc4c73e5fe8f5095ff1d5549db78a2bc10"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d31f6f08740a46e5315c9279b8f987c9d1fce486"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="04c307a8a8439adf38c0f14d065275f571a7b486"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>

View File

@ -1,9 +1,9 @@
{
"git": {
"git_revision": "05380df3158fa39e1dde1687c0bf11a71f8c6868",
"git_revision": "e7d268074ee3c9eeb191c2205c0e35992fb3915d",
"remote": "https://git.mozilla.org/releases/gaia.git",
"branch": ""
},
"revision": "fc4f5dfe591a45cb03ae221489f41b56f38c883a",
"revision": "f7c817ea239637d874d7099f2ccaafc6c3766389",
"repo_path": "integration/gaia-central"
}

View File

@ -17,10 +17,10 @@
</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="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d31f6f08740a46e5315c9279b8f987c9d1fce486"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="04c307a8a8439adf38c0f14d065275f571a7b486"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="05380df3158fa39e1dde1687c0bf11a71f8c6868"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e7d268074ee3c9eeb191c2205c0e35992fb3915d"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="fffc68521ebb1501d6b015c6d1c4a17a04fdb2e2"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d31f6f08740a46e5315c9279b8f987c9d1fce486"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="04c307a8a8439adf38c0f14d065275f571a7b486"/>
<!-- Stock Android things -->
<project groups="pdk,linux" name="platform/prebuilts/clang/linux-x86/host/3.5" path="prebuilts/clang/linux-x86/host/3.5" revision="ffc05a232799fe8fcb3e47b7440b52b1fb4244c0"/>
<project groups="pdk,linux,arm" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.8" revision="337e0ef5e40f02a1ae59b90db0548976c70a7226"/>

View File

@ -178,6 +178,9 @@ pref("app.update.badge", true);
#else
pref("app.update.badge", false);
#endif
// Give the user x seconds to reboot before showing a badge on the hamburger
// button. default=4 days
pref("app.update.badgeWaitTime", 345600);
// If set to true, the Update Service will apply updates in the background
// when it finishes downloading them.

View File

@ -2363,11 +2363,19 @@ function BrowserViewSourceOfDocument(aArgsOrDocument) {
let inTab = Services.prefs.getBoolPref("view_source.tab");
if (inTab) {
let viewSourceURL = `view-source:${args.URL}`;
let tab = gBrowser.loadOneTab(viewSourceURL, {
let tabBrowser = gBrowser;
// In the case of sidebars and chat windows, gBrowser is defined but null,
// because no #content element exists. For these cases, we need to find
// the most recent browser window.
if (!tabBrowser) {
let browserWindow = RecentWindow.getMostRecentBrowserWindow();
tabBrowser = browserWindow.gBrowser;
}
let tab = tabBrowser.loadOneTab(viewSourceURL, {
relatedToCurrent: true,
inBackground: false
});
args.viewSourceBrowser = gBrowser.getBrowserForTab(tab);
args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
top.gViewSourceUtils.viewSourceInBrowser(args);
} else {
top.gViewSourceUtils.viewSource(args);
@ -2561,18 +2569,27 @@ function PageProxyClickHandler(aEvent)
// Setup the hamburger button badges for updates, if enabled.
let gMenuButtonUpdateBadge = {
enabled: false,
badgeWaitTime: 0,
timer: null,
init: function () {
try {
this.enabled = Services.prefs.getBoolPref("app.update.badge");
} catch (e) {}
if (this.enabled) {
try {
this.badgeWaitTime = Services.prefs.getIntPref("app.update.badgeWaitTime");
} catch (e) {
this.badgeWaitTime = 345600; // 4 days
}
PanelUI.menuButton.classList.add("badged-button");
Services.obs.addObserver(this, "update-staged", false);
}
},
uninit: function () {
if (this.timer)
this.timer.cancel();
if (this.enabled) {
Services.obs.removeObserver(this, "update-staged");
PanelUI.panel.removeEventListener("popupshowing", this, true);
@ -2616,23 +2633,14 @@ let gMenuButtonUpdateBadge = {
case STATE_APPLIED_SVC:
case STATE_PENDING:
case STATE_PENDING_SVC:
// If the update is successfully applied, or if the updater has fallen back
// to non-staged updates, add a badge to the hamburger menu to indicate an
// update will be applied once the browser restarts.
PanelUI.menuButton.setAttribute("update-status", "succeeded");
let brandBundle = document.getElementById("bundle_brand");
let brandShortName = brandBundle.getString("brandShortName");
stringId = "appmenu.restartNeeded.description";
updateButtonText = gNavigatorBundle.getFormattedString(stringId,
[brandShortName]);
updateButton.setAttribute("label", updateButtonText);
updateButton.setAttribute("update-status", "succeeded");
updateButton.hidden = false;
PanelUI.panel.addEventListener("popupshowing", this, true);
if (this.timer) {
return;
}
// Give the user badgeWaitTime seconds to react before prompting.
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.timer.initWithCallback(this, this.badgeWaitTime * 1000,
this.timer.TYPE_ONE_SHOT);
// The timer callback will call uninit() when it completes.
break;
case STATE_FAILED:
// Background update has failed, let's show the UI responsible for
@ -2649,13 +2657,29 @@ let gMenuButtonUpdateBadge = {
PanelUI.panel.addEventListener("popupshowing", this, true);
this.uninit();
break;
case STATE_DOWNLOADING:
// We've fallen back to downloading the full update because the partial
// update failed to get staged in the background. Therefore we need to keep
// our observer.
return;
}
},
notify: function () {
// If the update is successfully applied, or if the updater has fallen back
// to non-staged updates, add a badge to the hamburger menu to indicate an
// update will be applied once the browser restarts.
PanelUI.menuButton.setAttribute("update-status", "succeeded");
let brandBundle = document.getElementById("bundle_brand");
let brandShortName = brandBundle.getString("brandShortName");
stringId = "appmenu.restartNeeded.description";
updateButtonText = gNavigatorBundle.getFormattedString(stringId,
[brandShortName]);
let updateButton = document.getElementById("PanelUI-update-status");
updateButton.setAttribute("label", updateButtonText);
updateButton.setAttribute("update-status", "succeeded");
updateButton.hidden = false;
PanelUI.panel.addEventListener("popupshowing", this, true);
this.uninit();
},

View File

@ -1007,11 +1007,19 @@ nsContextMenu.prototype = {
let inTab = Services.prefs.getBoolPref("view_source.tab");
if (inTab) {
let tab = gBrowser.loadOneTab("about:blank", {
let tabBrowser = gBrowser;
// In the case of sidebars and chat windows, gBrowser is defined but null,
// because no #content element exists. For these cases, we need to find
// the most recent browser window.
if (!tabBrowser) {
let browserWindow = RecentWindow.getMostRecentBrowserWindow();
tabBrowser = browserWindow.gBrowser;
}
let tab = tabBrowser.loadOneTab("about:blank", {
relatedToCurrent: true,
inBackground: false
});
let viewSourceBrowser = gBrowser.getBrowserForTab(tab);
let viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
if (aContext == "selection") {
top.gViewSourceUtils
.viewSourceFromSelectionInBrowser(reference, viewSourceBrowser);

View File

@ -163,7 +163,7 @@ loop.conversation = (function(mozL10n) {
dispatcher: dispatcher,
mozLoop: navigator.mozLoop}), document.querySelector("#main"));
document.body.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("dir", "rtl");//mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({

View File

@ -163,7 +163,7 @@ loop.conversation = (function(mozL10n) {
dispatcher={dispatcher}
mozLoop={navigator.mozLoop} />, document.querySelector("#main"));
document.body.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("dir", "rtl");//mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({

View File

@ -91,6 +91,7 @@ loop.store = loop.store || {};
"openRoom",
"shareRoomUrl",
"updateRoomContext",
"updateRoomContextDone",
"updateRoomContextError",
"updateRoomList"
],
@ -115,7 +116,8 @@ loop.store = loop.store || {};
error: null,
pendingCreation: false,
pendingInitialRetrieval: false,
rooms: []
rooms: [],
savingContext: false
};
},
@ -473,6 +475,7 @@ loop.store = loop.store || {};
* @param {sharedActions.UpdateRoomContext} actionData
*/
updateRoomContext: function(actionData) {
this.setStoreState({ savingContext: true });
this._mozLoop.rooms.get(actionData.roomToken, function(err, room) {
if (err) {
this.dispatchAction(new sharedActions.UpdateRoomContextError({
@ -520,28 +523,38 @@ loop.store = loop.store || {};
// When no properties have been set on the roomData object, there's nothing
// to save.
if (!Object.getOwnPropertyNames(roomData).length) {
this.dispatchAction(new sharedActions.UpdateRoomContextDone());
return;
}
this.setStoreState({error: null});
this._mozLoop.rooms.update(actionData.roomToken, roomData,
function(err, data) {
if (err) {
this.dispatchAction(new sharedActions.UpdateRoomContextError({
error: err
}));
}
var action = err ?
new sharedActions.UpdateRoomContextError({ error: err }) :
new sharedActions.UpdateRoomContextDone();
this.dispatchAction(action);
}.bind(this));
}.bind(this));
},
/**
* Handles the updateRoomContextDone action.
*/
updateRoomContextDone: function() {
this.setStoreState({ savingContext: false });
},
/**
* Updating the context data attached to a room error.
*
* @param {sharedActions.UpdateRoomContextError} actionData
*/
updateRoomContextError: function(actionData) {
this.setStoreState({error: actionData.error});
this.setStoreState({
error: actionData.error,
savingContext: false
});
}
});
})(document.mozL10n || navigator.mozL10n);

View File

@ -29,6 +29,8 @@ loop.roomViews = (function(mozL10n) {
this._onActiveRoomStateChanged);
this.listenTo(this.props.roomStore, "change:error",
this._onRoomError);
this.listenTo(this.props.roomStore, "change:savingContext",
this._onRoomSavingContext);
},
componentWillUnmount: function() {
@ -53,11 +55,21 @@ loop.roomViews = (function(mozL10n) {
}
},
_onRoomSavingContext: function() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({savingContext: this.props.roomStore.getStoreState("savingContext")});
}
},
getInitialState: function() {
var storeState = this.props.roomStore.getStoreState("activeRoom");
return _.extend({
// Used by the UI showcase.
roomState: this.props.roomState || storeState.roomState
roomState: this.props.roomState || storeState.roomState,
savingContext: false
}, storeState);
}
};
@ -174,6 +186,7 @@ loop.roomViews = (function(mozL10n) {
mozLoop: React.PropTypes.object.isRequired,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
savingContext: React.PropTypes.bool,
show: React.PropTypes.bool.isRequired,
showContext: React.PropTypes.bool.isRequired
},
@ -243,7 +256,11 @@ loop.roomViews = (function(mozL10n) {
mozL10n.get("context_add_some_label")
)
),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("div", {className: cx({
"btn-group": true,
"call-action-group": true,
hide: this.state.editMode
})},
React.createElement("button", {className: "btn btn-info btn-email",
onClick: this.handleEmailButtonClick},
mozL10n.get("email_link_button")
@ -270,6 +287,7 @@ loop.roomViews = (function(mozL10n) {
dispatcher: this.props.dispatcher,
editMode: this.state.editMode,
error: this.props.error,
savingContext: this.props.savingContext,
mozLoop: this.props.mozLoop,
onEditModeChange: this.handleEditModeChange,
roomData: this.props.roomData,
@ -290,6 +308,7 @@ loop.roomViews = (function(mozL10n) {
onEditModeChange: React.PropTypes.func,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
savingContext: React.PropTypes.bool.isRequired,
show: React.PropTypes.bool.isRequired
},
@ -379,6 +398,18 @@ loop.roomViews = (function(mozL10n) {
this.setState({ show: false });
},
handleContextClick: function(event) {
event.stopPropagation();
event.preventDefault();
var url = this._getURL();
if (!url || !url.location) {
return;
}
this.props.mozLoop.openURL(url.location);
},
handleEditClick: function(event) {
event.preventDefault();
@ -397,13 +428,13 @@ loop.roomViews = (function(mozL10n) {
newRoomURL: context.url,
newRoomDescription: context.description,
newRoomThumbnail: context.previewImage
}, this.handleFormSubmit);
});
} else {
this.setState({
newRoomURL: "",
newRoomDescription: "",
newRoomThumbnail: ""
}, this.handleFormSubmit);
});
}
},
@ -469,7 +500,6 @@ loop.roomViews = (function(mozL10n) {
var thumbnail = url && url.thumbnail || "";
var urlDescription = url && url.description || "";
var location = url && url.location || "";
var checkboxLabel = null;
var locationData = null;
if (location) {
locationData = checkboxLabel = sharedUtils.formatURL(location);
@ -483,43 +513,50 @@ loop.roomViews = (function(mozL10n) {
var cx = React.addons.classSet;
if (this.state.editMode) {
var availableContext = this.state.availableContext;
// The checkbox shows as checked when there's already context data
// attached to this room.
var checked = !!urlDescription;
var checkboxLabel = urlDescription || (availableContext && availableContext.url ?
availableContext.description : "");
return (
React.createElement("div", {className: "room-context"},
React.createElement("div", {className: "room-context-content"},
React.createElement("p", {className: cx({"error": !!this.props.error,
"error-display-area": true})},
mozL10n.get("rooms_change_failed_label")
),
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
React.createElement(sharedViews.Checkbox, {
checked: !!url,
disabled: !!url || !checkboxLabel,
label: mozL10n.get("context_edit_activate_label", {
title: checkboxLabel ? checkboxLabel.hostname : ""
}),
onChange: this.handleCheckboxChange,
value: location}),
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("textarea", {rows: "2", type: "text", className: "room-context-name",
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("context_edit_name_placeholder"),
valueLink: this.linkState("newRoomName")}),
React.createElement("input", {type: "text", className: "room-context-url",
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: "https://",
valueLink: this.linkState("newRoomURL")}),
React.createElement("textarea", {rows: "4", type: "text", className: "room-context-comments",
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("context_edit_comments_placeholder"),
valueLink: this.linkState("newRoomDescription")})
),
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick,
title: mozL10n.get("cancel_button")})
)
React.createElement("div", {className: "room-context editMode"},
React.createElement("p", {className: cx({"error": !!this.props.error,
"error-display-area": true})},
mozL10n.get("rooms_change_failed_label")
),
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
React.createElement(sharedViews.Checkbox, {
additionalClass: cx({ hide: !checkboxLabel }),
checked: checked,
disabled: checked,
label: checkboxLabel,
onChange: this.handleCheckboxChange,
value: location}),
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("input", {type: "text", className: "room-context-name",
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("context_edit_name_placeholder"),
valueLink: this.linkState("newRoomName")}),
React.createElement("input", {type: "text", className: "room-context-url",
onKeyDown: this.handleTextareaKeyDown,
placeholder: "https://",
disabled: availableContext && availableContext.url === this.state.newRoomURL,
valueLink: this.linkState("newRoomURL")}),
React.createElement("textarea", {rows: "3", type: "text", className: "room-context-comments",
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("context_edit_comments_placeholder"),
valueLink: this.linkState("newRoomDescription")})
),
React.createElement("button", {className: "btn btn-info",
disabled: this.props.savingContext,
onClick: this.handleFormSubmit},
mozL10n.get("context_save_label")
),
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick,
title: mozL10n.get("cancel_button")})
)
);
}
@ -530,25 +567,23 @@ loop.roomViews = (function(mozL10n) {
return (
React.createElement("div", {className: "room-context"},
React.createElement("img", {className: "room-context-thumbnail", src: thumbnail}),
React.createElement("div", {className: "room-context-content"},
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
React.createElement("div", {className: "room-context-content",
onClick: this.handleContextClick},
React.createElement("img", {className: "room-context-thumbnail", src: thumbnail}),
React.createElement("div", {className: "room-context-description",
title: urlDescription}, this._truncate(urlDescription)),
React.createElement("a", {className: "room-context-url",
href: location,
target: "_blank",
title: locationData.location}, locationData.hostname),
this.props.roomData.roomDescription ?
React.createElement("div", {className: "room-context-comment"}, this.props.roomData.roomDescription) :
null,
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick,
title: mozL10n.get("context_hide_tooltip")}),
React.createElement("button", {className: "room-context-btn-edit",
onClick: this.handleEditClick,
title: mozL10n.get("context_edit_tooltip")})
)
title: urlDescription},
this._truncate(urlDescription),
React.createElement("a", {className: "room-context-url",
title: locationData.location}, locationData.hostname)
)
),
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick,
title: mozL10n.get("context_hide_tooltip")}),
React.createElement("button", {className: "room-context-btn-edit",
onClick: this.handleEditClick,
title: mozL10n.get("context_edit_tooltip")})
)
);
}
@ -671,6 +706,7 @@ loop.roomViews = (function(mozL10n) {
error: this.state.error,
mozLoop: this.props.mozLoop,
roomData: roomData,
savingContext: this.state.savingContext,
show: shouldRenderInvitationOverlay,
showContext: shouldRenderContextView,
socialShareButtonAvailable: this.state.socialShareButtonAvailable,
@ -696,6 +732,7 @@ loop.roomViews = (function(mozL10n) {
React.createElement(DesktopRoomContextView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
savingContext: this.state.savingContext,
mozLoop: this.props.mozLoop,
roomData: roomData,
show: !shouldRenderInvitationOverlay && shouldRenderContextView})

View File

@ -29,6 +29,8 @@ loop.roomViews = (function(mozL10n) {
this._onActiveRoomStateChanged);
this.listenTo(this.props.roomStore, "change:error",
this._onRoomError);
this.listenTo(this.props.roomStore, "change:savingContext",
this._onRoomSavingContext);
},
componentWillUnmount: function() {
@ -53,11 +55,21 @@ loop.roomViews = (function(mozL10n) {
}
},
_onRoomSavingContext: function() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({savingContext: this.props.roomStore.getStoreState("savingContext")});
}
},
getInitialState: function() {
var storeState = this.props.roomStore.getStoreState("activeRoom");
return _.extend({
// Used by the UI showcase.
roomState: this.props.roomState || storeState.roomState
roomState: this.props.roomState || storeState.roomState,
savingContext: false
}, storeState);
}
};
@ -174,6 +186,7 @@ loop.roomViews = (function(mozL10n) {
mozLoop: React.PropTypes.object.isRequired,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
savingContext: React.PropTypes.bool,
show: React.PropTypes.bool.isRequired,
showContext: React.PropTypes.bool.isRequired
},
@ -243,7 +256,11 @@ loop.roomViews = (function(mozL10n) {
{mozL10n.get("context_add_some_label")}
</a>
</div>
<div className="btn-group call-action-group">
<div className={cx({
"btn-group": true,
"call-action-group": true,
hide: this.state.editMode
})}>
<button className="btn btn-info btn-email"
onClick={this.handleEmailButtonClick}>
{mozL10n.get("email_link_button")}
@ -270,6 +287,7 @@ loop.roomViews = (function(mozL10n) {
dispatcher={this.props.dispatcher}
editMode={this.state.editMode}
error={this.props.error}
savingContext={this.props.savingContext}
mozLoop={this.props.mozLoop}
onEditModeChange={this.handleEditModeChange}
roomData={this.props.roomData}
@ -290,6 +308,7 @@ loop.roomViews = (function(mozL10n) {
onEditModeChange: React.PropTypes.func,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
savingContext: React.PropTypes.bool.isRequired,
show: React.PropTypes.bool.isRequired
},
@ -379,6 +398,18 @@ loop.roomViews = (function(mozL10n) {
this.setState({ show: false });
},
handleContextClick: function(event) {
event.stopPropagation();
event.preventDefault();
var url = this._getURL();
if (!url || !url.location) {
return;
}
this.props.mozLoop.openURL(url.location);
},
handleEditClick: function(event) {
event.preventDefault();
@ -397,13 +428,13 @@ loop.roomViews = (function(mozL10n) {
newRoomURL: context.url,
newRoomDescription: context.description,
newRoomThumbnail: context.previewImage
}, this.handleFormSubmit);
});
} else {
this.setState({
newRoomURL: "",
newRoomDescription: "",
newRoomThumbnail: ""
}, this.handleFormSubmit);
});
}
},
@ -469,7 +500,6 @@ loop.roomViews = (function(mozL10n) {
var thumbnail = url && url.thumbnail || "";
var urlDescription = url && url.description || "";
var location = url && url.location || "";
var checkboxLabel = null;
var locationData = null;
if (location) {
locationData = checkboxLabel = sharedUtils.formatURL(location);
@ -483,43 +513,50 @@ loop.roomViews = (function(mozL10n) {
var cx = React.addons.classSet;
if (this.state.editMode) {
var availableContext = this.state.availableContext;
// The checkbox shows as checked when there's already context data
// attached to this room.
var checked = !!urlDescription;
var checkboxLabel = urlDescription || (availableContext && availableContext.url ?
availableContext.description : "");
return (
<div className="room-context">
<div className="room-context-content">
<p className={cx({"error": !!this.props.error,
"error-display-area": true})}>
{mozL10n.get("rooms_change_failed_label")}
</p>
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
<sharedViews.Checkbox
checked={!!url}
disabled={!!url || !checkboxLabel}
label={mozL10n.get("context_edit_activate_label", {
title: checkboxLabel ? checkboxLabel.hostname : ""
})}
onChange={this.handleCheckboxChange}
value={location} />
<form onSubmit={this.handleFormSubmit}>
<textarea rows="2" type="text" className="room-context-name"
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("context_edit_name_placeholder")}
valueLink={this.linkState("newRoomName")} />
<input type="text" className="room-context-url"
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder="https://"
valueLink={this.linkState("newRoomURL")} />
<textarea rows="4" type="text" className="room-context-comments"
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("context_edit_comments_placeholder")}
valueLink={this.linkState("newRoomDescription")} />
</form>
<button className="room-context-btn-close"
onClick={this.handleCloseClick}
title={mozL10n.get("cancel_button")}/>
</div>
<div className="room-context editMode">
<p className={cx({"error": !!this.props.error,
"error-display-area": true})}>
{mozL10n.get("rooms_change_failed_label")}
</p>
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
<sharedViews.Checkbox
additionalClass={cx({ hide: !checkboxLabel })}
checked={checked}
disabled={checked}
label={checkboxLabel}
onChange={this.handleCheckboxChange}
value={location} />
<form onSubmit={this.handleFormSubmit}>
<input type="text" className="room-context-name"
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("context_edit_name_placeholder")}
valueLink={this.linkState("newRoomName")} />
<input type="text" className="room-context-url"
onKeyDown={this.handleTextareaKeyDown}
placeholder="https://"
disabled={availableContext && availableContext.url === this.state.newRoomURL}
valueLink={this.linkState("newRoomURL")} />
<textarea rows="3" type="text" className="room-context-comments"
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("context_edit_comments_placeholder")}
valueLink={this.linkState("newRoomDescription")} />
</form>
<button className="btn btn-info"
disabled={this.props.savingContext}
onClick={this.handleFormSubmit}>
{mozL10n.get("context_save_label")}
</button>
<button className="room-context-btn-close"
onClick={this.handleCloseClick}
title={mozL10n.get("cancel_button")}/>
</div>
);
}
@ -530,25 +567,23 @@ loop.roomViews = (function(mozL10n) {
return (
<div className="room-context">
<img className="room-context-thumbnail" src={thumbnail}/>
<div className="room-context-content">
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
<div className="room-context-content"
onClick={this.handleContextClick}>
<img className="room-context-thumbnail" src={thumbnail}/>
<div className="room-context-description"
title={urlDescription}>{this._truncate(urlDescription)}</div>
<a className="room-context-url"
href={location}
target="_blank"
title={locationData.location}>{locationData.hostname}</a>
{this.props.roomData.roomDescription ?
<div className="room-context-comment">{this.props.roomData.roomDescription}</div> :
null}
<button className="room-context-btn-close"
onClick={this.handleCloseClick}
title={mozL10n.get("context_hide_tooltip")}/>
<button className="room-context-btn-edit"
onClick={this.handleEditClick}
title={mozL10n.get("context_edit_tooltip")}/>
title={urlDescription}>
{this._truncate(urlDescription)}
<a className="room-context-url"
title={locationData.location}>{locationData.hostname}</a>
</div>
</div>
<button className="room-context-btn-close"
onClick={this.handleCloseClick}
title={mozL10n.get("context_hide_tooltip")}/>
<button className="room-context-btn-edit"
onClick={this.handleEditClick}
title={mozL10n.get("context_edit_tooltip")}/>
</div>
);
}
@ -671,6 +706,7 @@ loop.roomViews = (function(mozL10n) {
error={this.state.error}
mozLoop={this.props.mozLoop}
roomData={roomData}
savingContext={this.state.savingContext}
show={shouldRenderInvitationOverlay}
showContext={shouldRenderContextView}
socialShareButtonAvailable={this.state.socialShareButtonAvailable}
@ -696,6 +732,7 @@ loop.roomViews = (function(mozL10n) {
<DesktopRoomContextView
dispatcher={this.props.dispatcher}
error={this.state.error}
savingContext={this.state.savingContext}
mozLoop={this.props.mozLoop}
roomData={roomData}
show={!shouldRenderInvitationOverlay && shouldRenderContextView} />

View File

@ -313,7 +313,8 @@
width: 100%;
}
.call-action-group > .btn {
.call-action-group > .btn,
.room-context > .btn {
min-height: 26px;
border-radius: 2px;
margin: 0 4px;
@ -1005,11 +1006,19 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
width: 100%;
font-size: .9em;
display: flex;
flex-flow: row nowrap;
flex-flow: column nowrap;
align-content: flex-start;
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
/* Make the context view float atop the video elements. */
z-index: 2;
}
.room-context.editMode {
/* Stretch to the maximum available space whilst not covering the conversation
toolbar (26px). */
height: calc(100% - 26px);
}
.room-invitation-overlay .room-context {
@ -1019,11 +1028,26 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
flex: 0 1 auto;
}
.room-invitation-overlay .room-context.editMode {
height: 100%;
}
.room-context-content {
flex: 1 1 auto;
text-align: start;
display: flex;
flex-flow: row nowrap;
font-size: .9em;
}
.room-context-thumbnail {
width: 16px;
/* 16px icon size + 3px border width. */
width: 19px;
max-height: 19px;
border: 3px solid #fff;
border-radius: 3px;
background-color: #fff;
-moz-margin-end: 1ch;
margin-bottom: 1em;
order: 1;
flex: 0 1 auto;
}
@ -1031,30 +1055,24 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
display: none;
}
.room-context-content {
order: 2;
flex: 1 1 auto;
text-align: start;
}
.room-context-content > .error-display-area.error {
.room-context > .error-display-area.error {
display: block;
background-color: rgba(215,67,69,.8);
border-radius: 3px;
padding: .5em;
}
.room-context-content > .error-display-area {
.room-context > .error-display-area {
display: none;
}
.room-context-content > .error-display-area.error {
.room-context > .error-display-area.error {
margin: 1em 0 .5em 0;
text-align: center;
text-shadow: 1px 1px 0 rgba(0,0,0,.3);
}
.room-context-content > .checkbox-wrapper {
.room-context > .checkbox-wrapper {
margin-bottom: .5em;
}
@ -1062,9 +1080,8 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
margin-bottom: 1em;
}
.room-context-label,
.room-context-description,
.room-context-content > .checkbox-wrapper > label {
.room-context > .checkbox-wrapper > label {
color: #fff;
}
@ -1077,45 +1094,70 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
word-wrap: break-word;
}
.room-context-url {
color: #59A1D7;
:not(input).room-context-url {
color: #0095dd;
font-style: italic;
text-decoration: none;
margin-bottom: 1em;
display: block;
cursor: pointer;
}
.room-context-url:hover {
text-decoration: underline;
}
.room-context-content > form > textarea,
.room-context-content > form > input[type="text"] {
display: block;
background: rgba(0,0,0,.5);
color: #fff;
font-family: "Helvetica Neue", Arial, sans;
font-size: 1em;
border: 1px solid #999;
.room-context > form {
width: 100%;
padding: .2em .4em;
border-radius: 3px;
resize: none;
}
.room-context-content > form > textarea:not(:last-of-type),
.room-context-content > form > input[type="text"] {
.room-context > form > textarea,
.room-context > form > input[type="text"] {
display: block;
background: rgba(0,0,0,.5);
font-family: "Helvetica Neue", Arial, sans;
border: 1px solid rgba(255,255,255,.2);
width: 100%;
padding: .5em;
border-radius: 3px;
resize: none;
color: #fff;
}
.room-context > form > textarea {
font-size: 1em;
}
.room-context > form > input:not([disabled]).room-context-url {
color: #0095dd;
}
.room-context > form > input[disabled] {
background-color: rgba(255,255,255,.2);
color: rgba(255,255,255,.4);
}
.room-context > form > textarea:not(:last-of-type),
.room-context > form > input[type="text"] {
margin: 0 0 .5em 0;
}
.room-context > .btn {
margin: .5em 0 0;
font-size: 1.1em;
padding: 0 .5em;
align-self: flex-end;
}
.room-context-btn-close,
.room-context-btn-edit {
position: absolute;
right: 5px;
top: 5px;
right: 8px;
/* 8px offset + 2px border-top */
top: 10px;
width: 8px;
height: 8px;
background-color: transparent;
background-image: url("../img/icons-10x10.svg#close");
background-image: url("../img/icons-10x10.svg#close-darkergrey");
background-size: 8px 8px;
background-repeat: no-repeat;
border: 0;
@ -1124,8 +1166,8 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
}
.room-context-btn-edit {
right: 18px;
background-image: url("../img/icons-10x10.svg#edit");
right: 20px;
background-image: url("../img/icons-10x10.svg#edit-darkergrey");
}
.room-context-btn-edit:hover,
@ -1141,11 +1183,11 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
body[dir=rtl] .room-context-btn-close,
body[dir=rtl] .room-context-btn-edit {
right: auto;
left: 5px;
left: 8px;
}
body[dir=rtl] .room-context-btn-edit {
left: 18px;
left: 20px;
}
/* Standalone rooms */

View File

@ -22,6 +22,9 @@
use[id$="-disabled"] {
fill: rgba(255,255,255,0.4);
}
use[id$="-darkergrey"] {
fill: #999;
}
</style>
<defs>
<polygon id="close-shape" points="10,1.717 8.336,0.049 5.024,3.369 1.663,0 0,1.668 3.36,5.037 0.098,8.307 1.762,9.975 5.025,6.705 8.311,10 9.975,8.332 6.688,5.037"/>
@ -33,6 +36,7 @@
<use id="close" xlink:href="#close-shape"/>
<use id="close-active" xlink:href="#close-shape"/>
<use id="close-disabled" xlink:href="#close-shape"/>
<use id="close-darkergrey" xlink:href="#close-shape"/>
<use id="dropdown" xlink:href="#dropdown-shape"/>
<use id="dropdown-white" xlink:href="#dropdown-shape"/>
<use id="dropdown-active" xlink:href="#dropdown-shape"/>
@ -40,6 +44,7 @@
<use id="edit" xlink:href="#edit-shape"/>
<use id="edit-active" xlink:href="#edit-shape"/>
<use id="edit-disabled" xlink:href="#edit-shape"/>
<use id="edit-darkergrey" xlink:href="#edit-shape"/>
<use id="expand" xlink:href="#expand-shape"/>
<use id="expand-active" xlink:href="#expand-shape"/>
<use id="expand-disabled" xlink:href="#expand-shape"/>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -368,6 +368,12 @@ loop.shared.actions = (function() {
error: [Error, Object]
}),
/**
* Updating the context data attached to a room finished successfully.
*/
UpdateRoomContextDone: Action.define("updateRoomContextDone", {
}),
/**
* Copy a room url into the user's clipboard.
* XXX: should move to some roomActions module - refs bug 1079284

View File

@ -435,7 +435,7 @@ loop.store.ActiveRoomStore = (function() {
// XXX Ideally we'd do this check before joining a room, but we're waiting
// for the UX for that. See bug 1166824. In the meantime this gives us
// additional information for analysis.
loop.shared.utils.hasAudioDevices(function(hasAudio) {
loop.shared.utils.hasAudioOrVideoDevices(function(hasAudio) {
if (hasAudio) {
// MEDIA_WAIT causes the views to dispatch sharedActions.SetupStreamElements,
// which in turn starts the sdk obtaining the device permission.

View File

@ -312,12 +312,12 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
* @param {Function} callback Called with a boolean which is true if there
* are audio devices present.
*/
function hasAudioDevices(callback) {
function hasAudioOrVideoDevices(callback) {
// mediaDevices is the official API for the spec.
if ("mediaDevices" in rootNavigator) {
rootNavigator.mediaDevices.enumerateDevices().then(function(result) {
function checkForInput(device) {
return device.kind === "audioinput";
return device.kind === "audioinput" || device.kind === "videoinput";
}
callback(result.some(checkForInput));
@ -329,7 +329,7 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
} else if ("MediaStreamTrack" in rootObject) {
rootObject.MediaStreamTrack.getSources(function(result) {
function checkForInput(device) {
return device.kind === "audio";
return device.kind === "audio" || device.kind === "video";
}
callback(result.some(checkForInput));
@ -745,7 +745,7 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
isFirefoxOS: isFirefoxOS,
isOpera: isOpera,
getUnsupportedPlatform: getUnsupportedPlatform,
hasAudioDevices: hasAudioDevices,
hasAudioOrVideoDevices: hasAudioOrVideoDevices,
locationData: locationData,
atob: atob,
btoa: btoa,

View File

@ -663,7 +663,7 @@ loop.shared.views = (function(_, l10n) {
disabled: this.props.disabled
};
if (this.props.additionalClass) {
checkClasses[this.props.additionalClass] = true;
wrapperClasses[this.props.additionalClass] = true;
}
return (
React.createElement("div", {className: cx(wrapperClasses),

View File

@ -663,7 +663,7 @@ loop.shared.views = (function(_, l10n) {
disabled: this.props.disabled
};
if (this.props.additionalClass) {
checkClasses[this.props.additionalClass] = true;
wrapperClasses[this.props.additionalClass] = true;
}
return (
<div className={cx(wrapperClasses)}

View File

@ -644,9 +644,26 @@ describe("loop.store.RoomStore", function () {
});
});
it("should flag the the store as saving context", function() {
expect(store.getStoreState().savingContext).to.eql(false);
sandbox.stub(fakeMozLoop.rooms, "update", function(roomToken, roomData, cb) {
expect(store.getStoreState().savingContext).to.eql(true);
cb();
});
dispatcher.dispatch(new sharedActions.UpdateRoomContext({
roomToken: "42abc",
newRoomName: "silly name"
}));
expect(store.getStoreState().savingContext).to.eql(false);
});
it("should store any update-encountered error", function() {
var err = new Error("fake");
sandbox.stub(fakeMozLoop.rooms, "update", function(roomToken, roomData, cb) {
expect(store.getStoreState().savingContext).to.eql(true);
cb(err);
});
@ -655,7 +672,9 @@ describe("loop.store.RoomStore", function () {
newRoomName: "silly name"
}));
expect(store.getStoreState().error).eql(err);
var state = store.getStoreState();
expect(state.error).eql(err);
expect(state.savingContext).to.eql(false);
});
it("should ensure only submitting a non-empty room name", function() {
@ -667,6 +686,7 @@ describe("loop.store.RoomStore", function () {
}));
sinon.assert.notCalled(fakeMozLoop.rooms.update);
expect(store.getStoreState().savingContext).to.eql(false);
});
it("should save updated context information", function() {
@ -723,6 +743,7 @@ describe("loop.store.RoomStore", function () {
}));
sinon.assert.notCalled(fakeMozLoop.rooms.update);
expect(store.getStoreState().savingContext).to.eql(false);
});
});
});

View File

@ -96,7 +96,7 @@ describe("loop.roomViews", function () {
roomStore: roomStore
}));
var expectedState = _.extend({foo: "bar"},
var expectedState = _.extend({foo: "bar", savingContext: false},
activeRoomStore.getInitialStoreState());
expect(testView.state).eql(expectedState);
@ -676,10 +676,8 @@ describe("loop.roomViews", function () {
expect(node).to.not.eql(null);
expect(node.querySelector(".room-context-thumbnail").src).to.
eql(fakeContextURL.thumbnail);
expect(node.querySelector(".room-context-description").textContent).to.
eql(fakeContextURL.description);
expect(node.querySelector(".room-context-comment").textContent).to.
eql(view.props.roomData.roomDescription);
expect(node.querySelector(".room-context-description").firstChild.textContent).
to.eql(fakeContextURL.description);
});
it("should not render optional data", function() {
@ -727,12 +725,13 @@ describe("loop.roomViews", function () {
expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
});
it("should show the checkbox as disabled when no context is available", function() {
it("should show the checkbox as disabled when context is already set", function() {
view = mountTestComponent({
editMode: true,
roomData: {
roomToken: "fakeToken",
roomName: "fakeName"
roomName: "fakeName",
roomContextUrls: [fakeContextURL]
}
});
@ -758,6 +757,7 @@ describe("loop.roomViews", function () {
expect(view.state.availableContext.previewImage).to.eql(favicon);
var node = view.getDOMNode();
expect(node.querySelector(".checkbox-wrapper").classList.contains("disabled")).to.eql(true);
expect(node.querySelector(".room-context-name").value).to.eql(roomName);
expect(node.querySelector(".room-context-url").value).to.eql(fakeContextURL.location);
expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
@ -765,6 +765,28 @@ describe("loop.roomViews", function () {
next();
});
});
it("should hide the checkbox when no context data is stored or available", function(next) {
view = mountTestComponent({
roomData: {
roomToken: "fakeToken",
roomName: "Hello, is it me you're looking for?"
}
});
// Switch to editMode via setting the prop, since we can control that
// better.
view.setProps({ editMode: true }, function() {
// First check if availableContext is set correctly.
expect(view.state.availableContext).to.not.eql(null);
expect(view.state.availableContext.previewImage).to.eql(favicon);
var node = view.getDOMNode();
expect(node.querySelector(".checkbox-wrapper").classList.contains("hide")).to.eql(true);
next();
});
});
});
describe("Update Room", function() {
@ -785,13 +807,13 @@ describe("loop.roomViews", function () {
roomNameBox = view.getDOMNode().querySelector(".room-context-name");
});
it("should dispatch a UpdateRoomContext action when the focus is lost",
it("should dispatch a UpdateRoomContext action when the save button is clicked",
function() {
React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
value: "reallyFake"
}});
React.addons.TestUtils.Simulate.blur(roomNameBox);
React.addons.TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-info"));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,

View File

@ -640,7 +640,7 @@ describe("loop.store.ActiveRoomStore", function () {
});
it("should set the state to MEDIA_WAIT if media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioDevices").callsArgWith(0, true);
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, true);
store.joinRoom();
@ -648,7 +648,7 @@ describe("loop.store.ActiveRoomStore", function () {
});
it("should not set the state to MEDIA_WAIT if no media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioDevices").callsArgWith(0, false);
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false);
store.joinRoom();
@ -656,7 +656,7 @@ describe("loop.store.ActiveRoomStore", function () {
});
it("should dispatch `ConnectionFailure` if no media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioDevices").callsArgWith(0, false);
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false);
store.joinRoom();

View File

@ -141,7 +141,7 @@ describe("loop.shared.utils", function() {
});
});
describe("hasAudioDevices", function() {
describe("#hasAudioOrVideoDevices", function() {
var fakeNavigatorObject, fakeWindowObject;
beforeEach(function() {
@ -168,17 +168,18 @@ describe("loop.shared.utils", function() {
delete fakeNavigatorObject.mediaDevices;
delete fakeWindowObject.MediaStreamTrack;
sharedUtils.hasAudioDevices(function(result) {
sharedUtils.hasAudioOrVideoDevices(function(result) {
expect(result).eql(true);
done();
});
});
it("should return false if no audio devices exist according to navigator.mediaDevices", function(done) {
it("should return false if no audio nor video devices exist according to navigator.mediaDevices", function(done) {
delete fakeWindowObject.MediaStreamTrack;
fakeNavigatorObject.mediaDevices.enumerateDevices.returns(Promise.resolve([]));
sharedUtils.hasAudioDevices(function(result) {
sharedUtils.hasAudioOrVideoDevices(function(result) {
try {
expect(result).eql(false);
done();
@ -193,11 +194,6 @@ describe("loop.shared.utils", function() {
fakeNavigatorObject.mediaDevices.enumerateDevices.returns(
Promise.resolve([{
deviceId: "15234",
groupId: "",
kind: "videoinput",
label: ""
}, {
deviceId: "54321",
groupId: "",
kind: "audioinput",
@ -205,7 +201,7 @@ describe("loop.shared.utils", function() {
}])
);
sharedUtils.hasAudioDevices(function(result) {
sharedUtils.hasAudioOrVideoDevices(function(result) {
try {
expect(result).eql(true);
done();
@ -215,11 +211,33 @@ describe("loop.shared.utils", function() {
});
});
it("should return false if no audio devices exist according to window.MediaStreamTrack", function(done) {
it("should return true if video devices exist according to navigator.mediaDevices", function(done) {
delete fakeWindowObject.MediaStreamTrack;
fakeNavigatorObject.mediaDevices.enumerateDevices.returns(
Promise.resolve([{
deviceId: "15234",
groupId: "",
kind: "videoinput",
label: ""
}])
);
sharedUtils.hasAudioOrVideoDevices(function(result) {
try {
expect(result).eql(true);
done();
} catch (ex) {
done(ex);
}
});
});
it("should return false if no audio nor video devices exist according to window.MediaStreamTrack", function(done) {
delete fakeNavigatorObject.mediaDevices;
fakeWindowObject.MediaStreamTrack.getSources.callsArgWith(0, []);
sharedUtils.hasAudioDevices(function(result) {
sharedUtils.hasAudioOrVideoDevices(function(result) {
try {
expect(result).eql(false);
done();
@ -233,18 +251,33 @@ describe("loop.shared.utils", function() {
delete fakeNavigatorObject.mediaDevices;
fakeWindowObject.MediaStreamTrack.getSources.callsArgWith(0, [{
facing: "",
id: "15234",
kind: "video",
label: ""
}, {
facing: "",
id: "54321",
kind: "audio",
label: ""
}]);
sharedUtils.hasAudioDevices(function(result) {
sharedUtils.hasAudioOrVideoDevices(function(result) {
try {
expect(result).eql(true);
done();
} catch (ex) {
done(ex);
}
});
});
it("should return true if video devices exist according to window.MediaStreamTrack", function(done) {
delete fakeNavigatorObject.mediaDevices;
fakeWindowObject.MediaStreamTrack.getSources.callsArgWith(0, [{
facing: "",
id: "15234",
kind: "video",
label: ""
}]);
sharedUtils.hasAudioOrVideoDevices(function(result) {
try {
expect(result).eql(true);
done();

View File

@ -0,0 +1,15 @@
# Ignore d3
browser/devtools/shared/d3.js
browser/devtools/webaudioeditor/lib/dagre-d3.js
# Ignore codemirror
browser/devtools/sourceeditor/codemirror/*.js
browser/devtools/sourceeditor/codemirror/**/*.js
# Ignore jquery test libs
browser/devtools/markupview/test/lib_*
# Ignore pre-processed files
browser/devtools/framework/toolbox-process-window.js
browser/devtools/performance/system.js
browser/devtools/webide/webide-prefs.js

406
browser/devtools/.eslintrc Normal file
View File

@ -0,0 +1,406 @@
{
"env": {
"es6": true
},
"globals": {
"Cc": true,
"Ci": true,
"Components": true,
"console": true,
"Cr": true,
"Cu": true,
"devtools": true,
"dump": true,
"EventEmitter": true,
"exports": true,
"loader": true,
"module": true,
"require": true,
"Services": true,
"Task": true,
"XPCOMUtils": true,
},
"rules": {
// These are the rules that have been configured so far to match the
// devtools coding style.
// Disallow using variables outside the blocks they are defined (especially
// since only let and const are used, see "no-var").
"block-scoped-var": 2,
// Enforce one true brace style (opening brace on the same line) and avoid
// start and end braces on the same line.
"brace-style": [2, "1tbs", {"allowSingleLine": false}],
// Require camel case names
"camelcase": 2,
// Disallow trailing commas. Not valid JSON notation.
"comma-dangle": 1,
// Enforce spacing before and after comma
"comma-spacing": [2, {"before": false, "after": true}],
// Enforce one true comma style.
"comma-style": [2, "last"],
// Warn about cyclomatic complexity in functions.
"complexity": 1,
// Require return statements to either always or never specify values.
"consistent-return": 2,
// Don't warn for inconsistent naming when capturing this (not so important
// with auto-binding fat arrow functions).
"consistent-this": 0,
// Enforce curly brace conventions for all control statements.
"curly": 2,
// Don't require a default case in switch statements. Avoid being forced to
// add a bogus default when you know all possible cases are handled.
"default-case": 0,
// Don't enforce consistent newlines before or after dots. This depends on
// what gives the most readable code.
"dot-location": [1, "object"],
// Encourage the use of dot notation whenever possible.
"dot-notation": 2,
// Enforce newline at the end of file, with no multiple empty lines.
"eol-last": 2,
// Allow using == instead of ===, in the interest of landing something since
// the devtools codebase is split on convention here.
"eqeqeq": 0,
// Don't require function expressions to have a name.
// This makes the code more verbose and hard to read. Our engine already
// does a fantastic job assigning a name to the function, which includes
// the enclosing function name, and worst case you have a line number that
// you can just look up.
"func-names": 0,
// Allow use of function declarations and expressions.
"func-style": 0,
// Deprecated, will be removed in 1.0.
"generator-star": 0,
// Enforce the spacing around the * in generator functions.
"generator-star-spacing": [1, "after"],
// Deprecated, will be removed in 1.0.
"global-strict": 0,
// Only useful in a node environment.
"handle-callback-err": 0,
// Tab width.
"indent": [2, 2],
// Enforces spacing between keys and values in object literal properties.
"key-spacing": [1, {"beforeColon": false, "afterColon": true}],
// Allow mixed 'LF' and 'CRLF' as linebreaks.
"linebreak-style": 0,
// Don't enforce the maximum depth that blocks can be nested. The complexity
// rule is a better rule to check this.
"max-depth": 0,
// Maximum length of a line.
"max-len": [1, 80],
// Maximum depth callbacks can be nested.
"max-nested-callbacks": [2, 3],
// Don't limit the number of parameters that can be used in a function.
"max-params": 0,
// Don't limit the maximum number of statement allowed in a function. We
// already have the complexity rule that's a better measurement.
"max-statements": 0,
// Require a capital letter for constructors, only check if all new
// operators are followed by a capital letter. Don't warn when capitalized
// functions are used without the new operator.
"new-cap": [2, {"capIsNew": false}],
// Disallow the omission of parentheses when invoking a constructor with no
// arguments.
"new-parens": 2,
// Disallow use of the Array constructor.
"no-array-constructor": 2,
// Allow use of bitwise operators.
"no-bitwise": 0,
// Disallow use of arguments.caller or arguments.callee.
"no-caller": 2,
// Disallow the catch clause parameter name being the same as a variable in
// the outer scope, to avoid confusion.
"no-catch-shadow": 1,
// Deprecated, will be removed in 1.0.
"no-comma-dangle": 0,
// Disallow assignment in conditional expressions.
"no-cond-assign": 2,
// Allow using the console API.
"no-console": 0,
// Allow using constant expressions in conditions like while (true)
"no-constant-condition": 0,
// Allow use of the continue statement.
"no-continue": 0,
// Disallow control characters in regular expressions.
"no-control-regex": 2,
// Disallow use of debugger.
"no-debugger": 2,
// Disallow deletion of variables (deleting properties is fine).
"no-delete-var": 2,
// Allow division operators explicitly at beginning of regular expression.
"no-div-regex": 0,
// Disallow duplicate arguments in functions.
"no-dupe-args": 2,
// Disallow duplicate keys when creating object literals.
"no-dupe-keys": 2,
// Disallow a duplicate case label.
"no-duplicate-case": 2,
// Disallow else after a return in an if. The else around the second return
// here is useless:
// if (something) { return false; } else { return true; }
"no-else-return": 2,
// Disallow empty statements. This will report an error for:
// try { something(); } catch (e) {}
// but will not report it for:
// try { something(); } catch (e) { /* Silencing the error because ...*/ }
// which is a valid use case.
"no-empty": 2,
// Disallow the use of empty character classes in regular expressions.
"no-empty-class": 2,
// Disallow use of labels for anything other then loops and switches.
"no-empty-label": 2,
// Disallow use of eval(). We have other APIs to evaluate code in content.
"no-eval": 2,
// Disallow assigning to the exception in a catch block.
"no-ex-assign": 2,
// Disallow adding to native types
"no-extend-native": 2,
// Disallow unnecessary function binding.
"no-extra-bind": 2,
// Disallow double-negation boolean casts in a boolean context.
"no-extra-boolean-cast": 2,
// Allow unnecessary parentheses, as they may make the code more readable.
"no-extra-parens": 0,
// Disallow unnecessary semicolons.
"no-extra-semi": 2,
// Deprecated, will be removed in 1.0.
"no-extra-strict": 0,
// Disallow fallthrough of case statements, except if there is a comment.
"no-fallthrough": 2,
// Allow the use of leading or trailing decimal points in numeric literals.
"no-floating-decimal": 0,
// Disallow comments inline after code.
"no-inline-comments": 1,
// Disallow if as the only statement in an else block.
"no-lonely-if": 2,
// Allow mixing regular variable and require declarations (not a node env).
"no-mixed-requires": 0,
// Disallow mixed spaces and tabs for indentation.
"no-mixed-spaces-and-tabs": 2,
// Disallow use of multiple spaces (sometimes used to align const values,
// array or object items, etc.). It's hard to maintain and doesn't add that
// much benefit.
"no-multi-spaces": 1,
// Disallow use of multiline strings (use template strings instead).
"no-multi-str": 1,
// Disallow multiple empty lines.
"no-multiple-empty-lines": [1, {"max": 1}],
// Disallow reassignments of native objects.
"no-native-reassign": 2,
// Disallow nested ternary expressions, they make the code hard to read.
"no-nested-ternary": 2,
// Allow use of new operator with the require function.
"no-new-require": 0,
// Disallow use of octal literals.
"no-octal": 1,
// Allow reassignment of function parameters.
"no-param-reassign": 0,
// Allow string concatenation with __dirname and __filename (not a node env).
"no-path-concat": 0,
// Allow use of unary operators, ++ and --.
"no-plusplus": 0,
// Allow using process.env (not a node environment).
"no-process-env": 0,
// Allow using process.exit (not a node environment).
"no-process-exit": 0,
// Disallow usage of __proto__ property.
"no-proto": 2,
// Disallow declaring the same variable more than once (we use let anyway).
"no-redeclare": 2,
// Disallow multiple spaces in a regular expression literal.
"no-regex-spaces": 2,
// Allow reserved words being used as object literal keys.
"no-reserved-keys": 0,
// Don't restrict usage of specified node modules (not a node environment).
"no-restricted-modules": 0,
// Disallow use of assignment in return statement. It is preferable for a
// single line of code to have only one easily predictable effect.
"no-return-assign": 2,
// Allow use of javascript: urls.
"no-script-url": 0,
// Disallow comparisons where both sides are exactly the same.
"no-self-compare": 2,
// Disallow use of comma operator.
"no-sequences": 2,
// Warn about declaration of variables already declared in the outer scope.
// This isn't an error because it sometimes is useful to use the same name
// in a small helper function rather than having to come up with another
// random name.
// Still, making this a warning can help people avoid being confused.
"no-shadow": 1,
// Disallow shadowing of names such as arguments.
"no-shadow-restricted-names": 2,
// Deprecated, will be removed in 1.0.
"no-space-before-semi": 0,
// Disallow space between function identifier and application.
"no-spaced-func": 1,
// Disallow sparse arrays, eg. let arr = [,,2].
// Array destructuring is fine though:
// for (let [, breakpointPromise] of aPromises)
"no-sparse-arrays": 2,
// Allow use of synchronous methods (not a node environment).
"no-sync": 0,
// Allow the use of ternary operators.
"no-ternary": 0,
// Disallow throwing literals (eg. throw "error" instead of
// throw new Error("error")).
"no-throw-literal": 2,
// Disallow trailing whitespace at the end of lines.
"no-trailing-spaces": 2,
// Disallow use of undeclared variables unless mentioned in a /*global */
// block.
// This should really be a 2, but until we define all globals in comments
// and .eslintrc, keeping this as a 1.
"no-undef": 2,
// Allow dangling underscores in identifiers (for privates).
"no-underscore-dangle": 0,
// Allow use of undefined variable.
"no-undefined": 0,
// Disallow the use of Boolean literals in conditional expressions.
"no-unneeded-ternary": 2,
// Disallow unreachable statements after a return, throw, continue, or break
// statement.
"no-unreachable": 2,
// Disallow declaration of variables that are not used in the code
"no-unused-vars": 2,
// Allow using variables before they are defined.
"no-use-before-define": 0,
// Require let or const instead of var.
"no-var": 2,
// Allow using TODO/FIXME comments.
"no-warning-comments": 0,
// Disallow use of the with statement.
"no-with": 2,
// Don't require method and property shorthand syntax for object literals.
// We use this in the code a lot, but not consistently, and this seems more
// like something to check at code review time.
"object-shorthand": 0,
// Allow more than one variable declaration per function.
"one-var": 0,
// Disallow padding within blocks.
"padded-blocks": [1, "never"],
// Don't require quotes around object literal property names.
"quote-props": 0,
// Double quotes should be used.
"quotes": [1, "double"],
// Require use of the second argument for parseInt().
"radix": 2,
// Always require use of semicolons wherever they are valid.
"semi": [1, "always"],
// Enforce spacing after semicolons.
"semi-spacing": [1, {"before": false, "after": true}],
// Don't require to sort variables within the same declaration block.
// Anyway, one-var is disabled.
"sort-vars": 0,
// Deprecated, will be removed in 1.0.
"space-after-function-name": 0,
// Require a space after keywords.
"space-after-keywords": [1, "always"],
// Deprecated, will be removed in 1.0.
"space-after-function-name": 0,
// Require a space before the start brace of a block.
"space-before-blocks": [1, "always"],
// Deprecated, will be removed in 1.0.
"space-before-function-parentheses": 0,
// Disallow space before function opening parenthesis.
"space-before-function-paren": [1, "never"],
// Disable the rule that checks if spaces inside {} and [] are there or not.
// Our code is split on conventions, and it'd be nice to have 2 rules
// instead, one for [] and one for {}. So, disabling until we write them.
"space-in-brackets": 0,
// Disallow spaces inside parentheses.
"space-in-parens": [1, "never"],
// Require spaces around operators, except for a|0.
"space-infix-ops": [1, {"int32Hint": true}],
// Require a space after return, throw, and case.
"space-return-throw-case": 1,
// Require spaces before/after unary operators (words on by default,
// nonwords off by default).
"space-unary-ops": [1, { "words": true, "nonwords": false }],
// Deprecated, will be removed in 1.0.
"space-unary-word-ops": 0,
// Require a space immediately following the // in a line comment.
"spaced-line-comment": [1, "always"],
// Require "use strict" to be defined globally in the script.
"strict": [2, "global"],
// Disallow comparisons with the value NaN.
"use-isnan": 2,
// Warn about invalid JSDoc comments.
// Disabled for now because of https://github.com/eslint/eslint/issues/2270
// The rule fails on some jsdoc comments like in:
// browser/devtools/webconsole/console-output.js
"valid-jsdoc": 0,
// Ensure that the results of typeof are compared against a valid string.
"valid-typeof": 2,
// Allow vars to be declared anywhere in the scope.
"vars-on-top": 0,
// Don't require immediate function invocation to be wrapped in parentheses.
"wrap-iife": 0,
// Don't require regex literals to be wrapped in parentheses (which
// supposedly prevent them from being mistaken for division operators).
"wrap-regex": 0,
// Disallow Yoda conditions (where literal value comes first).
"yoda": 2,
// And these are the rules that haven't been discussed so far, and that are
// disabled for now until we introduce them, one at a time.
// Require for-in loops to have an if statement.
"guard-for-in": 0,
// allow/disallow an empty newline after var statement
"newline-after-var": 0,
// disallow the use of alert, confirm, and prompt
"no-alert": 0,
// disallow comparisons to null without a type-checking operator
"no-eq-null": 0,
// disallow overwriting functions written as function declarations
"no-func-assign": 0,
// disallow use of eval()-like methods
"no-implied-eval": 0,
// disallow function or variable declarations in nested blocks
"no-inner-declarations": 0,
// disallow invalid regular expression strings in the RegExp constructor
"no-invalid-regexp": 0,
// disallow irregular whitespace outside of strings and comments
"no-irregular-whitespace": 0,
// disallow usage of __iterator__ property
"no-iterator": 0,
// disallow labels that share a name with a variable
"no-label-var": 0,
// disallow use of labeled statements
"no-labels": 0,
// disallow unnecessary nested blocks
"no-lone-blocks": 0,
// disallow creation of functions within loops
"no-loop-func": 0,
// disallow negation of the left operand of an in expression
"no-negated-in-lhs": 0,
// disallow use of new operator when not part of the assignment or
// comparison
"no-new": 0,
// disallow use of new operator for Function object
"no-new-func": 0,
// disallow use of the Object constructor
"no-new-object": 0,
// disallows creating new instances of String,Number, and Boolean
"no-new-wrappers": 0,
// disallow the use of object properties of the global object (Math and
// JSON) as functions
"no-obj-calls": 0,
// disallow use of octal escape sequences in string literals, such as
// var foo = "Copyright \251";
"no-octal-escape": 0,
// disallow use of undefined when initializing variables
"no-undef-init": 0,
// disallow usage of expressions in statement position
"no-unused-expressions": 0,
// disallow use of void operator
"no-void": 0,
// disallow wrapping of non-IIFE statements in parens
"no-wrap-func": 0,
// require assignment operator shorthand where possible or prohibit it
// entirely
"operator-assignment": 0,
// enforce operators to be placed before or after line breaks
"operator-linebreak": 0,
}
}

View File

@ -0,0 +1,50 @@
// Parent config file for all devtools browser mochitest files.
{
"rules": {
// Only disallow non-global unused vars, so that things like the test
// function do not produce errors.
"no-unused-vars": [2, {"vars": "local"}],
// Allow using undefined variables so that tests can refer to functions
// and variables defined in head.js files, without having to maintain a
// list of globals in each .eslintrc file.
// Note that bug 1168340 will eventually help auto-registering globals
// from head.js files.
"no-undef": 0,
"block-scoped-var": 0
},
// All globals made available in the test environment.
"globals": {
"add_task": true,
"Assert": true,
"content": true,
"document": true,
"EventUtils": true,
"executeSoon": true,
"export_assertions": true,
"finish": true,
"gBrowser": true,
"gDevTools": true,
"getRootDirectory": true,
"getTestFilePath": true,
"gTestPath": true,
"info": true,
"is": true,
"isnot": true,
"navigator": true,
"ok": true,
"promise": true,
"registerCleanupFunction": true,
"requestLongerTimeout": true,
"setTimeout": true,
"SimpleTest": true,
"SpecialPowers": true,
"test": true,
"todo": true,
"todo_is": true,
"todo_isnot": true,
"waitForClipboard": true,
"waitForExplicitFinish": true,
"waitForFocus": true,
"window": true,
}
}

View File

@ -0,0 +1,51 @@
// Parent config file for all devtools browser mochitest files.
{
"rules": {
// Allow non-camelcase so that run_test doesn't produce a warning.
"camelcase": 0,
// Only disallow non-global unused vars, so that things like the test
// function do not produce errors.
"no-unused-vars": [2, {"vars": "local"}],
// Allow using undefined variables so that tests can refer to functions
// and variables defined in head.js files, without having to maintain a
// list of globals in each .eslintrc file.
// Note that bug 1168340 will eventually help auto-registering globals
// from head.js files.
"no-undef": 0,
"block-scoped-var": 0
},
// All globals made available in the test environment.
"globals": {
"add_task": true,
"add_test": true,
"Assert": true,
"deepEqual": true,
"do_check_eq": true,
"do_check_false": true,
"do_check_neq": true,
"do_check_null": true,
"do_check_true": true,
"do_execute_soon": true,
"do_get_cwd": true,
"do_get_file": true,
"do_get_idle": true,
"do_get_profile": true,
"do_load_module": true,
"do_parse_document": true,
"do_print": true,
"do_register_cleanup": true,
"do_test_finished": true,
"do_test_pending": true,
"do_throw": true,
"do_timeout": true,
"equal": true,
"load": true,
"notDeepEqual": true,
"notEqual": true,
"notStrictEqual": true,
"ok": true,
"run_next_test": true,
"run_test": true,
"strictEqual": true,
}
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -63,6 +63,7 @@ support-files =
[browser_cmd_jsb.js]
support-files =
browser_cmd_jsb_script.jsi
[browser_cmd_listen.js]
[browser_cmd_media.js]
support-files =
browser_cmd_media.html

View File

@ -0,0 +1,79 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the listen/unlisten commands work as they should.
const TEST_URI = "http://example.com/browser/browser/devtools/commandline/"+
"test/browser_cmd_cookie.html";
function test() {
return Task.spawn(testTask).then(finish, helpers.handleError);
}
let tests = {
testInput: function(options) {
return helpers.audit(options, [
{
setup: 'listen',
check: {
input: 'listen',
markup: 'VVVVVV',
status: 'VALID'
},
},
{
setup: 'unlisten',
check: {
input: 'unlisten',
markup: 'VVVVVVVV',
status: 'VALID'
},
exec: {
output: 'All TCP ports closed'
}
},
{
setup: function() {
return helpers.setInput(options, 'listen');
},
check: {
input: 'listen',
hints: ' [port]',
markup: 'VVVVVV',
status: 'VALID'
},
exec: {
output: 'Listening on port 6080'
}
},
{
setup: function() {
return helpers.setInput(options, 'listen 8000');
},
exec: {
output: 'Listening on port 8000'
}
},
{
setup: function() {
return helpers.setInput(options, 'unlisten');
},
exec: {
output: 'All TCP ports closed'
}
}
]);
},
};
function* testTask() {
Services.prefs.setBoolPref('devtools.debugger.remote-enabled', true);
let options = yield helpers.openTab(TEST_URI);
yield helpers.openToolbar(options);
yield helpers.runTests(options, tests);
yield helpers.closeToolbar(options);
yield helpers.closeTab(options);
Services.prefs.clearUserPref('devtools.debugger.remote-enabled');
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -104,6 +104,21 @@ const CATEGORY_MAPPINGS = {
* for `.marker-details-bullet.{COLORNAME}` for the equivilent
* entry in ./browser/themes/shared/devtools/performance.inc.css
* https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
* - collapseFunc: A function determining how markers are collapsed together.
* Invoked with 3 arguments: the current parent marker, the
* current marker and a method for peeking i markers ahead. If
* nothing is returned, the marker is added as a standalone entry
* in the waterfall. Otherwise, an object needs to be returned
* with the following properties:
* - toParent: The parent marker name (needs to be an entry in
* the `TIMELINE_BLUEPRINT` itself).
* - withData: An object containing some properties to staple
* on the parent marker.
* - forceNew: True if a new parent marker needs to be created
* even though there is one currently available
* with the same name.
* - forceEnd: True if the current parent marker is full after
* this collapse operation and should be finalized.
* - fields: An optional array of marker properties you wish to display in the
* marker details view. For example, a field in the array such as
* { property: "aCauseName", label: "Cause" } would render a string
@ -127,51 +142,64 @@ const TIMELINE_BLUEPRINT = {
"Styles": {
group: 0,
colorName: "graphs-purple",
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.styles2"),
fields: getStylesFields,
},
"Reflow": {
group: 0,
colorName: "graphs-purple",
label: L10N.getStr("timeline.label.reflow2")
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.reflow2"),
},
"Paint": {
group: 0,
colorName: "graphs-green",
label: L10N.getStr("timeline.label.paint")
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.paint"),
},
/* Group 1 - JS */
"DOMEvent": {
group: 1,
colorName: "graphs-yellow",
collapseFunc: collapseDOMIntoDOMJS,
label: L10N.getStr("timeline.label.domevent"),
fields: getDOMEventFields,
},
"Javascript": {
group: 1,
colorName: "graphs-yellow",
collapseFunc: either(collapseJSIntoDOMJS, collapseConsecutiveIdentical),
label: getJSLabel,
fields: getJSFields,
},
"meta::DOMEvent+JS": {
colorName: "graphs-yellow",
label: getDOMJSLabel,
fields: getDOMEventFields,
},
"Parse HTML": {
group: 1,
colorName: "graphs-yellow",
label: L10N.getStr("timeline.label.parseHTML")
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.parseHTML"),
},
"Parse XML": {
group: 1,
colorName: "graphs-yellow",
label: L10N.getStr("timeline.label.parseXML")
collapseFunc: collapseConsecutiveIdentical,
label: L10N.getStr("timeline.label.parseXML"),
},
"GarbageCollection": {
group: 1,
colorName: "graphs-red",
collapseFunc: collapseAdjacentGC,
label: getGCLabel,
fields: [
{ property: "causeName", label: "Reason:" },
{ property: "nonincrementalReason", label: "Non-incremental Reason:" }
]
],
},
/* Group 2 - User Controlled */
@ -182,7 +210,7 @@ const TIMELINE_BLUEPRINT = {
fields: [{
property: "causeName",
label: L10N.getStr("timeline.markerDetail.consoleTimerName")
}]
}],
},
"TimeStamp": {
group: 2,
@ -191,10 +219,83 @@ const TIMELINE_BLUEPRINT = {
fields: [{
property: "causeName",
label: "Label:"
}]
}],
},
};
/**
* Helper for creating a function that returns the first defined result from
* a list of functions passed in as params, in order.
* @param ...function fun
* @return any
*/
function either(...fun) {
return function() {
for (let f of fun) {
let result = f.apply(null, arguments);
if (result !== undefined) return result;
}
}
}
/**
* A series of collapsers used by the blueprint. These functions are
* consecutively invoked on a moving window of two markers.
*/
function collapseConsecutiveIdentical(parent, curr, peek) {
// If there is a parent marker currently being filled and the current marker
// should go into the parent marker, make it so.
if (parent && parent.name == curr.name) {
return { toParent: parent.name };
}
// Otherwise if the current marker is the same type as the next marker type,
// create a new parent marker containing the current marker.
let next = peek(1);
if (next && curr.name == next.name) {
return { toParent: curr.name };
}
}
function collapseAdjacentGC(parent, curr, peek) {
let next = peek(1);
if (next && (next.start < curr.end || next.start - curr.end <= 10 /* ms */)) {
return collapseConsecutiveIdentical(parent, curr, peek);
}
}
function collapseDOMIntoDOMJS(parent, curr, peek) {
// If the next marker is a JavaScript marker, create a new meta parent marker
// containing the current marker.
let next = peek(1);
if (next && next.name == "Javascript") {
return {
forceNew: true,
toParent: "meta::DOMEvent+JS",
withData: {
type: curr.type,
eventPhase: curr.eventPhase
},
};
}
}
function collapseJSIntoDOMJS(parent, curr, peek) {
// If there is a parent marker currently being filled, and it's the one
// created from a `DOMEvent` via `collapseDOMIntoDOMJS`, then the current
// marker has to go into that one.
if (parent && parent.name == "meta::DOMEvent+JS") {
return {
forceEnd: true,
toParent: "meta::DOMEvent+JS",
withData: {
stack: curr.stack,
endStack: curr.endStack
},
};
}
}
/**
* A series of formatters used by the blueprint.
*/
@ -236,6 +337,10 @@ function getJSLabel (marker={}) {
return generic;
}
function getDOMJSLabel (marker={}) {
return `Event (${marker.type})`;
}
/**
* Returns a hash for computing a fields object for a JS marker. If the cause
* is considered content (so an entry exists in the JS_MARKER_MAP), do not display it

View File

@ -0,0 +1,122 @@
/* 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";
/**
* Utility functions for collapsing markers into a waterfall.
*/
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/performance/global", true);
/**
* Collapses markers into a tree-like structure. Currently, this only goes
* one level deep.
* @param object markerNode
* @param array markersList
*/
function collapseMarkersIntoNode({ markerNode, markersList }) {
let [getOrCreateParentNode, getCurrentParentNode, clearParentNode] = makeParentNodeFactory();
for (let i = 0, len = markersList.length; i < len; i++) {
let curr = markersList[i];
let blueprint = TIMELINE_BLUEPRINT[curr.name];
let parentNode = getCurrentParentNode();
let collapse = blueprint.collapseFunc || (() => null);
let peek = distance => markersList[i + distance];
let collapseInfo = collapse(parentNode, curr, peek);
if (collapseInfo) {
let { toParent, withData, forceNew, forceEnd } = collapseInfo;
// If the `forceNew` prop is set on the collapse info, then a new parent
// marker needs to be created even if there is one already available.
if (forceNew) {
clearParentNode();
}
// If the `toParent` prop is set on the collapse info, then this marker
// can be collapsed into a higher-level parent marker.
if (toParent) {
let parentNode = getOrCreateParentNode(markerNode, toParent, curr.start);
parentNode.end = curr.end;
parentNode.submarkers.push(curr);
for (let key in withData) {
parentNode[key] = withData[key];
}
}
// If the `forceEnd` prop is set on the collapse info, then the higher-level
// parent marker is full and should be finalized.
if (forceEnd) {
clearParentNode();
}
} else {
clearParentNode();
markerNode.submarkers.push(curr);
}
}
}
/**
* Creates an empty parent marker, which functions like a regular marker,
* but is able to hold additional child markers.
* @param string name
* @param number start [optional]
* @param number end [optional]
* @return object
*/
function makeEmptyMarkerNode(name, start, end) {
return {
name: name,
start: start,
end: end,
submarkers: []
};
}
/**
* Creates a factory for markers containing other markers.
* @return array[function]
*/
function makeParentNodeFactory() {
let marker;
return [
/**
* Gets the current parent marker for the given marker name. If it doesn't
* exist, it creates it and appends it to another parent marker.
* @param object owner
* @param string name
* @param number start
* @return object
*/
function getOrCreateParentNode(owner, name, start) {
if (marker && marker.name == name) {
return marker;
} else {
marker = makeEmptyMarkerNode(name, start);
owner.submarkers.push(marker);
return marker;
}
},
/**
* Gets the current marker marker.
* @return object
*/
function getCurrentParentNode() {
return marker;
},
/**
* Clears the current marker marker.
*/
function clearParentNode() {
marker = null;
}
];
}
exports.makeEmptyMarkerNode = makeEmptyMarkerNode;
exports.collapseMarkersIntoNode = collapseMarkersIntoNode;

View File

@ -8,7 +8,6 @@
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
@ -29,27 +28,29 @@ loader.lazyRequireGetter(this, "MarkerUtils",
*/
function MarkerDetails(parent, splitter) {
EventEmitter.decorate(this);
this._onClick = this._onClick.bind(this);
this._document = parent.ownerDocument;
this._parent = parent;
this._splitter = splitter;
this._splitter.addEventListener("mouseup", () => this.emit("resize"));
this._onClick = this._onClick.bind(this);
this._onSplitterMouseUp = this._onSplitterMouseUp.bind(this);
this._parent.addEventListener("click", this._onClick);
this._splitter.addEventListener("mouseup", this._onSplitterMouseUp);
}
MarkerDetails.prototype = {
/**
* Removes any node references from this view.
* Sets this view's width.
* @param boolean
*/
destroy: function() {
this.empty();
this._parent.removeEventListener("click", this._onClick);
this._parent = null;
this._splitter = null;
set width(value) {
this._parent.setAttribute("width", value);
},
/**
* Clears the view.
* Clears the marker details from this view.
*/
empty: function() {
this._parent.innerHTML = "";
@ -60,8 +61,8 @@ MarkerDetails.prototype = {
*
* @param object params
* An options object holding:
* marker - The marker to display.
* frames - Array of stack frame information; see stack.js.
* - marker: The marker to display.
* - frames: Array of stack frame information; see stack.js.
*/
render: function({ marker, frames }) {
this.empty();
@ -69,10 +70,10 @@ MarkerDetails.prototype = {
let elements = [];
elements.push(MarkerUtils.DOM.buildTitle(this._document, marker));
elements.push(MarkerUtils.DOM.buildDuration(this._document, marker));
MarkerUtils.DOM.buildFields(this._document, marker).forEach(field => elements.push(field));
MarkerUtils.DOM.buildFields(this._document, marker).forEach(f => elements.push(f));
// Build a stack element -- and use the "startStack" label if
// we have both a star and endStack.
// we have both a startStack and endStack.
if (marker.stack) {
let type = marker.endStack ? "startStack" : "stack";
elements.push(MarkerUtils.DOM.buildStackTrace(this._document, {
@ -98,6 +99,13 @@ MarkerDetails.prototype = {
this.emit("view-source", data.url, data.line);
}
},
/**
* Handles the "mouseup" event on the marker details view splitter.
*/
_onSplitterMouseUp: function() {
this.emit("resize");
}
};
/**

View File

@ -0,0 +1,305 @@
/* 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";
/**
* This file contains the "marker" view, essentially a detailed list
* of all the markers in the timeline data.
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
const { Heritage } = require("resource:///modules/devtools/ViewHelpers.jsm");
const { AbstractTreeItem } = require("resource:///modules/devtools/AbstractTreeItem.jsm");
const { TIMELINE_BLUEPRINT: ORIGINAL_BP } = require("devtools/performance/global");
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const LEVEL_INDENT = 10; // px
const ARROW_NODE_OFFSET = -15; // px
const WATERFALL_MARKER_SIDEBAR_WIDTH = 175; // px
const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5; // px
/**
* A detailed waterfall view for the timeline data.
*
* @param MarkerView owner
* The MarkerView considered the "owner" marker. This newly created
* instance will be represent the "submarker". Should be null for root nodes.
* @param object marker
* Details about this marker, like { name, start, end, submarkers } etc.
* @param number level [optional]
* The indentation level in the waterfall tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
* calculations. Defaults to false.
*/
function MarkerView({ owner, marker, level, hidden }) {
AbstractTreeItem.call(this, {
parent: owner,
level: level|0 - (hidden ? 1 : 0)
});
this.marker = marker;
this.hidden = !!hidden;
this._onItemBlur = this._onItemBlur.bind(this);
this._onItemFocus = this._onItemFocus.bind(this);
}
MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
/**
* Calculates and stores the available width for the waterfall.
* This should be invoked every time the container node is resized.
*/
recalculateBounds: function() {
this.root._waterfallWidth = this.bounds.width - WATERFALL_MARKER_SIDEBAR_WIDTH;
},
/**
* Sets a list of names and colors used to paint markers.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
* @param object blueprint
*/
set blueprint(blueprint) {
this.root._blueprint = blueprint;
},
get blueprint() {
return this.root._blueprint;
},
/**
* Sets the { startTime, endTime }, in milliseconds.
* @param object interval
*/
set interval(interval) {
this.root._interval = interval;
},
get interval() {
return this.root._interval;
},
/**
* Gets the current waterfall width.
* @return number
*/
getWaterfallWidth: function() {
return this._waterfallWidth;
},
/**
* Gets the data scale amount for the current width and interval.
* @return number
*/
getDataScale: function() {
let startTime = this.root._interval.startTime|0;
let endTime = this.root._interval.endTime|0;
return this.root._waterfallWidth / (endTime - startTime);
},
/**
* Creates the view for this waterfall node.
* @param nsIDOMNode document
* @param nsIDOMNode arrowNode
* @return nsIDOMNode
*/
_displaySelf: function(document, arrowNode) {
let targetNode = document.createElement("hbox");
targetNode.className = "waterfall-tree-item";
if (this == this.root) {
// Bounds are needed for properly positioning and scaling markers in
// the waterfall, but it's sufficient to make those calculations only
// for the root node.
this.root.recalculateBounds();
// The AbstractTreeItem propagates events to the root, so we don't
// need to listen them on descendant items in the tree.
this._addEventListeners();
} else {
// Root markers are an implementation detail and shouldn't be shown.
this._buildMarkerCells(document, targetNode, arrowNode);
}
if (this.hidden) {
targetNode.style.display = "none";
}
return targetNode;
},
/**
* Populates this node in the waterfall tree with the corresponding "markers".
* @param array:AbstractTreeItem children
*/
_populateSelf: function(children) {
let submarkers = this.marker.submarkers;
if (!submarkers || !submarkers.length) {
return;
}
let blueprint = this.root._blueprint;
let startTime = this.root._interval.startTime;
let endTime = this.root._interval.endTime;
let newLevel = this.level + 1;
for (let i = 0, len = submarkers.length; i < len; i++) {
let marker = submarkers[i];
// If this marker isn't in the global timeline blueprint, don't display
// it, but dump a warning message to the console.
if (!(marker.name in blueprint)) {
if (!(marker.name in ORIGINAL_BP)) {
console.warn(`Marker not found in timeline blueprint: ${marker.name}.`);
}
continue;
}
if (!isMarkerInRange(marker, startTime|0, endTime|0)) {
continue;
}
children.push(new MarkerView({
owner: this,
marker: marker,
level: newLevel,
inverted: this.inverted
}));
}
},
/**
* Builds all the nodes representing a marker in the waterfall.
* @param nsIDOMNode document
* @param nsIDOMNode targetNode
* @param nsIDOMNode arrowNode
*/
_buildMarkerCells: function(doc, targetNode, arrowNode) {
// Root markers are an implementation detail and shouldn't be shown.
let marker = this.marker;
if (marker.name == "(root)") {
return;
}
let style = this.root._blueprint[marker.name];
let startTime = this.root._interval.startTime;
let endTime = this.root._interval.endTime;
let sidebarCell = this._buildMarkerSidebar(
doc, style, marker);
let timebarCell = this._buildMarkerTimebar(
doc, style, marker, startTime, endTime, arrowNode);
targetNode.appendChild(sidebarCell);
targetNode.appendChild(timebarCell);
// Don't render an expando-arrow for leaf nodes.
let submarkers = this.marker.submarkers;
let hasDescendants = submarkers && submarkers.length > 0;
if (hasDescendants) {
targetNode.setAttribute("expandable", "");
} else {
arrowNode.setAttribute("invisible", "");
}
targetNode.setAttribute("level", this.level);
},
/**
* Functions creating each cell in this waterfall view.
* Invoked by `_displaySelf`.
*/
_buildMarkerSidebar: function(doc, style, marker) {
let cell = doc.createElement("hbox");
cell.className = "waterfall-sidebar theme-sidebar";
cell.setAttribute("width", WATERFALL_MARKER_SIDEBAR_WIDTH);
cell.setAttribute("align", "center");
let bullet = doc.createElement("hbox");
bullet.className = `waterfall-marker-bullet marker-color-${style.colorName}`;
bullet.style.transform = `translateX(${this.level * LEVEL_INDENT}px)`;
bullet.setAttribute("type", marker.name);
cell.appendChild(bullet);
let name = doc.createElement("description");
let label = MarkerUtils.getMarkerLabel(marker);
name.className = "plain waterfall-marker-name";
name.style.transform = `translateX(${this.level * LEVEL_INDENT}px)`;
name.setAttribute("crop", "end");
name.setAttribute("flex", "1");
name.setAttribute("value", label);
name.setAttribute("tooltiptext", label);
cell.appendChild(name);
return cell;
},
_buildMarkerTimebar: function(doc, style, marker, startTime, endTime, arrowNode) {
let cell = doc.createElement("hbox");
cell.className = "waterfall-marker waterfall-background-ticks";
cell.setAttribute("align", "center");
cell.setAttribute("flex", "1");
let dataScale = this.getDataScale();
let offset = (marker.start - startTime) * dataScale;
let width = (marker.end - marker.start) * dataScale;
arrowNode.style.transform =`translateX(${offset + ARROW_NODE_OFFSET}px)`;
cell.appendChild(arrowNode);
let bar = doc.createElement("hbox");
bar.className = `waterfall-marker-bar marker-color-${style.colorName}`;
bar.style.transform = `translateX(${offset}px)`;
bar.setAttribute("type", marker.name);
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_TIMEBAR_WIDTH_MIN));
cell.appendChild(bar);
return cell;
},
/**
* Adds the event listeners for this particular tree item.
*/
_addEventListeners: function() {
this.on("focus", this._onItemFocus);
this.on("blur", this._onItemBlur);
},
/**
* Handler for the "blur" event on the root item.
*/
_onItemBlur: function() {
this.root.emit("unselected");
},
/**
* Handler for the "mousedown" event on the root item.
*/
_onItemFocus: function(e, item) {
this.root.emit("selected", item.marker);
}
});
/**
* Checks if a given marker is in the specified time range.
*
* @param object e
* The marker containing the { start, end } timestamps.
* @param number start
* The earliest allowed time.
* @param number end
* The latest allowed time.
* @return boolean
* True if the marker fits inside the specified time range.
*/
function isMarkerInRange(e, start, end) {
let m_start = e.start|0;
let m_end = e.end|0;
return (m_start >= start && m_end <= end) || // bounds inside
(m_start < start && m_end > end) || // bounds outside
(m_start < start && m_end >= start && m_end <= end) || // overlap start
(m_end > end && m_start >= start && m_start <= end); // overlap end
}
exports.MarkerView = MarkerView;
exports.WATERFALL_MARKER_SIDEBAR_WIDTH = WATERFALL_MARKER_SIDEBAR_WIDTH;

View File

@ -19,6 +19,8 @@ loader.lazyRequireGetter(this, "getColor",
"devtools/shared/theme", true);
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "TickUtils",
"devtools/performance/waterfall-ticks", true);
const OVERVIEW_HEADER_HEIGHT = 14; // px
const OVERVIEW_ROW_HEIGHT = 11; // px
@ -75,7 +77,7 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
for (let type in blueprint) {
this._paintBatches.set(type, { style: blueprint[type], batch: [] });
this._lastGroup = Math.max(this._lastGroup, blueprint[type].group);
this._lastGroup = Math.max(this._lastGroup, blueprint[type].group || 0);
}
},
@ -143,7 +145,12 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
let tickInterval = this._findOptimalTickInterval(dataScale);
let tickInterval = TickUtils.findOptimalTickInterval({
ticksMultiple: OVERVIEW_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: OVERVIEW_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
ctx.textBaseline = "middle";
ctx.font = fontSize + "px " + fontFamily;
@ -190,32 +197,6 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
return canvas;
},
/**
* Finds the optimal tick interval between time markers in this overview.
*/
_findOptimalTickInterval: function(dataScale) {
let timingStep = OVERVIEW_HEADER_TICKS_MULTIPLE;
let spacingMin = OVERVIEW_HEADER_TICKS_SPACING_MIN * this._pixelRatio;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > spacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < spacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
},
/**
* Sets the theme via `theme` to either "light" or "dark",
* and updates the internal styling to match. Requires a redraw

View File

@ -56,11 +56,11 @@ const sum = vals => vals.reduce((a, b) => a + b, 0);
* parent node is used for all rows.
*
* @param CallView caller
* The CallView considered the "caller" frame. This instance will be
* represent the "callee". Should be null for root nodes.
* The CallView considered the "caller" frame. This newly created
* instance will be represent the "callee". Should be null for root nodes.
* @param ThreadNode | FrameNode frame
* Details about this function, like { samples, duration, calls } etc.
* @param number level
* @param number level [optional]
* The indentation level in the call tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
@ -213,7 +213,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
* Invoked by `_displaySelf`.
*/
_createTimeCell: function(doc, duration, isSelf = false) {
let cell = doc.createElement("label");
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-duration" : "duration");
cell.setAttribute("crop", "end");
@ -221,7 +221,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
return cell;
},
_createExecutionCell: function(doc, percentage, isSelf = false) {
let cell = doc.createElement("label");
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-percentage" : "percentage");
cell.setAttribute("crop", "end");
@ -229,7 +229,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
return cell;
},
_createAllocationsCell: function(doc, count, isSelf = false) {
let cell = doc.createElement("label");
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", isSelf ? "self-allocations" : "allocations");
cell.setAttribute("crop", "end");
@ -237,7 +237,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
return cell;
},
_createSamplesCell: function(doc, count) {
let cell = doc.createElement("label");
let cell = doc.createElement("description");
cell.className = "plain call-tree-cell";
cell.setAttribute("type", "samples");
cell.setAttribute("crop", "end");
@ -254,7 +254,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
// Don't render a name label node if there's no function name. A different
// location label node will be rendered instead.
if (frameName) {
let nameNode = doc.createElement("label");
let nameNode = doc.createElement("description");
nameNode.className = "plain call-tree-name";
nameNode.setAttribute("flex", "1");
nameNode.setAttribute("crop", "end");
@ -277,7 +277,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
},
_appendFunctionDetailsCells: function(doc, cell, frameInfo) {
if (frameInfo.fileName) {
let urlNode = doc.createElement("label");
let urlNode = doc.createElement("description");
urlNode.className = "plain call-tree-url";
urlNode.setAttribute("flex", "1");
urlNode.setAttribute("crop", "end");
@ -288,21 +288,21 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
}
if (frameInfo.line) {
let lineNode = doc.createElement("label");
let lineNode = doc.createElement("description");
lineNode.className = "plain call-tree-line";
lineNode.setAttribute("value", ":" + frameInfo.line);
cell.appendChild(lineNode);
}
if (frameInfo.column) {
let columnNode = doc.createElement("label");
let columnNode = doc.createElement("description");
columnNode.className = "plain call-tree-column";
columnNode.setAttribute("value", ":" + frameInfo.column);
cell.appendChild(columnNode);
}
if (frameInfo.host) {
let hostNode = doc.createElement("label");
let hostNode = doc.createElement("description");
hostNode.className = "plain call-tree-host";
hostNode.setAttribute("value", frameInfo.host);
cell.appendChild(hostNode);
@ -313,7 +313,7 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
cell.appendChild(spacerNode);
if (frameInfo.categoryData.label) {
let categoryNode = doc.createElement("label");
let categoryNode = doc.createElement("description");
categoryNode.className = "plain call-tree-category";
categoryNode.style.color = frameInfo.categoryData.color;
categoryNode.setAttribute("value", frameInfo.categoryData.label);

View File

@ -0,0 +1,187 @@
/* 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";
/**
* This file contains the "waterfall ticks" view, a header for the
* markers displayed in the waterfall.
*/
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "WATERFALL_MARKER_SIDEBAR_WIDTH",
"devtools/performance/marker-view", true);
const HTML_NS = "http://www.w3.org/1999/xhtml";
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
/**
* A header for a markers waterfall.
*
* @param MarkerView root
* The root item of the waterfall tree.
*/
function WaterfallHeader(root) {
this.root = root;
}
WaterfallHeader.prototype = {
/**
* Creates and appends this header as the first element of the specified
* parent element.
*
* @param nsIDOMNode parentNode
* The parent element for this header.
*/
attachTo: function(parentNode) {
let document = parentNode.ownerDocument;
let startTime = this.root.interval.startTime;
let dataScale = this.root.getDataScale();
let waterfallWidth = this.root.getWaterfallWidth();
let header = this._buildNode(document, startTime, dataScale, waterfallWidth);
parentNode.insertBefore(header, parentNode.firstChild);
this._drawWaterfallBackground(document, dataScale, waterfallWidth);
},
/**
* Creates the node displaying this view.
*/
_buildNode: function(doc, startTime, dataScale, waterfallWidth) {
let container = doc.createElement("hbox");
container.className = "waterfall-header-container";
container.setAttribute("flex", "1");
let sidebar = doc.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_MARKER_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
container.appendChild(sidebar);
let name = doc.createElement("description");
name.className = "plain waterfall-header-name";
name.setAttribute("value", L10N.getStr("timeline.records"));
sidebar.appendChild(name);
let ticks = doc.createElement("hbox");
ticks.className = "waterfall-header-ticks waterfall-background-ticks";
ticks.setAttribute("align", "center");
ticks.setAttribute("flex", "1");
container.appendChild(ticks);
let tickInterval = findOptimalTickInterval({
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
for (let x = 0; x < waterfallWidth; x += tickInterval) {
let left = x + WATERFALL_HEADER_TEXT_PADDING;
let time = Math.round(x / dataScale + startTime);
let label = L10N.getFormatStr("timeline.tick", time);
let node = doc.createElement("description");
node.className = "plain waterfall-header-tick";
node.style.transform = "translateX(" + left + "px)";
node.setAttribute("value", label);
ticks.appendChild(node);
}
return container;
},
/**
* Creates the background displayed on the marker's waterfall.
*/
_drawWaterfallBackground: function(doc, dataScale, waterfallWidth) {
if (!this._canvas || !this._ctx) {
this._canvas = doc.createElementNS(HTML_NS, "canvas");
this._ctx = this._canvas.getContext("2d");
}
let canvas = this._canvas;
let ctx = this._ctx;
// Nuke the context.
let canvasWidth = canvas.width = waterfallWidth;
let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
// Start over.
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let view8bit = new Uint8ClampedArray(buf);
let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
let tickInterval = findOptimalTickInterval({
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
dataScale: dataScale
});
// Insert one pixel for each division on each scale.
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
let increment = tickInterval * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
let position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
doc.mozSetImageElement("waterfall-background", canvas);
}
};
/**
* Finds the optimal tick interval between time markers in this timeline.
*
* @param number ticksMultiple
* @param number ticksSpacingMin
* @param number dataScale
* @return number
*/
function findOptimalTickInterval({ ticksMultiple, ticksSpacingMin, dataScale }) {
let timingStep = ticksMultiple;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > ticksSpacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < ticksSpacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
}
exports.WaterfallHeader = WaterfallHeader;
exports.TickUtils = { findOptimalTickInterval };

View File

@ -1,620 +0,0 @@
/* 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";
/**
* This file contains the "waterfall" view, essentially a detailed list
* of all the markers in the timeline data.
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
loader.lazyRequireGetter(this, "L10N",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
loader.lazyImporter(this, "setNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyImporter(this, "clearNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const WATERFALL_SIDEBAR_WIDTH = 200; // px
const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
const WATERFALL_ROWCOUNT_ONPAGEUPDOWN = 10;
/**
* A detailed waterfall view for the timeline data.
*
* @param nsIDOMNode parent
* The parent node holding the waterfall.
* @param nsIDOMNode container
* The container node that key events should be bound to.
* @param Object blueprint
* List of names and colors defining markers.
*/
function Waterfall(parent, container, blueprint) {
EventEmitter.decorate(this);
this._parent = parent;
this._document = parent.ownerDocument;
this._container = container;
this._fragment = this._document.createDocumentFragment();
this._outstandingMarkers = [];
this._headerContents = this._document.createElement("hbox");
this._headerContents.className = "waterfall-header-contents";
this._parent.appendChild(this._headerContents);
this._listContents = this._document.createElement("vbox");
this._listContents.className = "waterfall-list-contents";
this._listContents.setAttribute("flex", "1");
this._parent.appendChild(this._listContents);
this.setupKeys();
this._isRTL = this._getRTL();
// Lazy require is a bit slow, and these are hot objects.
this._l10n = L10N;
this._blueprint = blueprint;
this._setNamedTimeout = setNamedTimeout;
this._clearNamedTimeout = clearNamedTimeout;
// Selected row index. By default, we want the first
// row to be selected.
this._selectedRowIdx = 0;
// Default rowCount
this.rowCount = WATERFALL_ROWCOUNT_ONPAGEUPDOWN;
}
Waterfall.prototype = {
/**
* Removes any node references from this view.
*/
destroy: function() {
this._parent = this._document = this._container = null;
},
/**
* Populates this view with the provided data source.
*
* @param object data
* An object containing the following properties:
* - markers: a list of markers received from the controller
* - interval: the { startTime, endTime }, in milliseconds
*/
setData: function({ markers, interval }) {
this.clearView();
this._markers = markers;
this._interval = interval;
let { startTime, endTime } = interval;
let dataScale = this._waterfallWidth / (endTime - startTime);
this._drawWaterfallBackground(dataScale);
this._buildHeader(this._headerContents, startTime, dataScale);
this._buildMarkers(this._listContents, markers, startTime, endTime, dataScale);
this.selectRow(this._selectedRowIdx);
},
/**
* List of names and colors used to paint markers.
* @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
*/
setBlueprint: function(blueprint) {
this._blueprint = blueprint;
},
/**
* Keybindings.
*/
setupKeys: function() {
let pane = this._container;
pane.addEventListener("keydown", e => {
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx - 1);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx + 1);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_HOME) {
e.preventDefault();
this.selectNearestRow(0);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_END) {
e.preventDefault();
this.selectNearestRow(this._listContents.children.length);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx - this.rowCount);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx + this.rowCount);
}
}, true);
},
/**
* Depopulates this view.
*/
clearView: function() {
while (this._headerContents.hasChildNodes()) {
this._headerContents.firstChild.remove();
}
while (this._listContents.hasChildNodes()) {
this._listContents.firstChild.remove();
}
this._listContents.scrollTop = 0;
this._outstandingMarkers.length = 0;
this._clearNamedTimeout("flush-outstanding-markers");
},
/**
* Calculates and stores the available width for the waterfall.
* This should be invoked every time the container window is resized.
*/
recalculateBounds: function() {
let bounds = this._parent.getBoundingClientRect();
this._waterfallWidth = bounds.width - WATERFALL_SIDEBAR_WIDTH;
},
/**
* Creates the header part of this view.
*
* @param nsIDOMNode parent
* The parent node holding the header.
* @param number startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildHeader: function(parent, startTime, dataScale) {
let container = this._document.createElement("hbox");
container.className = "waterfall-header-container";
container.setAttribute("flex", "1");
let sidebar = this._document.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
container.appendChild(sidebar);
let name = this._document.createElement("label");
name.className = "plain waterfall-header-name";
name.setAttribute("value", this._l10n.getStr("timeline.records"));
sidebar.appendChild(name);
let ticks = this._document.createElement("hbox");
ticks.className = "waterfall-header-ticks waterfall-background-ticks";
ticks.setAttribute("align", "center");
ticks.setAttribute("flex", "1");
container.appendChild(ticks);
let offset = this._isRTL ? this._waterfallWidth : 0;
let direction = this._isRTL ? -1 : 1;
let tickInterval = this._findOptimalTickInterval({
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
dataScale: dataScale
});
for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
let left = x + direction * WATERFALL_HEADER_TEXT_PADDING;
let time = Math.round(x / dataScale + startTime);
let label = this._l10n.getFormatStr("timeline.tick", time);
let node = this._document.createElement("label");
node.className = "plain waterfall-header-tick";
node.style.transform = "translateX(" + (left - offset) + "px)";
node.setAttribute("value", label);
ticks.appendChild(node);
}
parent.appendChild(container);
},
/**
* Creates the markers part of this view.
*
* @param nsIDOMNode parent
* The parent node holding the markers.
* @param number startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildMarkers: function(parent, markers, startTime, endTime, dataScale) {
let rowsCount = 0;
let markerIdx = -1;
for (let marker of markers) {
markerIdx++;
if (!isMarkerInRange(marker, startTime, endTime)) {
continue;
}
if (!(marker.name in this._blueprint)) {
continue;
}
// Only build and display a finite number of markers initially, to
// preserve a snappy UI. After a certain delay, continue building the
// outstanding markers while there's (hopefully) no user interaction.
let arguments_ = [this._fragment, marker, startTime, dataScale, markerIdx, rowsCount];
if (rowsCount++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
this._buildMarker.apply(this, arguments_);
} else {
this._outstandingMarkers.push(arguments_);
}
}
// If there are no outstanding markers, add a dummy "spacer" at the end
// to fill up any remaining available space in the UI.
if (!this._outstandingMarkers.length) {
this._buildMarker(this._fragment, null);
}
// Otherwise prepare flushing the outstanding markers after a small delay.
else {
let delay = WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY;
let func = () => this._buildOutstandingMarkers(parent);
this._setNamedTimeout("flush-outstanding-markers", delay, func);
}
parent.appendChild(this._fragment);
},
/**
* Finishes building the outstanding markers in this view.
* @see Waterfall.prototype._buildMarkers
*/
_buildOutstandingMarkers: function(parent) {
if (!this._outstandingMarkers.length) {
return;
}
for (let args of this._outstandingMarkers) {
this._buildMarker.apply(this, args);
}
this._outstandingMarkers.length = 0;
parent.appendChild(this._fragment);
this.selectRow(this._selectedRowIdx);
},
/**
* Creates a single marker in this view.
*
* @param nsIDOMNode parent
* The parent node holding the marker.
* @param object marker
* The { name, start, end } marker in the data source.
* @param startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
* @param number markerIdx
* Index of the marker in this._markers
* @param number rowIdx
* Index of current row
*/
_buildMarker: function(parent, marker, startTime, dataScale, markerIdx, rowIdx) {
let container = this._document.createElement("hbox");
container.setAttribute("markerIdx", markerIdx);
container.className = "waterfall-marker-container";
if (marker) {
this._buildMarkerSidebar(container, marker);
this._buildMarkerWaterfall(container, marker, startTime, dataScale, markerIdx);
container.onclick = () => this.selectRow(rowIdx);
} else {
this._buildMarkerSpacer(container);
container.setAttribute("flex", "1");
container.setAttribute("is-spacer", "");
}
parent.appendChild(container);
},
/**
* Select first row.
*/
resetSelection: function() {
this.selectRow(0);
},
/**
* Select a marker in the waterfall.
*
* @param number idx
* Index of the row to select. -1 clears the selection.
*/
selectRow: function(idx) {
let prev = this._listContents.children[this._selectedRowIdx];
if (prev) {
prev.classList.remove("selected");
}
this._selectedRowIdx = idx;
let row = this._listContents.children[idx];
if (row && !row.hasAttribute("is-spacer")) {
row.focus();
row.classList.add("selected");
let markerIdx = row.getAttribute("markerIdx");
this.emit("selected", this._markers[markerIdx]);
this.ensureRowIsVisible(row);
} else {
this.emit("unselected");
}
},
/**
* Find a valid row to select.
*
* @param number idx
* Index of the row to select.
*/
selectNearestRow: function(idx) {
if (this._listContents.children.length == 0) {
return;
}
idx = Math.max(idx, 0);
idx = Math.min(idx, this._listContents.children.length - 1);
let row = this._listContents.children[idx];
if (row && row.hasAttribute("is-spacer")) {
if (idx > 0) {
return this.selectNearestRow(idx - 1);
} else {
return;
}
}
this.selectRow(idx);
},
/**
* Scroll waterfall to ensure row is in the viewport.
*
* @param number idx
* Index of the row to select.
*/
ensureRowIsVisible: function(row) {
let parent = row.parentNode;
let parentRect = parent.getBoundingClientRect();
let rowRect = row.getBoundingClientRect();
let yDelta = rowRect.top - parentRect.top;
if (yDelta < 0) {
parent.scrollTop += yDelta;
}
yDelta = parentRect.bottom - rowRect.bottom;
if (yDelta < 0) {
parent.scrollTop -= yDelta;
}
},
/**
* Creates the sidebar part of a marker in this view.
*
* @param nsIDOMNode container
* The container node representing the marker in this view.
* @param object marker
* @see Waterfall.prototype._buildMarker
*/
_buildMarkerSidebar: function(container, marker) {
let blueprint = this._blueprint[marker.name];
let sidebar = this._document.createElement("hbox");
sidebar.className = "waterfall-sidebar theme-sidebar";
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
sidebar.setAttribute("align", "center");
let bullet = this._document.createElement("hbox");
bullet.className = `waterfall-marker-bullet marker-color-${blueprint.colorName}`;
bullet.setAttribute("type", marker.name);
sidebar.appendChild(bullet);
let name = this._document.createElement("label");
name.setAttribute("crop", "end");
name.setAttribute("flex", "1");
name.className = "plain waterfall-marker-name";
let label = MarkerUtils.getMarkerLabel(marker);
name.setAttribute("value", label);
name.setAttribute("tooltiptext", label);
sidebar.appendChild(name);
container.appendChild(sidebar);
},
/**
* Creates the waterfall part of a marker in this view.
*
* @param nsIDOMNode container
* The container node representing the marker.
* @param object marker
* @see Waterfall.prototype._buildMarker
* @param startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_buildMarkerWaterfall: function(container, marker, startTime, dataScale) {
let blueprint = this._blueprint[marker.name];
let waterfall = this._document.createElement("hbox");
waterfall.className = "waterfall-marker-item waterfall-background-ticks";
waterfall.setAttribute("align", "center");
waterfall.setAttribute("flex", "1");
let start = (marker.start - startTime) * dataScale;
let width = (marker.end - marker.start) * dataScale;
let offset = this._isRTL ? this._waterfallWidth : 0;
let bar = this._document.createElement("hbox");
bar.className = `waterfall-marker-bar marker-color-${blueprint.colorName}`;
bar.style.transform = "translateX(" + (start - offset) + "px)";
bar.setAttribute("type", marker.name);
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_BAR_WIDTH_MIN));
waterfall.appendChild(bar);
container.appendChild(waterfall);
},
/**
* Creates a dummy spacer as an empty marker.
*
* @param nsIDOMNode container
* The container node representing the marker.
*/
_buildMarkerSpacer: function(container) {
let sidebarSpacer = this._document.createElement("spacer");
sidebarSpacer.className = "waterfall-sidebar theme-sidebar";
sidebarSpacer.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
let waterfallSpacer = this._document.createElement("spacer");
waterfallSpacer.className = "waterfall-marker-item waterfall-background-ticks";
waterfallSpacer.setAttribute("flex", "1");
container.appendChild(sidebarSpacer);
container.appendChild(waterfallSpacer);
},
/**
* Creates the background displayed on the marker's waterfall.
*
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_drawWaterfallBackground: function(dataScale) {
if (!this._canvas || !this._ctx) {
this._canvas = this._document.createElementNS(HTML_NS, "canvas");
this._ctx = this._canvas.getContext("2d");
}
let canvas = this._canvas;
let ctx = this._ctx;
// Nuke the context.
let canvasWidth = canvas.width = this._waterfallWidth;
let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
// Start over.
let imageData = ctx.createImageData(canvasWidth, canvasHeight);
let pixelArray = imageData.data;
let buf = new ArrayBuffer(pixelArray.length);
let view8bit = new Uint8ClampedArray(buf);
let view32bit = new Uint32Array(buf);
// Build new millisecond tick lines...
let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
let tickInterval = this._findOptimalTickInterval({
ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
dataScale: dataScale
});
// Insert one pixel for each division on each scale.
for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
let increment = tickInterval * Math.pow(2, i);
for (let x = 0; x < canvasWidth; x += increment) {
let position = x | 0;
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
}
alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
}
// Flush the image data and cache the waterfall background.
pixelArray.set(view8bit);
ctx.putImageData(imageData, 0, 0);
this._document.mozSetImageElement("waterfall-background", canvas);
},
/**
* Finds the optimal tick interval between time markers in this timeline.
*
* @param number ticksMultiple
* @param number ticksSpacingMin
* @param number dataScale
* @return number
*/
_findOptimalTickInterval: function({ ticksMultiple, ticksSpacingMin, dataScale }) {
let timingStep = ticksMultiple;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > ticksSpacingMin) {
return dataScale;
}
while (true) {
let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < ticksSpacingMin) {
timingStep <<= 1;
continue;
}
return scaledStep;
}
},
/**
* Returns true if this is document is in RTL mode.
* @return boolean
*/
_getRTL: function() {
let win = this._document.defaultView;
let doc = this._document.documentElement;
return win.getComputedStyle(doc, null).direction == "rtl";
}
};
/**
* Checks if a given marker is in the specified time range.
*
* @param object e
* The marker containing the { start, end } timestamps.
* @param number start
* The earliest allowed time.
* @param number end
* The latest allowed time.
* @return boolean
* True if the marker fits inside the specified time range.
*/
function isMarkerInRange(e, start, end) {
return (e.start >= start && e.end <= end) || // bounds inside
(e.start < start && e.end > end) || // bounds outside
(e.start < start && e.end >= start && e.end <= end) || // overlap start
(e.end > end && e.start >= start && e.start <= end); // overlap end
}
exports.Waterfall = Waterfall;

View File

@ -15,11 +15,13 @@ EXTRA_JS_MODULES.devtools.performance += [
'modules/logic/recording-model.js',
'modules/logic/recording-utils.js',
'modules/logic/tree-model.js',
'modules/logic/waterfall-utils.js',
'modules/widgets/graphs.js',
'modules/widgets/marker-details.js',
'modules/widgets/marker-view.js',
'modules/widgets/markers-overview.js',
'modules/widgets/tree-view.js',
'modules/widgets/waterfall.js',
'modules/widgets/waterfall-ticks.js',
'panel.js'
]

View File

@ -25,12 +25,16 @@ loader.lazyRequireGetter(this, "RecordingModel",
"devtools/performance/recording-model", true);
loader.lazyRequireGetter(this, "GraphsController",
"devtools/performance/graphs", true);
loader.lazyRequireGetter(this, "Waterfall",
"devtools/performance/waterfall", true);
loader.lazyRequireGetter(this, "WaterfallHeader",
"devtools/performance/waterfall-ticks", true);
loader.lazyRequireGetter(this, "MarkerView",
"devtools/performance/marker-view", true);
loader.lazyRequireGetter(this, "MarkerDetails",
"devtools/performance/marker-details", true);
loader.lazyRequireGetter(this, "MarkerUtils",
"devtools/performance/marker-utils");
loader.lazyRequireGetter(this, "WaterfallUtils",
"devtools/performance/waterfall-utils");
loader.lazyRequireGetter(this, "CallView",
"devtools/performance/tree-view", true);
loader.lazyRequireGetter(this, "ThreadNode",

View File

@ -241,12 +241,13 @@
<!-- Waterfall -->
<hbox id="waterfall-view" flex="1">
<vbox id="waterfall-breakdown" flex="1" />
<vbox flex="1">
<hbox id="waterfall-header" />
<vbox id="waterfall-breakdown" flex="1" />
</vbox>
<splitter class="devtools-side-splitter"/>
<vbox id="waterfall-details"
class="theme-sidebar"
width="150"
height="150"/>
class="theme-sidebar"/>
</hbox>
<!-- JS Tree and JIT view -->

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -145,4 +145,6 @@ skip-if = e10s # GC events seem unreliable in multiprocess
[browser_timeline-filters.js]
[browser_timeline-waterfall-background.js]
[browser_timeline-waterfall-generic.js]
[browser_timeline-waterfall-rerender.js]
[browser_timeline-waterfall-sidebar.js]
[browser_waterfall-collapse.js]

View File

@ -68,8 +68,11 @@ function* spawnTest() {
counters.ticks.push({ delta, timestamps });
lastTickDelta = delta;
}
else if (name === "frames") {
// Nothing to do here.
}
else {
throw new Error("unknown event " + name);
ok(false, `Received unknown event: ${name}`);
}
if (name === "markers" && counters[name].length === 1 ||

View File

@ -18,6 +18,11 @@ function* spawnTest() {
let treeRoot = new MyCustomTreeItem(gDataSrc, { parent: null });
treeRoot.attachTo(container);
ok(!treeRoot.expanded,
"The root node should not be expanded yet.");
ok(!treeRoot.populated,
"The root node should not be populated yet.");
is(container.childNodes.length, 1,
"The container node should have one child available.");
is(container.childNodes[0], treeRoot.target,

View File

@ -16,14 +16,9 @@ function* spawnTest() {
// Populate the tree and test `expand`, `collapse` and `getChild`...
let treeRoot = new MyCustomTreeItem(gDataSrc, { parent: null });
treeRoot.autoExpandDepth = 1;
treeRoot.attachTo(container);
ok(!treeRoot.expanded,
"The root node should not be expanded yet.");
ok(!treeRoot.populated,
"The root node should not be populated yet.");
treeRoot.expand();
ok(treeRoot.expanded,
"The root node should now be expanded.");
ok(treeRoot.populated,

View File

@ -15,11 +15,20 @@ function* spawnTest() {
"The timeline blueprint has at least one entry.");
for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) {
ok("group" in value,
"Each entry in the timeline blueprint contains a `group` key.");
ok("colorName" in value,
"Each entry in the timeline blueprint contains a `colorName` key.");
ok("label" in value,
"Each entry in the timeline blueprint contains a `label` key.");
if (key.startsWith("meta::")) {
ok(!("group" in value),
"No meta entry in the timeline blueprint can contain a `group` key.");
ok("colorName" in value,
"Each meta entry in the timeline blueprint contains a `colorName` key.");
ok("label" in value,
"Each meta entry in the timeline blueprint contains a `label` key.");
} else {
ok("group" in value,
"Each entry in the timeline blueprint contains a `group` key.");
ok("colorName" in value,
"Each entry in the timeline blueprint contains a `colorName` key.");
ok("label" in value,
"Each entry in the timeline blueprint contains a `label` key.");
}
}
}

View File

@ -7,7 +7,7 @@
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let { $, $$, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
let { TimelineGraph } = devtools.require("devtools/performance/graphs");
let { rowHeight: MARKERS_GRAPH_ROW_HEIGHT } = TimelineGraph.prototype;
@ -24,20 +24,15 @@ function* spawnTest() {
yield stopRecording(panel);
let overview = OverviewView.graphs.get("timeline");
let waterfall = WaterfallView.waterfall;
// Select everything
OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE })
$("#filter-button").click();
yield waitUntil(() => !waterfall._outstandingMarkers.length);
let menuItem1 = $("menuitem[marker-type=Styles]");
let menuItem2 = $("menuitem[marker-type=Reflow]");
let menuItem3 = $("menuitem[marker-type=Paint]");
let overview = OverviewView.graphs.get("timeline");
let originalHeight = overview.fixedHeight;
ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (1)");
@ -46,9 +41,7 @@ function* spawnTest() {
let heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin);
yield once(menuItem1, "command");
yield waitUntil(() => !waterfall._outstandingMarkers.length);
yield waitForOverviewAndCommand(overview, menuItem1);
is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (2)");
@ -57,9 +50,7 @@ function* spawnTest() {
heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin);
yield once(menuItem2, "command");
yield waitUntil(() => !waterfall._outstandingMarkers.length);
yield waitForOverviewAndCommand(overview, menuItem2);
is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (3)");
@ -68,9 +59,7 @@ function* spawnTest() {
heightBefore = overview.fixedHeight;
EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
yield once(menuItem3, "command");
yield waitUntil(() => !waterfall._outstandingMarkers.length);
yield waitForOverviewAndCommand(overview, menuItem3);
is(overview.fixedHeight, heightBefore - MARKERS_GRAPH_ROW_HEIGHT, "Overview is smaller");
ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
@ -79,11 +68,9 @@ function* spawnTest() {
for (let item of [menuItem1, menuItem2, menuItem3]) {
EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
yield once(item, "command");
yield waitForOverviewAndCommand(overview, item);
}
yield waitUntil(() => !waterfall._outstandingMarkers.length);
ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (5)");
ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (5)");
ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (5)");
@ -93,3 +80,9 @@ function* spawnTest() {
yield teardown(panel);
finish();
}
function waitForOverviewAndCommand(overview, item) {
let overviewRendered = overview.once("refresh");
let menuitemCommandDispatched = once(item, "command");
return Promise.all([overviewRendered, menuitemCommandDispatched]);
}

View File

@ -25,27 +25,30 @@ function* spawnTest() {
DetailsView.selectView("waterfall"),
once(WaterfallView, EVENTS.WATERFALL_RENDERED)
]);
yield stopRecording(panel);
ok(true, "Recording has ended.");
yield rendered;
ok(true, "Recording has rendered.");
// Test the waterfall background.
let parentWidth = $("#waterfall-view").getBoundingClientRect().width;
let sidebarWidth = $(".waterfall-sidebar").getBoundingClientRect().width;
let detailsWidth = $("#waterfall-details").getBoundingClientRect().width;
let waterfallWidth = WaterfallView.waterfall._waterfallWidth;
let waterfallWidth = WaterfallView._markersRoot._waterfallWidth;
is(waterfallWidth, parentWidth - sidebarWidth - detailsWidth,
"The waterfall width is correct.")
ok(WaterfallView.waterfall._canvas,
ok(WaterfallView._waterfallHeader._canvas,
"A canvas should be created after the recording ended.");
ok(WaterfallView.waterfall._ctx,
ok(WaterfallView._waterfallHeader._ctx,
"A 2d context should be created after the recording ended.");
is(WaterfallView.waterfall._canvas.width, waterfallWidth,
is(WaterfallView._waterfallHeader._canvas.width, waterfallWidth,
"The canvas width is correct.");
is(WaterfallView.waterfall._canvas.height, 1,
is(WaterfallView._waterfallHeader._canvas.height, 1,
"The canvas height is correct.");
yield teardown(panel);

View File

@ -42,26 +42,22 @@ function* spawnTest() {
ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
"Some header tick labels should have been created inside the tick node.");
// Test the markers container.
ok($(".waterfall-marker-container"),
"A marker container should have been created.");
// Test the markers sidebar (left).
ok($$(".waterfall-marker-container > .waterfall-sidebar").length,
ok($$(".waterfall-tree-item > .waterfall-sidebar").length,
"Some marker sidebar nodes should have been created.");
ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-bullet").length,
ok($$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-bullet").length,
"Some marker color bullets should have been created inside the sidebar.");
ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-name").length,
ok($$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-name").length,
"Some marker name labels should have been created inside the sidebar.");
// Test the markers waterfall (right).
ok($$(".waterfall-marker-item").length,
ok($$(".waterfall-tree-item > .waterfall-marker").length,
"Some marker waterfall nodes should have been created.");
ok($$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar").length,
ok($$(".waterfall-tree-item > .waterfall-marker > .waterfall-marker-bar").length,
"Some marker color bars should have been created inside the waterfall.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,57 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the waterfall remembers the selection when rerendering.
*/
function* spawnTest() {
let { target, panel } = yield initPerformance(SIMPLE_URL);
let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
yield startRecording(panel);
ok(true, "Recording has started.");
let updated = 0;
OverviewView.on(EVENTS.OVERVIEW_RENDERED, () => updated++);
ok((yield waitUntil(() => updated > 0)),
"The overview graphs were updated a bunch of times.");
ok((yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length > 0)),
"There are some markers available.");
yield stopRecording(panel);
ok(true, "Recording has ended.");
let initialBarsCount = $$(".waterfall-marker-bar").length;
// Select a portion of the overview.
let timeline = OverviewView.graphs.get("timeline");
let rerendered = WaterfallView.once(EVENTS.WATERFALL_RENDERED);
timeline.setSelection({ start: 0, end: timeline.width / 2 })
yield rerendered;
// Focus the second item in the tree.
WaterfallView._markersRoot.getChild(1).focus();
let beforeResizeBarsCount = $$(".waterfall-marker-bar").length;
ok(beforeResizeBarsCount < initialBarsCount,
"A subset of the total markers was selected.");
is(Array.indexOf($$(".waterfall-tree-item"), $(".waterfall-tree-item:focus")), 2,
"The correct item was focused in the tree.");
rerendered = WaterfallView.once(EVENTS.WATERFALL_RENDERED);
EventUtils.sendMouseEvent({ type: "mouseup" }, WaterfallView.detailsSplitter);
yield rerendered;
let afterResizeBarsCount = $$(".waterfall-marker-bar").length;
is(afterResizeBarsCount, beforeResizeBarsCount,
"The same subset of the total markers remained visible.");
is(Array.indexOf($$(".waterfall-tree-item"), $(".waterfall-tree-item:focus")), 2,
"The correct item is still focused in the tree.");
yield teardown(panel);
finish();
}

View File

@ -7,10 +7,18 @@
function* spawnTest() {
let { target, panel } = yield initPerformance(SIMPLE_URL);
let { $, $$, EVENTS, PerformanceController, OverviewView } = panel.panelWin;
let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
let { L10N, TIMELINE_BLUEPRINT } = devtools.require("devtools/performance/global");
let { getMarkerLabel } = devtools.require("devtools/performance/marker-utils");
// Hijack the markers massaging part of creating the waterfall view,
// to prevent collapsing markers and allowing this test to verify
// everything individually. A better solution would be to just expand
// all markers first and then skip the meta nodes, but I'm lazy.
WaterfallView._prepareWaterfallTree = markers => {
return { submarkers: markers };
};
yield startRecording(panel);
ok(true, "Recording has started.");
@ -26,34 +34,39 @@ function* spawnTest() {
ok(true, "Recording has ended.");
// Select everything
OverviewView.graphs.get("timeline").setSelection({ start: 0, end: OverviewView.graphs.get("timeline").width })
let timeline = OverviewView.graphs.get("timeline");
let rerendered = WaterfallView.once(EVENTS.WATERFALL_RENDERED);
timeline.setSelection({ start: 0, end: timeline.width })
yield rerendered;
let bars = $$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar");
let bars = $$(".waterfall-marker-bar");
let markers = PerformanceController.getCurrentRecording().getMarkers();
ok(bars.length > 2, "got at least 3 markers");
ok(bars.length > 2, "Got at least 3 markers (1)");
ok(markers.length > 2, "Got at least 3 markers (2)");
let sidebar = $("#waterfall-details");
for (let i = 0; i < bars.length; i++) {
let bar = bars[i];
bar.click();
let m = markers[i];
EventUtils.sendMouseEvent({ type: "mousedown" }, bar);
is($("#waterfall-details .marker-details-type").getAttribute("value"), getMarkerLabel(m),
"sidebar title matches markers name");
"Sidebar title matches markers name.");
let tooltip = $(".marker-details-duration").getAttribute("tooltiptext");
let printedDuration = $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
let duration = $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
// Values are rounded. We don't use a strict equality.
is(toMs(m.end - m.start), printedDuration, "sidebar duration is valid");
is(toMs(m.end - m.start), duration, "Sidebar duration is valid.");
// For some reason, anything that creates "→" here turns it into a "â" for some reason.
// So just check that start and end time are in there somewhere.
ok(tooltip.indexOf(toMs(m.start)) !== -1, "tooltip has start time");
ok(tooltip.indexOf(toMs(m.end)) !== -1, "tooltip has end time");
ok(tooltip.indexOf(toMs(m.start)) !== -1, "Tooltip has start time.");
ok(tooltip.indexOf(toMs(m.end)) !== -1, "Tooltip has end time.");
}
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,358 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the waterfall collapsing logic works properly.
*/
function test() {
const WaterfallUtils = devtools.require("devtools/performance/waterfall-utils");
let rootMarkerNode = WaterfallUtils.makeEmptyMarkerNode("(root)");
WaterfallUtils.collapseMarkersIntoNode({
markerNode: rootMarkerNode,
markersList: gTestMarkers
});
is(rootMarkerNode.toSource(), gExpectedOutput.toSource(),
"The markers didn't collapse properly.");
finish();
}
const gTestMarkers = [
// Test collapsing Style markers
{
start: 1,
end: 2,
name: "Styles"
},
{
start: 3,
end: 4,
name: "Styles"
},
// Test collapsing Reflow markers
{
start: 5,
end: 6,
name: "Reflow"
},
{
start: 7,
end: 8,
name: "Reflow"
},
// Test collapsing Paint markers
{
start: 9,
end: 10,
name: "Paint"
}, {
start: 11,
end: 12,
name: "Paint"
},
// Test standalone DOMEvent markers followed by a different marker
{
start: 13,
end: 14,
name: "DOMEvent",
eventPhase: 1,
type: "foo1"
},
{
start: 15,
end: 16,
name: "TimeStamp"
},
// Test a DOMEvent marker followed by a Javascript marker.
{
start: 17,
end: 18,
name: "DOMEvent",
eventPhase: 2,
type: "foo2"
}, {
start: 19,
end: 20,
name: "Javascript",
stack: 1,
endStack: 2
},
// Test another DOMEvent marker followed by a Javascript marker.
{
start: 21,
end: 22,
name: "DOMEvent",
eventPhase: 3,
type: "foo3"
}, {
start: 23,
end: 24,
name: "Javascript",
stack: 3,
endStack: 4
},
// Test a DOMEvent marker followed by multiple Javascript markers.
{
start: 25,
end: 26,
name: "DOMEvent",
eventPhase: 4,
type: "foo4"
}, {
start: 27,
end: 28,
name: "Javascript",
stack: 5,
endStack: 6
}, {
start: 29,
end: 30,
name: "Javascript",
stack: 7,
endStack: 8
}, {
start: 31,
end: 32,
name: "Javascript",
stack: 9,
endStack: 10
},
// Test multiple DOMEvent markers followed by multiple Javascript markers.
{
start: 33,
end: 34,
name: "DOMEvent",
eventPhase: 5,
type: "foo5"
}, {
start: 35,
end: 36,
name: "DOMEvent",
eventPhase: 6,
type: "foo6"
}, {
start: 37,
end: 38,
name: "DOMEvent",
eventPhase: 7,
type: "foo6"
}, {
start: 39,
end: 40,
name: "Javascript",
stack: 11,
endStack: 12
}, {
start: 41,
end: 42,
name: "Javascript",
stack: 13,
endStack: 14
}, {
start: 43,
end: 44,
name: "Javascript",
stack: 15,
endStack: 16
},
// Test a lonely marker at the end.
{
start: 45,
end: 46,
name: "GarbageCollection"
}
];
const gExpectedOutput = {
name: "(root)",
start: (void 0),
end: (void 0),
submarkers: [{
name: "Styles",
start: 1,
end: 4,
submarkers: [{
start: 1,
end: 2,
name: "Styles"
}, {
start: 3,
end: 4,
name: "Styles"
}]
}, {
name: "Reflow",
start: 5,
end: 8,
submarkers: [{
start: 5,
end: 6,
name: "Reflow"
}, {
start: 7,
end: 8,
name: "Reflow"
}]
}, {
name: "Paint",
start: 9,
end: 12,
submarkers: [{
start: 9,
end: 10,
name: "Paint"
}, {
start: 11,
end: 12,
name: "Paint"
}]
}, {
start: 13,
end: 14,
name: "DOMEvent",
eventPhase: 1,
type: "foo1"
}, {
start: 15,
end: 16,
name: "TimeStamp"
}, {
name: "meta::DOMEvent+JS",
start: 17,
end: 20,
submarkers: [{
start: 17,
end: 18,
name: "DOMEvent",
eventPhase: 2,
type: "foo2"
}, {
start: 19,
end: 20,
name: "Javascript",
stack: 1,
endStack: 2
}],
type: "foo2",
eventPhase: 2,
stack: 1,
endStack: 2
}, {
name: "meta::DOMEvent+JS",
start: 21,
end: 24,
submarkers: [{
start: 21,
end: 22,
name: "DOMEvent",
eventPhase: 3,
type: "foo3"
}, {
start: 23,
end: 24,
name: "Javascript",
stack: 3,
endStack: 4
}],
type: "foo3",
eventPhase: 3,
stack: 3,
endStack: 4
}, {
name: "meta::DOMEvent+JS",
start: 25,
end: 28,
submarkers: [{
start: 25,
end: 26,
name: "DOMEvent",
eventPhase: 4,
type: "foo4"
}, {
start: 27,
end: 28,
name: "Javascript",
stack: 5,
endStack: 6
}],
type: "foo4",
eventPhase: 4,
stack: 5,
endStack: 6
}, {
name: "Javascript",
start: 29,
end: 32,
submarkers: [{
start: 29,
end: 30,
name: "Javascript",
stack: 7,
endStack: 8
}, {
start: 31,
end: 32,
name: "Javascript",
stack: 9,
endStack: 10
}]
}, {
start: 33,
end: 34,
name: "DOMEvent",
eventPhase: 5,
type: "foo5"
}, {
start: 35,
end: 36,
name: "DOMEvent",
eventPhase: 6,
type: "foo6"
}, {
name: "meta::DOMEvent+JS",
start: 37,
end: 40,
submarkers: [{
start: 37,
end: 38,
name: "DOMEvent",
eventPhase: 7,
type: "foo6"
}, {
start: 39,
end: 40,
name: "Javascript",
stack: 11,
endStack: 12
}],
type: "foo6",
eventPhase: 7,
stack: 11,
endStack: 12
}, {
name: "Javascript",
start: 41,
end: 44,
submarkers: [{
start: 41,
end: 42,
name: "Javascript",
stack: 13,
endStack: 14
}, {
start: 43,
end: 44,
name: "Javascript",
stack: 15,
endStack: 16
}]
}, {
start: 45,
end: 46,
name: "GarbageCollection"
}]
};

View File

@ -3,7 +3,8 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const MARKER_DETAILS_WIDTH = 300;
const WATERFALL_RESIZE_EVENTS_DRAIN = 100; // ms
const MARKER_DETAILS_WIDTH = 200;
/**
* Waterfall view containing the timeline markers, controlled by DetailsView.
@ -26,24 +27,22 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
initialize: function () {
DetailsSubview.initialize.call(this);
// TODO bug 1167093 save the previously set width, and ensure minimum width
$("#waterfall-details").setAttribute("width", MARKER_DETAILS_WIDTH);
this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"));
this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
this._onMarkerSelected = this._onMarkerSelected.bind(this);
this._onResize = this._onResize.bind(this);
this._onViewSource = this._onViewSource.bind(this);
this.waterfall.on("selected", this._onMarkerSelected);
this.waterfall.on("unselected", this._onMarkerSelected);
this.headerContainer = $("#waterfall-header");
this.breakdownContainer = $("#waterfall-breakdown");
this.detailsContainer = $("#waterfall-details");
this.detailsSplitter = $("#waterfall-view > splitter");
this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
this.details.on("resize", this._onResize);
this.details.on("view-source", this._onViewSource);
window.addEventListener("resize", this._onResize);
let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint);
this.waterfall.recalculateBounds();
// TODO bug 1167093 save the previously set width, and ensure minimum width
this.details.width = MARKER_DETAILS_WIDTH;
},
/**
@ -52,10 +51,9 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
destroy: function () {
DetailsSubview.destroy.call(this);
this.waterfall.off("selected", this._onMarkerSelected);
this.waterfall.off("unselected", this._onMarkerSelected);
this.details.off("resize", this._onResize);
this.details.off("view-source", this._onViewSource);
window.removeEventListener("resize", this._onResize);
},
/**
@ -69,7 +67,9 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
let startTime = interval.startTime || 0;
let endTime = interval.endTime || recording.getDuration();
let markers = recording.getMarkers();
this.waterfall.setData({ markers, interval: { startTime, endTime } });
let rootMarkerNode = this._prepareWaterfallTree(markers);
this._populateWaterfallTree(rootMarkerNode, { startTime, endTime });
this.emit(EVENTS.WATERFALL_RENDERED);
},
@ -79,18 +79,15 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
*/
_onMarkerSelected: function (event, marker) {
let recording = PerformanceController.getCurrentRecording();
// Race condition in tests due to lazy rendering of markers in the
// waterfall? intermittent bug 1157523
if (!recording) {
return;
}
let frames = recording.getFrames();
if (event === "selected") {
this.details.render({ toolbox: gToolbox, marker, frames });
this._selected = marker;
}
if (event === "unselected") {
this.details.empty();
this._selected = null;
}
},
@ -98,8 +95,10 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
* Called when the marker details view is resized.
*/
_onResize: function () {
this.waterfall.recalculateBounds();
this.render();
setNamedTimeout("waterfall-resize", WATERFALL_RESIZE_EVENTS_DRAIN, () => {
this._markersRoot.recalculateBounds();
this.render(OverviewView.getTimeInterval());
});
},
/**
@ -107,7 +106,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
*/
_onObservedPrefChange: function(_, prefName) {
let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint);
this._markersRoot.blueprint = blueprint;
},
/**
@ -117,5 +116,59 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
gToolbox.viewSourceInDebugger(file, line);
},
/**
* Called when the recording is stopped and prepares data to
* populate the waterfall tree.
*/
_prepareWaterfallTree: function(markers) {
let rootMarkerNode = WaterfallUtils.makeEmptyMarkerNode("(root)");
WaterfallUtils.collapseMarkersIntoNode({
markerNode: rootMarkerNode,
markersList: markers
});
return rootMarkerNode;
},
/**
* Renders the waterfall tree.
*/
_populateWaterfallTree: function(rootMarkerNode, interval) {
let root = new MarkerView({
marker: rootMarkerNode,
// The root node is irrelevant in a waterfall tree.
hidden: true,
// The waterfall tree should not expand by default.
autoExpandDepth: 0
});
let header = new WaterfallHeader(root);
this._markersRoot = root;
this._waterfallHeader = header;
let blueprint = PerformanceController.getTimelineBlueprint();
root.blueprint = blueprint;
root.interval = interval;
root.on("selected", this._onMarkerSelected);
root.on("unselected", this._onMarkerSelected);
this.breakdownContainer.innerHTML = "";
root.attachTo(this.breakdownContainer);
this.headerContainer.innerHTML = "";
header.attachTo(this.headerContainer);
// If an item was previously selected in this view, attempt to
// re-select it by traversing the newly created tree.
if (this._selected) {
let item = root.find(i => i.marker == this._selected);
if (item) {
item.focus();
}
}
},
toString: () => "[object WaterfallView]"
});

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the common devtools xpcshell eslintrc config.
"extends": "../../../.eslintrc.xpcshell"
}

View File

@ -5,12 +5,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cu = Components.utils;
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://gre/modules/devtools/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
this.EXPORTED_SYMBOLS = ["AbstractTreeItem"];
@ -117,13 +121,12 @@ function AbstractTreeItem({ parent, level }) {
this._parentItem = parent;
this._level = level || 0;
this._childTreeItems = [];
this._onArrowClick = this._onArrowClick.bind(this);
this._onClick = this._onClick.bind(this);
this._onDoubleClick = this._onDoubleClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._onFocus = this._onFocus.bind(this);
EventEmitter.decorate(this);
// Events are always propagated through the root item. Decorating every
// tree item as an event emitter is a very costly operation.
if (this == this._rootItem) {
EventEmitter.decorate(this);
}
}
AbstractTreeItem.prototype = {
@ -150,7 +153,8 @@ AbstractTreeItem.prototype = {
* @return nsIDOMNode
*/
_displaySelf: function(document, arrowNode) {
throw "This method needs to be implemented by inheriting classes.";
throw new Error(
"The `_displaySelf` method needs to be implemented by inheriting classes.");
},
/**
@ -162,7 +166,16 @@ AbstractTreeItem.prototype = {
* @param array:AbstractTreeItem children
*/
_populateSelf: function(children) {
throw "This method needs to be implemented by inheriting classes.";
throw new Error(
"The `_populateSelf` method needs to be implemented by inheriting classes.");
},
/**
* Gets the this tree's owner document.
* @return Document
*/
get document() {
return this._containerNode.ownerDocument;
},
/**
@ -221,18 +234,36 @@ AbstractTreeItem.prototype = {
return this._expanded;
},
/**
* Gets the bounds for this tree's container without flushing.
* @return object
*/
get bounds() {
let win = this.document.defaultView;
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
return utils.getBoundsWithoutFlushing(this._containerNode);
},
/**
* Creates and appends this tree item to the specified parent element.
*
* @param nsIDOMNode containerNode
* The parent element for this tree item (and every other tree item).
* @param nsIDOMNode beforeNode
* The child element which should succeed this tree item.
* @param nsIDOMNode fragmentNode [optional]
* An optional document fragment temporarily holding this tree item in
* the current batch. Defaults to the `containerNode`.
* @param nsIDOMNode beforeNode [optional]
* An optional child element which should succeed this tree item.
*/
attachTo: function(containerNode, beforeNode = null) {
attachTo: function(containerNode, fragmentNode = containerNode, beforeNode = null) {
this._containerNode = containerNode;
this._constructTargetNode();
containerNode.insertBefore(this._targetNode, beforeNode);
if (beforeNode) {
fragmentNode.insertBefore(this._targetNode, beforeNode);
} else {
fragmentNode.appendChild(this._targetNode);
}
if (this._level < this.autoExpandDepth) {
this.expand();
@ -265,6 +296,7 @@ AbstractTreeItem.prototype = {
}
this._expanded = true;
this._arrowNode.setAttribute("open", "");
this._targetNode.setAttribute("expanded", "");
this._toggleChildren(true);
this._rootItem.emit("expand", this);
},
@ -278,6 +310,7 @@ AbstractTreeItem.prototype = {
}
this._expanded = false;
this._arrowNode.removeAttribute("open");
this._targetNode.removeAttribute("expanded", "");
this._toggleChildren(false);
this._rootItem.emit("collapse", this);
},
@ -292,6 +325,33 @@ AbstractTreeItem.prototype = {
return this._childTreeItems[index];
},
/**
* Calls the provided function on all the descendants of this item.
* If this item was never expanded, then no descendents exist yet.
* @param function cb
*/
traverse: function(cb) {
for (let child of this._childTreeItems) {
cb(child);
child.bfs();
}
},
/**
* Calls the provided function on all descendants of this item until
* a truthy value is returned by the predicate.
* @param function predicate
* @return AbstractTreeItem
*/
find: function(predicate) {
for (let child of this._childTreeItems) {
if (predicate(child) || child.find(predicate)) {
return child;
}
}
return null;
},
/**
* Shows or hides all the children of this item in the tree. If neessary,
* populates this item with children.
@ -315,17 +375,16 @@ AbstractTreeItem.prototype = {
* Shows all children of this item in the tree.
*/
_showChildren: function() {
let childTreeItems = this._childTreeItems;
let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
let nextNode = this._getSiblingAtDelta(1);
// First append the child items, and afterwards append any descendants.
// Otherwise, the tree will become garbled and nodes will intertwine.
for (let item of childTreeItems) {
item.attachTo(this._containerNode, nextNode);
// If this is the root item and we're not expanding any child nodes,
// it is safe to append everything at once.
if (this == this._rootItem && this.autoExpandDepth == 0) {
this._appendChildrenBatch();
}
for (let item of expandedChildTreeItems) {
item._showChildren();
// Otherwise, append the child items and their descendants successively;
// if not, the tree will become garbled and nodes will intertwine,
// since all the tree items are sharing a single container node.
else {
this._appendChildrenSuccessive();
}
},
@ -339,6 +398,40 @@ AbstractTreeItem.prototype = {
}
},
/**
* Appends all children in a single batch.
* This only works properly for root nodes when no child nodes will expand.
*/
_appendChildrenBatch: function() {
if (this._fragment === undefined) {
this._fragment = this.document.createDocumentFragment();
}
let childTreeItems = this._childTreeItems;
for (let i = 0, len = childTreeItems.length; i < len; i++) {
childTreeItems[i].attachTo(this._containerNode, this._fragment);
}
this._containerNode.appendChild(this._fragment);
},
/**
* Appends all children successively.
*/
_appendChildrenSuccessive: function() {
let childTreeItems = this._childTreeItems;
let expandedChildTreeItems = childTreeItems.filter(e => e._expanded);
let nextNode = this._getSiblingAtDelta(1);
for (let i = 0, len = childTreeItems.length; i < len; i++) {
childTreeItems[i].attachTo(this._containerNode, undefined, nextNode);
}
for (let i = 0, len = expandedChildTreeItems.length; i < len; i++) {
expandedChildTreeItems[i]._showChildren();
}
},
/**
* Constructs and stores the target node displaying this tree item.
*/
@ -346,7 +439,14 @@ AbstractTreeItem.prototype = {
if (this._constructed) {
return;
}
let document = this._containerNode.ownerDocument;
this._onArrowClick = this._onArrowClick.bind(this);
this._onClick = this._onClick.bind(this);
this._onDoubleClick = this._onDoubleClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._onFocus = this._onFocus.bind(this);
this._onBlur = this._onBlur.bind(this);
let document = this.document;
let arrowNode = this._arrowNode = document.createElement("hbox");
arrowNode.className = "arrow theme-twisty";
@ -359,6 +459,7 @@ AbstractTreeItem.prototype = {
targetNode.addEventListener("dblclick", this._onDoubleClick);
targetNode.addEventListener("keypress", this._onKeyPress);
targetNode.addEventListener("focus", this._onFocus);
targetNode.addEventListener("blur", this._onBlur);
this._constructed = true;
},
@ -434,7 +535,6 @@ AbstractTreeItem.prototype = {
if (!e.target.classList.contains("arrow")) {
this._onArrowClick(e);
}
this.focus();
},
@ -477,5 +577,12 @@ AbstractTreeItem.prototype = {
*/
_onFocus: function(e) {
this._rootItem.emit("focus", this);
},
/**
* Handler for the "blur" event on the element displaying this tree item.
*/
_onBlur: function(e) {
this._rootItem.emit("blur", this);
}
};

View File

@ -3,19 +3,32 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { ViewHelpers } = require("resource:///modules/devtools/ViewHelpers.jsm");
const { AbstractCanvasGraph, GraphArea, GraphAreaDragger } = require("resource:///modules/devtools/Graphs.jsm");
const { Promise } = require("resource://gre/modules/Promise.jsm");
const { Task } = require("resource://gre/modules/Task.jsm");
const { getColor } = require("devtools/shared/theme");
const EventEmitter = require("devtools/toolkit/event-emitter");
const FrameUtils = require("devtools/performance/frame-utils");
const { ViewHelpers } = require("resource:///modules/devtools/ViewHelpers.jsm");
const { setNamedTimeout, clearNamedTimeout } = require("resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
loader.lazyRequireGetter(this, "getColor",
"devtools/shared/theme", true);
loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
"devtools/performance/global", true);
loader.lazyRequireGetter(this, "FrameUtils",
"devtools/performance/frame-utils");
loader.lazyImporter(this, "AbstractCanvasGraph",
"resource:///modules/devtools/Graphs.jsm");
loader.lazyImporter(this, "GraphArea",
"resource:///modules/devtools/Graphs.jsm");
loader.lazyImporter(this, "GraphAreaDragger",
"resource:///modules/devtools/Graphs.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
const L10N = new ViewHelpers.L10N();
const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
@ -112,7 +125,7 @@ function FlameGraph(parent, sharpness) {
EventEmitter.decorate(this);
this._parent = parent;
this._ready = Promise.defer();
this._ready = promise.defer();
this.setTheme();

View File

@ -15,6 +15,7 @@
overflow: hidden;
margin: 0;
padding: 0;
font-size: 0;
}
</style>
</head>

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -2685,13 +2685,45 @@ RuleEditor.prototype = {
return;
}
let ruleView = this.ruleView;
let elementStyle = ruleView._elementStyle;
let element = elementStyle.element;
let supportsUnmatchedRules =
this.rule.domRule.supportsModifySelectorUnmatched;
this.isEditing = true;
this.rule.domRule.modifySelector(aValue).then(isModified => {
this.rule.domRule.modifySelector(element, aValue).then(response => {
this.isEditing = false;
if (isModified) {
this.ruleView.refreshPanel();
if (!supportsUnmatchedRules) {
if (response) {
this.ruleView.refreshPanel();
}
return;
}
let {ruleProps, isMatching} = response;
if (!ruleProps) {
return;
}
let newRule = new Rule(elementStyle, ruleProps);
let editor = new RuleEditor(ruleView, newRule);
let rules = elementStyle.rules;
rules.splice(rules.indexOf(this.rule), 1);
rules.push(newRule);
elementStyle._changed();
editor.element.setAttribute("unmatched", !isMatching);
this.element.parentNode.replaceChild(editor.element, this.element);
// Remove highlight for modified selector
if (ruleView.highlightedSelector &&
ruleView.highlightedSelector == this.rule.selectorText) {
ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
ruleView.highlightedSelector);
}
}).then(null, err => {
this.isEditing = false;

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

View File

@ -87,6 +87,9 @@ skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work wit
[browser_ruleview_edit-selector-commit.js]
[browser_ruleview_edit-selector_01.js]
[browser_ruleview_edit-selector_02.js]
[browser_ruleview_edit-selector_03.js]
[browser_ruleview_edit-selector_04.js]
[browser_ruleview_edit-selector_05.js]
[browser_ruleview_eyedropper.js]
[browser_ruleview_filtereditor-appears-on-swatch-click.js]
[browser_ruleview_filtereditor-commit-on-ENTER.js]

View File

@ -26,7 +26,7 @@ add_task(function*() {
info("Selecting the test element");
yield selectNode("#testid", inspector);
info("Waiting for rule view to change");
info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Adding the new rule");
@ -36,7 +36,7 @@ add_task(function*() {
yield testEditSelector(view, "span");
info("Selecting the modified element");
info("Selecting the modified element with the new rule");
yield selectNode("span", inspector);
yield checkModifiedElement(view, "span");
});
@ -49,14 +49,15 @@ function* testEditSelector(view, name) {
info("Entering a new selector name and committing");
editor.value = name;
info("Waiting for rule view to refresh");
let onRuleViewRefresh = once(view, "ruleview-refreshed");
info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewRefresh;
yield onRuleViewChanged;
is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
}
function* checkModifiedElement(view, name) {

View File

@ -79,14 +79,14 @@ function* testEditSelector(view, name) {
info("Entering a new selector name: " + name);
editor.input.value = name;
info("Waiting for rule view to refresh");
let onRuleViewRefresh = once(view, "ruleview-refreshed");
info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewRefresh;
yield onRuleViewChanged;
is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
}
function* checkModifiedElement(view, name, index) {

View File

@ -48,12 +48,7 @@ const TEST_DATA = [
];
add_task(function*() {
yield addTab("data:text/html;charset=utf-8,test escaping selector change reverts back to original value");
info("Creating the test document");
content.document.body.innerHTML = PAGE_CONTENT;
info("Opening the rule-view");
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
let {toolbox, inspector, view} = yield openRuleView();
info("Iterating over the test data");
@ -88,7 +83,7 @@ function* runTestData(inspector, view, data) {
"Value is as expected: " + expected);
is(idRuleEditor.isEditing, false, "Selector is not being edited.")
} else {
yield once(view, "ruleview-refreshed");
yield once(view, "ruleview-changed");
ok(getRuleViewRule(view, expected),
"Rule with " + name + " selector exists.");
}

View File

@ -13,24 +13,19 @@ let PAGE_CONTENT = [
' }',
'</style>',
'<div id="testid" class="testclass">Styled Node</div>',
'<span id="testid2">This is a span</span>'
'<span>This is a span</span>',
].join("\n");
add_task(function*() {
yield addTab("data:text/html;charset=utf-8,test rule view selector changes");
info("Creating the test document");
content.document.body.innerHTML = PAGE_CONTENT;
info("Opening the rule-view");
let {toolbox, inspector, view} = yield openRuleView();
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
let {inspector, view} = yield openRuleView();
info("Selecting the test element");
yield selectNode("#testid", inspector);
yield testEditSelector(view, "span");
info("Selecting the modified element");
yield selectNode("#testid2", inspector);
info("Selecting the modified element with the new rule");
yield selectNode("span", inspector);
yield checkModifiedElement(view, "span");
});
@ -48,16 +43,17 @@ function* testEditSelector(view, name) {
info("Entering a new selector name and committing");
editor.input.value = name;
info("Waiting for rule view to refresh");
let onRuleViewRefresh = once(view, "ruleview-refreshed");
info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewRefresh;
yield onRuleViewChanged;
is(view._elementStyle.rules.length, 1, "Should have 1 rule.");
is(getRuleViewRule(view, name), undefined,
name + " selector has been removed.");
is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
"Rule with " + name + " does not match the current element.");
}
function* checkModifiedElement(view, name) {

View File

@ -12,7 +12,7 @@ let PAGE_CONTENT = [
' .testclass {',
' text-align: center;',
' }',
' #testid3:first-letter {',
' #testid3::first-letter {',
' text-decoration: "italic"',
' }',
'</style>',
@ -23,21 +23,16 @@ let PAGE_CONTENT = [
].join("\n");
add_task(function*() {
yield addTab("data:text/html;charset=utf-8,test rule view selector changes");
info("Creating the test document");
content.document.body.innerHTML = PAGE_CONTENT;
info("Opening the rule-view");
let {toolbox, inspector, view} = yield openRuleView();
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
let {inspector, view} = yield openRuleView();
info("Selecting the test element");
yield selectNode(".testclass", inspector);
yield testEditSelector(view, "div:nth-child(2)");
yield testEditSelector(view, "div:nth-child(1)");
info("Selecting the modified element");
yield selectNode("#testid", inspector);
yield checkModifiedElement(view, "div:nth-child(2)");
yield checkModifiedElement(view, "div:nth-child(1)");
info("Selecting the test element");
yield selectNode("#testid3", inspector);
@ -63,16 +58,20 @@ function* testEditSelector(view, name) {
info("Entering a new selector name: " + name);
editor.input.value = name;
info("Waiting for rule view to refresh");
let onRuleViewRefresh = once(view, "ruleview-refreshed");
info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewRefresh;
yield onRuleViewChanged;
is(view._elementStyle.rules.length, 1, "Should have 1 rule.");
is(getRuleViewRule(view, name), undefined,
name + " selector has been removed.");
is(view._elementStyle.rules.length, 2, "Should have 2 rule.");
ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
let newRuleEditor = getRuleViewRuleEditor(view, 1) ||
getRuleViewRuleEditor(view, 1, 0);
ok(newRuleEditor.element.getAttribute("unmatched"),
"Rule with " + name + " does not match the current element.");
}
function* checkModifiedElement(view, name) {

View File

@ -0,0 +1,46 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Testing selector inplace-editor behaviors in the rule-view with invalid
// selectors
let TEST_URI = [
'<style type="text/css">',
' .testclass {',
' text-align: center;',
' }',
'</style>',
'<div class="testclass">Styled Node</div>',
].join("\n");
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode(".testclass", inspector);
yield testEditSelector(view, "asd@:::!");
});
function* testEditSelector(view, name) {
info("Test editing existing selector fields");
let ruleEditor = getRuleViewRuleEditor(view, 1);
info("Focusing an existing selector name in the rule-view");
let editor = yield focusEditableField(ruleEditor.selectorText);
is(inplaceEditor(ruleEditor.selectorText), editor,
"The selector editor got focused");
info("Entering a new selector name and committing");
editor.input.value = name;
EventUtils.synthesizeKey("VK_RETURN", {});
is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
is(getRuleViewRule(view, name), undefined,
"Rule with " + name + " selector should not exist.");
ok(getRuleViewRule(view, ".testclass"),
"Rule with .testclass selector exists.");
}

View File

@ -0,0 +1,67 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that the selector highlighter is removed when modifying a selector and
// the selector highlighter works for the newly added unmatched rule.
const TEST_URI = [
'<style type="text/css">',
' p {',
' background: red;',
' }',
'</style>',
'<p>Test the selector highlighter</p>'
].join("\n");
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
ok(!view.selectorHighlighter, "No selectorhighlighter exist in the rule-view");
yield selectNode("p", inspector);
yield testSelectorHighlight(view, "p");
yield testEditSelector(view, "body");
yield testSelectorHighlight(view, "body");
});
function* testSelectorHighlight(view, name) {
info("Test creating selector highlighter");
info("Clicking on a selector icon");
let icon = getRuleViewSelectorHighlighterIcon(view, name);
let onToggled = view.once("ruleview-selectorhighlighter-toggled");
EventUtils.synthesizeMouseAtCenter(icon, {}, view.doc.defaultView);
let isVisible = yield onToggled;
ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
ok(isVisible, "The toggle event says the highlighter is visible");
}
function* testEditSelector(view, name) {
info("Test editing existing selector fields");
let ruleEditor = getRuleViewRuleEditor(view, 1);
info("Focusing an existing selector name in the rule-view");
let editor = yield focusEditableField(ruleEditor.selectorText);
is(inplaceEditor(ruleEditor.selectorText), editor,
"The selector editor got focused");
info("Waiting for rule view to update");
let onToggled = view.once("ruleview-selectorhighlighter-toggled");
info("Entering a new selector name and committing");
editor.input.value = name;
EventUtils.synthesizeKey("VK_RETURN", {});
let isVisible = yield onToggled;
ok(!view.highlightedSelector, "The selectorhighlighter instance was removed");
ok(!isVisible, "The toggle event says the highlighter is not visible");
}

View File

@ -0,0 +1,108 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that adding a new property of an unmatched rule works properly.
let TEST_URI = [
'<style type="text/css">',
' #testid {',
' }',
' .testclass {',
' background-color: white;',
' }',
'</style>',
'<div id="testid">Styled Node</div>',
'<span class="testclass">This is a span</span>'
].join("\n");
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
info("Selecting the test element");
yield selectNode("#testid", inspector);
yield testEditSelector(view, "span");
yield testAddProperty(view);
info("Selecting the modified element with the new rule");
yield selectNode("span", inspector);
yield checkModifiedElement(view, "span");
});
function* testEditSelector(view, name) {
info("Test editing existing selector fields");
let ruleEditor = getRuleViewRuleEditor(view, 1);
info("Focusing an existing selector name in the rule-view");
let editor = yield focusEditableField(ruleEditor.selectorText);
is(inplaceEditor(ruleEditor.selectorText), editor,
"The selector editor got focused");
info("Entering a new selector name and committing");
editor.input.value = name;
info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewChanged;
is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
"Rule with " + name + " does not match the current element.");
}
function* checkModifiedElement(view, name) {
is(view._elementStyle.rules.length, 3, "Should have 3 rules.");
ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
}
function* testAddProperty(view) {
info("Test creating a new property");
let ruleEditor = getRuleViewRuleEditor(view, 1);
info("Focusing a new property name in the rule-view");
let editor = yield focusEditableField(ruleEditor.closeBrace);
is(inplaceEditor(ruleEditor.newPropSpan), editor,
"The new property editor got focused");
let input = editor.input;
info("Entering text-align in the property name editor");
input.value = "text-align";
info("Pressing return to commit and focus the new value field");
let onValueFocus = once(ruleEditor.element, "focus", true);
let onModifications = ruleEditor.rule._applyingModifications;
EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
yield onValueFocus;
yield onModifications;
// Getting the new value editor after focus
editor = inplaceEditor(view.doc.activeElement);
let textProp = ruleEditor.rule.textProps[0];
is(ruleEditor.rule.textProps.length, 1, "Created a new text property.");
is(ruleEditor.propertyList.children.length, 1, "Created a property editor.");
is(editor, inplaceEditor(textProp.editor.valueSpan),
"Editing the value span now.");
info("Entering a value and bluring the field to expect a rule change");
editor.input.value = "center";
let onBlur = once(editor.input, "blur");
onModifications = ruleEditor.rule._applyingModifications;
editor.input.blur();
yield onBlur;
yield onModifications;
is(textProp.value, "center", "Text prop should have been changed.");
is(textProp.overridden, false, "Property should not be overridden");
}

View File

@ -0,0 +1,4 @@
{
// Extend from the common devtools xpcshell eslintrc config.
"extends": "../../../.eslintrc.xpcshell"
}

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../.eslintrc.mochitests"
}

Some files were not shown because too many files have changed in this diff Show More