mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-08 19:04:45 +00:00
Merge Fx-Team to Mozilla-Central
This commit is contained in:
commit
d7e4c57fbb
@ -1184,6 +1184,7 @@ pref("devtools.webconsole.filter.networkinfo", true);
|
||||
pref("devtools.webconsole.filter.netwarn", true);
|
||||
pref("devtools.webconsole.filter.csserror", true);
|
||||
pref("devtools.webconsole.filter.cssparser", true);
|
||||
pref("devtools.webconsole.filter.csslog", false);
|
||||
pref("devtools.webconsole.filter.exception", true);
|
||||
pref("devtools.webconsole.filter.jswarn", true);
|
||||
pref("devtools.webconsole.filter.jslog", true);
|
||||
@ -1200,6 +1201,7 @@ pref("devtools.browserconsole.filter.networkinfo", true);
|
||||
pref("devtools.browserconsole.filter.netwarn", true);
|
||||
pref("devtools.browserconsole.filter.csserror", true);
|
||||
pref("devtools.browserconsole.filter.cssparser", true);
|
||||
pref("devtools.browserconsole.filter.csslog", false);
|
||||
pref("devtools.browserconsole.filter.exception", true);
|
||||
pref("devtools.browserconsole.filter.jswarn", true);
|
||||
pref("devtools.browserconsole.filter.jslog", true);
|
||||
@ -1295,8 +1297,10 @@ pref("pdfjs.firstRun", true);
|
||||
pref("pdfjs.previousHandler.preferredAction", 0);
|
||||
pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false);
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
// Shumway component (SWF player) is disabled by default. Also see bug 904346.
|
||||
pref("shumway.disabled", true);
|
||||
#endif
|
||||
|
||||
// The maximum amount of decoded image data we'll willingly keep around (we
|
||||
// might keep around more than this, but we'll try to get down to this value).
|
||||
|
@ -112,7 +112,19 @@
|
||||
}
|
||||
|
||||
function reloadProvider() {
|
||||
// Just incase the current provider *isn't* in a frameworker-error
|
||||
// state, reload the current one.
|
||||
Social.provider.reload();
|
||||
// If the problem is a frameworker-error, it may be that the child
|
||||
// process crashed - and if that happened, then *all* providers in that
|
||||
// process will have crashed. However, only the current provider is
|
||||
// likely to have the error surfaced in the UI - so we reload *all*
|
||||
// providers that are in a frameworker-error state.
|
||||
for (let provider of Social.providers) {
|
||||
if (provider.enabled && provider.errorState == "frameworker-error") {
|
||||
provider.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseQueryString();
|
||||
|
@ -1215,9 +1215,7 @@ var gBrowserInit = {
|
||||
|
||||
SessionStore.promiseInitialized.then(() => {
|
||||
// Enable the Restore Last Session command if needed
|
||||
if (SessionStore.canRestoreLastSession &&
|
||||
!PrivateBrowsingUtils.isWindowPrivate(window))
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", true);
|
||||
RestoreLastSessionObserver.init();
|
||||
|
||||
TabView.init();
|
||||
|
||||
@ -6721,19 +6719,16 @@ var gIdentityHandler = {
|
||||
* Click handler for the identity-box element in primary chrome.
|
||||
*/
|
||||
handleIdentityButtonEvent : function(event) {
|
||||
TelemetryStopwatch.start("FX_IDENTITY_POPUP_OPEN_MS");
|
||||
event.stopPropagation();
|
||||
|
||||
if ((event.type == "click" && event.button != 0) ||
|
||||
(event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
|
||||
event.keyCode != KeyEvent.DOM_VK_RETURN)) {
|
||||
TelemetryStopwatch.cancel("FX_IDENTITY_POPUP_OPEN_MS");
|
||||
return; // Left click, space or enter only
|
||||
}
|
||||
|
||||
// Don't allow left click, space or enter if the location has been modified.
|
||||
if (gURLBar.getAttribute("pageproxystate") != "valid") {
|
||||
TelemetryStopwatch.cancel("FX_IDENTITY_POPUP_OPEN_MS");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -6759,8 +6754,6 @@ var gIdentityHandler = {
|
||||
},
|
||||
|
||||
onPopupShown : function(event) {
|
||||
TelemetryStopwatch.finish("FX_IDENTITY_POPUP_OPEN_MS");
|
||||
|
||||
document.getElementById('identity-popup-more-info-button').focus();
|
||||
|
||||
this._identityPopup.addEventListener("blur", this, true);
|
||||
@ -7001,6 +6994,26 @@ function switchToTabHavingURI(aURI, aOpenNew) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let RestoreLastSessionObserver = {
|
||||
init: function () {
|
||||
if (SessionStore.canRestoreLastSession &&
|
||||
!PrivateBrowsingUtils.isWindowPrivate(window)) {
|
||||
Services.obs.addObserver(this, "sessionstore-last-session-cleared", true);
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", true);
|
||||
}
|
||||
},
|
||||
|
||||
observe: function () {
|
||||
// The last session can only be restored once so there's
|
||||
// no way we need to re-enable our menu item.
|
||||
Services.obs.removeObserver(this, "sessionstore-last-session-cleared");
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", false);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference])
|
||||
};
|
||||
|
||||
function restoreLastSession() {
|
||||
SessionStore.restoreLastSession();
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ support-files =
|
||||
social_activate.html
|
||||
social_activate_iframe.html
|
||||
social_chat.html
|
||||
social_crash_content_helper.js
|
||||
social_flyout.html
|
||||
social_mark.html
|
||||
social_panel.html
|
||||
@ -42,3 +43,4 @@ support-files =
|
||||
[browser_social_status.js]
|
||||
[browser_social_toolbar.js]
|
||||
[browser_social_window.js]
|
||||
[browser_social_workercrash.js]
|
||||
|
157
browser/base/content/test/social/browser_social_workercrash.js
Normal file
157
browser/base/content/test/social/browser_social_workercrash.js
Normal file
@ -0,0 +1,157 @@
|
||||
/* 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/. */
|
||||
|
||||
// This tests our recovery if a child content process hosting providers
|
||||
// crashes.
|
||||
|
||||
// A content script we inject into one of our browsers
|
||||
const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/browser/base/content/test/social/social_crash_content_helper.js";
|
||||
|
||||
let {getFrameWorkerHandle} = Cu.import("resource://gre/modules/FrameWorker.jsm", {});
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
|
||||
// We need to ensure all our workers are in the same content process.
|
||||
Services.prefs.setIntPref("dom.ipc.processCount", 1);
|
||||
|
||||
runSocialTestWithProvider(gProviders, function (finishcb) {
|
||||
Social.enabled = true;
|
||||
runSocialTests(tests, undefined, undefined, function() {
|
||||
Services.prefs.clearUserPref("dom.ipc.processCount");
|
||||
Services.prefs.clearUserPref("social.sidebar.open");
|
||||
Services.prefs.clearUserPref("social.allowMultipleWorkers");
|
||||
finishcb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let gProviders = [
|
||||
{
|
||||
name: "provider 1",
|
||||
origin: "https://example.com",
|
||||
sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html?provider1",
|
||||
workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
|
||||
iconURL: "chrome://branding/content/icon48.png"
|
||||
},
|
||||
{
|
||||
name: "provider 2",
|
||||
origin: "https://test1.example.com",
|
||||
sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider2",
|
||||
workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js",
|
||||
iconURL: "chrome://branding/content/icon48.png"
|
||||
}
|
||||
];
|
||||
|
||||
var tests = {
|
||||
testCrash: function(next) {
|
||||
// open the sidebar, then crash the child.
|
||||
let sbrowser = document.getElementById("social-sidebar-browser");
|
||||
onSidebarLoad(function() {
|
||||
// get the browser element for our provider.
|
||||
let fw = getFrameWorkerHandle(gProviders[0].workerURL);
|
||||
fw.port.close();
|
||||
fw._worker.browserPromise.then(browser => {
|
||||
let mm = browser.messageManager;
|
||||
mm.loadFrameScript(TEST_CONTENT_HELPER, false);
|
||||
// add an observer for the crash - after it sees the crash we attempt
|
||||
// a reload.
|
||||
let observer = new crashObserver(function() {
|
||||
info("Saw the process crash.")
|
||||
Services.obs.removeObserver(observer, 'ipc:content-shutdown');
|
||||
// Add another sidebar load listener - it should be the error page.
|
||||
onSidebarLoad(function() {
|
||||
ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page");
|
||||
// after reloading, the sidebar should reload
|
||||
onSidebarLoad(function() {
|
||||
// now ping both workers - they should both be alive.
|
||||
ensureWorkerLoaded(gProviders[0], function() {
|
||||
ensureWorkerLoaded(gProviders[1], function() {
|
||||
// and we are done!
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
// click the try-again button.
|
||||
sbrowser.contentDocument.getElementById("btnTryAgain").click();
|
||||
});
|
||||
});
|
||||
Services.obs.addObserver(observer, 'ipc:content-shutdown', false);
|
||||
// and cause the crash.
|
||||
mm.sendAsyncMessage("social-test:crash");
|
||||
});
|
||||
})
|
||||
Services.prefs.setBoolPref("social.sidebar.open", true);
|
||||
},
|
||||
}
|
||||
|
||||
function onSidebarLoad(callback) {
|
||||
let sbrowser = document.getElementById("social-sidebar-browser");
|
||||
sbrowser.addEventListener("load", function load() {
|
||||
sbrowser.removeEventListener("load", load, true);
|
||||
callback();
|
||||
}, true);
|
||||
}
|
||||
|
||||
function ensureWorkerLoaded(manifest, callback) {
|
||||
let fw = getFrameWorkerHandle(manifest.workerURL);
|
||||
// once the worker responds to a ping we know it must be up.
|
||||
let port = fw.port;
|
||||
port.onmessage = function(msg) {
|
||||
if (msg.data.topic == "pong") {
|
||||
port.close();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
port.postMessage({topic: "ping"})
|
||||
}
|
||||
|
||||
// More duplicated code from browser_thumbnails_brackground_crash.
|
||||
// Bug 915518 exists to unify these.
|
||||
|
||||
// This observer is needed so we can clean up all evidence of the crash so
|
||||
// the testrunner thinks things are peachy.
|
||||
let crashObserver = function(callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
crashObserver.prototype = {
|
||||
observe: function(subject, topic, data) {
|
||||
is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
|
||||
ok(subject instanceof Components.interfaces.nsIPropertyBag2,
|
||||
'Subject implements nsIPropertyBag2.');
|
||||
// we might see this called as the process terminates due to previous tests.
|
||||
// We are only looking for "abnormal" exits...
|
||||
if (!subject.hasKey("abnormal")) {
|
||||
info("This is a normal termination and isn't the one we are looking for...");
|
||||
return;
|
||||
}
|
||||
|
||||
var dumpID;
|
||||
if ('nsICrashReporter' in Components.interfaces) {
|
||||
dumpID = subject.getPropertyAsAString('dumpID');
|
||||
ok(dumpID, "dumpID is present and not an empty string");
|
||||
}
|
||||
|
||||
if (dumpID) {
|
||||
var minidumpDirectory = getMinidumpDirectory();
|
||||
removeFile(minidumpDirectory, dumpID + '.dmp');
|
||||
removeFile(minidumpDirectory, dumpID + '.extra');
|
||||
}
|
||||
this.callback();
|
||||
}
|
||||
}
|
||||
|
||||
function getMinidumpDirectory() {
|
||||
var dir = Services.dirsvc.get('ProfD', Components.interfaces.nsIFile);
|
||||
dir.append("minidumps");
|
||||
return dir;
|
||||
}
|
||||
function removeFile(directory, filename) {
|
||||
var file = directory.clone();
|
||||
file.append(filename);
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Ideally we would use CrashTestUtils.jsm, but that's only available for
|
||||
// xpcshell tests - so we just copy a ctypes crasher from it.
|
||||
Cu.import("resource://gre/modules/ctypes.jsm");
|
||||
let crash = function() { // this will crash when called.
|
||||
let zero = new ctypes.intptr_t(8);
|
||||
let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
|
||||
badptr.contents
|
||||
};
|
||||
|
||||
|
||||
TestHelper = {
|
||||
init: function() {
|
||||
addMessageListener("social-test:crash", this);
|
||||
},
|
||||
|
||||
receiveMessage: function(msg) {
|
||||
switch (msg.name) {
|
||||
case "social-test:crash":
|
||||
privateNoteIntentionalCrash();
|
||||
crash();
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
TestHelper.init();
|
@ -53,8 +53,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader",
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PdfJs",
|
||||
"resource://pdf.js/PdfJs.jsm");
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
|
||||
"resource://shumway/ShumwayUtils.jsm");
|
||||
#endif
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "webrtcUI",
|
||||
"resource:///modules/webrtcUI.jsm");
|
||||
@ -469,7 +471,9 @@ BrowserGlue.prototype = {
|
||||
BrowserNewTabPreloader.init();
|
||||
SignInToWebsiteUX.init();
|
||||
PdfJs.init();
|
||||
#ifdef NIGHTLY_BUILD
|
||||
ShumwayUtils.init();
|
||||
#endif
|
||||
webrtcUI.init();
|
||||
AboutHome.init();
|
||||
SessionStore.init();
|
||||
|
@ -23,6 +23,7 @@ const TAB_STATE_RESTORING = 2;
|
||||
|
||||
const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
|
||||
const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
|
||||
const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
|
||||
|
||||
// Maximum number of tabs to restore simultaneously. Previously controlled by
|
||||
// the browser.sessionstore.max_concurrent_tabs pref.
|
||||
@ -329,12 +330,6 @@ let SessionStoreInternal = {
|
||||
// number of tabs currently restoring
|
||||
_tabsRestoringCount: 0,
|
||||
|
||||
// The state from the previous session (after restoring pinned tabs). This
|
||||
// state is persisted and passed through to the next session during an app
|
||||
// restart to make the third party add-on warning not trash the deferred
|
||||
// session
|
||||
_lastSessionState: null,
|
||||
|
||||
// When starting Firefox with a single private window, this is the place
|
||||
// where we keep the session we actually wanted to restore in case the user
|
||||
// decides to later open a non-private window as well.
|
||||
@ -361,16 +356,15 @@ let SessionStoreInternal = {
|
||||
return this._deferredInitialized.promise;
|
||||
},
|
||||
|
||||
/* ........ Public Getters .............. */
|
||||
get canRestoreLastSession() {
|
||||
return !!this._lastSessionState;
|
||||
return LastSession.canRestore;
|
||||
},
|
||||
|
||||
set canRestoreLastSession(val) {
|
||||
// Cheat a bit; only allow false.
|
||||
if (val)
|
||||
return;
|
||||
this._lastSessionState = null;
|
||||
if (!val) {
|
||||
LastSession.clear();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -419,13 +413,15 @@ let SessionStoreInternal = {
|
||||
state = iniState;
|
||||
else
|
||||
state = null;
|
||||
if (remainingState.windows.length)
|
||||
this._lastSessionState = remainingState;
|
||||
|
||||
if (remainingState.windows.length) {
|
||||
LastSession.setState(remainingState);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Get the last deferred session in case the user still wants to
|
||||
// restore it
|
||||
this._lastSessionState = state.lastSessionState;
|
||||
LastSession.setState(state.lastSessionState);
|
||||
|
||||
let lastSessionCrashed =
|
||||
state.session && state.session.state &&
|
||||
@ -521,15 +517,11 @@ let SessionStoreInternal = {
|
||||
gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
|
||||
}, false);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "_max_tabs_undo", function () {
|
||||
this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
|
||||
return this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
|
||||
});
|
||||
this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
|
||||
this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "_max_windows_undo", function () {
|
||||
this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
|
||||
return this._prefBranch.getIntPref("sessionstore.max_windows_undo");
|
||||
});
|
||||
this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
|
||||
this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1066,7 +1058,7 @@ let SessionStoreInternal = {
|
||||
|
||||
if (aData != "restart") {
|
||||
// Throw away the previous session on shutdown
|
||||
this._lastSessionState = null;
|
||||
LastSession.clear();
|
||||
}
|
||||
|
||||
this._loadState = STATE_QUITTING; // just to be sure
|
||||
@ -1083,7 +1075,7 @@ let SessionStoreInternal = {
|
||||
// quit-application notification so the browser is about to exit.
|
||||
if (this._loadState == STATE_QUITTING)
|
||||
return;
|
||||
this._lastSessionState = null;
|
||||
LastSession.clear();
|
||||
let openWindows = {};
|
||||
this._forEachBrowserWindow(function(aWindow) {
|
||||
Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
|
||||
@ -1769,7 +1761,7 @@ let SessionStoreInternal = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores the session state stored in _lastSessionState. This will attempt
|
||||
* Restores the session state stored in LastSession. This will attempt
|
||||
* to merge data into the current session. If a window was opened at startup
|
||||
* with pinned tab(s), then the remaining data from the previous session for
|
||||
* that window will be opened into that winddow. Otherwise new windows will
|
||||
@ -1787,7 +1779,7 @@ let SessionStoreInternal = {
|
||||
windows[aWindow.__SS_lastSessionWindowID] = aWindow;
|
||||
});
|
||||
|
||||
let lastSessionState = this._lastSessionState;
|
||||
let lastSessionState = LastSession.getState();
|
||||
|
||||
// This shouldn't ever be the case...
|
||||
if (!lastSessionState.windows.length)
|
||||
@ -1831,7 +1823,7 @@ let SessionStoreInternal = {
|
||||
if (winState._closedTabs && winState._closedTabs.length) {
|
||||
let curWinState = this._windows[windowToUse.__SSi];
|
||||
curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs);
|
||||
curWinState._closedTabs.splice(this._prefBranch.getIntPref("sessionstore.max_tabs_undo"), curWinState._closedTabs.length);
|
||||
curWinState._closedTabs.splice(this._max_tabs_undo, curWinState._closedTabs.length);
|
||||
}
|
||||
|
||||
// Restore into that window - pretend it's a followup since we'll already
|
||||
@ -1866,7 +1858,7 @@ let SessionStoreInternal = {
|
||||
// Update the session start time using the restored session state.
|
||||
this._updateSessionStartTime(lastSessionState);
|
||||
|
||||
this._lastSessionState = null;
|
||||
LastSession.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -2127,8 +2119,8 @@ let SessionStoreInternal = {
|
||||
};
|
||||
|
||||
// Persist the last session if we deferred restoring it
|
||||
if (this._lastSessionState) {
|
||||
state.lastSessionState = this._lastSessionState;
|
||||
if (LastSession.canRestore) {
|
||||
state.lastSessionState = LastSession.getState();
|
||||
}
|
||||
|
||||
// If we were called by the SessionSaver and started with only a private
|
||||
@ -3526,8 +3518,8 @@ let SessionStoreInternal = {
|
||||
* from state. It will contain the cookies that go along with the history
|
||||
* entries in those tabs. It will also contain window position information.
|
||||
*
|
||||
* defaultState will be restored at startup. state will be placed into
|
||||
* this._lastSessionState and will be kept in case the user explicitly wants
|
||||
* defaultState will be restored at startup. state will be passed into
|
||||
* LastSession and will be kept in case the user explicitly wants
|
||||
* to restore the previous session (publicly exposed as restoreLastSession).
|
||||
*
|
||||
* @param state
|
||||
@ -4537,3 +4529,30 @@ let TabState = {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
// The state from the previous session (after restoring pinned tabs). This
|
||||
// state is persisted and passed through to the next session during an app
|
||||
// restart to make the third party add-on warning not trash the deferred
|
||||
// session
|
||||
let LastSession = {
|
||||
_state: null,
|
||||
|
||||
get canRestore() {
|
||||
return !!this._state;
|
||||
},
|
||||
|
||||
getState: function () {
|
||||
return this._state;
|
||||
},
|
||||
|
||||
setState: function (state) {
|
||||
this._state = state;
|
||||
},
|
||||
|
||||
clear: function () {
|
||||
if (this._state) {
|
||||
this._state = null;
|
||||
Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -21,6 +21,8 @@ function ManifestEditor(project) {
|
||||
ManifestEditor.prototype = {
|
||||
get manifest() { return this.project.manifest; },
|
||||
|
||||
get editable() { return this.project.type == "packaged"; },
|
||||
|
||||
show: function(containerElement) {
|
||||
let deferred = promise.defer();
|
||||
let iframe = document.createElement("iframe");
|
||||
@ -43,9 +45,12 @@ ManifestEditor.prototype = {
|
||||
let editor = this.editor = new VariablesView(variablesContainer);
|
||||
|
||||
editor.onlyEnumVisible = true;
|
||||
editor.eval = this._onEval;
|
||||
editor.switch = this._onSwitch;
|
||||
editor.delete = this._onDelete;
|
||||
|
||||
if (this.editable) {
|
||||
editor.eval = this._onEval;
|
||||
editor.switch = this._onSwitch;
|
||||
editor.delete = this._onDelete;
|
||||
}
|
||||
|
||||
return this.update();
|
||||
},
|
||||
@ -90,7 +95,7 @@ ManifestEditor.prototype = {
|
||||
},
|
||||
|
||||
save: function() {
|
||||
if (this.project.type == "packaged") {
|
||||
if (this.editable) {
|
||||
let validator = new AppValidator(this.project);
|
||||
let manifestFile = validator._getPackagedManifestFile();
|
||||
let path = manifestFile.path;
|
||||
|
@ -81,19 +81,21 @@ let UI = {
|
||||
return null;
|
||||
},
|
||||
|
||||
addPackaged: function() {
|
||||
let folder = this._selectFolder();
|
||||
addPackaged: function(folder) {
|
||||
if (!folder) {
|
||||
folder = this._selectFolder();
|
||||
}
|
||||
if (!folder)
|
||||
return;
|
||||
AppProjects.addPackaged(folder)
|
||||
.then(function (project) {
|
||||
UI.validate(project);
|
||||
UI.selectProject(project.location);
|
||||
});
|
||||
return AppProjects.addPackaged(folder)
|
||||
.then(function (project) {
|
||||
UI.validate(project);
|
||||
UI.selectProject(project.location);
|
||||
});
|
||||
},
|
||||
|
||||
addHosted: function() {
|
||||
let form = document.querySelector("#new-hosted-project-wrapper")
|
||||
let form = document.querySelector("#new-hosted-project-wrapper");
|
||||
if (!form.checkValidity())
|
||||
return;
|
||||
|
||||
|
@ -80,8 +80,9 @@
|
||||
<div class="project-errors" template='{"type":"textContent","path":"errors"}'></div>
|
||||
<div class="project-warnings" template='{"type":"textContent","path":"warnings"}'></div>
|
||||
</div>
|
||||
<div class="manifest-editor">
|
||||
<h2>&projects.manifestEditor;</h2>
|
||||
<div class="manifest-editor" template='{"type":"attribute","path":"type","name":"type"}'>
|
||||
<h2 class="editable" title="&projects.manifestEditorTooltip;">&projects.manifestEditor;</h2>
|
||||
<h2 class="viewable" title="&projects.manifestViewerTooltip;">&projects.manifestViewer;</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -2,5 +2,6 @@
|
||||
support-files =
|
||||
head.js
|
||||
hosted_app.manifest
|
||||
manifest.webapp
|
||||
|
||||
[browser_manifest_editor.js]
|
||||
|
@ -13,10 +13,10 @@ function test() {
|
||||
Services.prefs.setBoolPref(MANIFEST_EDITOR_ENABLED, true);
|
||||
let tab = yield openAppManager();
|
||||
yield selectProjectsPanel();
|
||||
yield addSampleHostedApp();
|
||||
yield addSamplePackagedApp();
|
||||
yield showSampleProjectDetails();
|
||||
yield changeManifestValue("name", "the best app");
|
||||
yield removeSampleHostedApp();
|
||||
yield removeSamplePackagedApp();
|
||||
yield removeTab(tab);
|
||||
Services.prefs.setBoolPref(MANIFEST_EDITOR_ENABLED, false);
|
||||
finish();
|
||||
|
@ -2,7 +2,7 @@
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
|
||||
|
||||
const {Promise: promise} =
|
||||
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
|
||||
@ -17,6 +17,8 @@ const TEST_BASE =
|
||||
"chrome://mochitests/content/browser/browser/devtools/app-manager/test/";
|
||||
const HOSTED_APP_MANIFEST = TEST_BASE + "hosted_app.manifest";
|
||||
|
||||
const PACKAGED_APP_DIR_PATH = getTestFilePath(".");
|
||||
|
||||
function addTab(url, targetWindow = window) {
|
||||
info("Adding tab: " + url);
|
||||
|
||||
@ -72,6 +74,18 @@ function removeSampleHostedApp() {
|
||||
return AppProjects.remove(HOSTED_APP_MANIFEST);
|
||||
}
|
||||
|
||||
function addSamplePackagedApp() {
|
||||
info("Adding sample packaged app");
|
||||
let appDir = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);
|
||||
appDir.initWithPath(PACKAGED_APP_DIR_PATH);
|
||||
return getProjectsWindow().UI.addPackaged(appDir);
|
||||
}
|
||||
|
||||
function removeSamplePackagedApp() {
|
||||
info("Removing sample packaged app");
|
||||
return AppProjects.remove(PACKAGED_APP_DIR_PATH);
|
||||
}
|
||||
|
||||
function getProjectsWindow() {
|
||||
return content.document.querySelector(".projects-panel").contentWindow;
|
||||
}
|
||||
|
3
browser/devtools/app-manager/test/manifest.webapp
Normal file
3
browser/devtools/app-manager/test/manifest.webapp
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "My packaged app"
|
||||
}
|
@ -1836,9 +1836,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
}
|
||||
// If the filename is relative, tack it onto the download directory
|
||||
if (!filename.match(/[\\\/]/)) {
|
||||
let tempfile = yield Downloads.getPreferredDownloadsDirectory();
|
||||
tempfile.append(filename);
|
||||
filename = tempfile.path;
|
||||
let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
|
||||
filename = OS.Path.join(preferredDir, filename);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -15,7 +15,6 @@ function SourcesView() {
|
||||
this._onEditorLoad = this._onEditorLoad.bind(this);
|
||||
this._onEditorUnload = this._onEditorUnload.bind(this);
|
||||
this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this);
|
||||
this._onEditorContextMenu = this._onEditorContextMenu.bind(this);
|
||||
this._onSourceSelect = this._onSourceSelect.bind(this);
|
||||
this._onSourceClick = this._onSourceClick.bind(this);
|
||||
this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
|
||||
@ -649,7 +648,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
||||
*/
|
||||
_onEditorLoad: function(aName, aEditor) {
|
||||
aEditor.on("cursorActivity", this._onEditorCursorActivity);
|
||||
aEditor.on("contextMenu", this._onEditorContextMenu);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -657,7 +655,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
||||
*/
|
||||
_onEditorUnload: function(aName, aEditor) {
|
||||
aEditor.off("cursorActivity", this._onEditorCursorActivity);
|
||||
aEditor.off("contextMenu", this._onEditorContextMenu);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -678,13 +675,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The context menu listener for the source editor.
|
||||
*/
|
||||
_onEditorContextMenu: function({ x, y }) {
|
||||
this._editorContextMenuLineNumber = DebuggerView.editor.getPositionFromCoords(x, y).line;
|
||||
},
|
||||
|
||||
/**
|
||||
* The select listener for the sources container.
|
||||
*/
|
||||
@ -874,15 +864,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
||||
* Called when the add breakpoint key sequence was pressed.
|
||||
*/
|
||||
_onCmdAddBreakpoint: function() {
|
||||
// If this command was executed via the context menu, add the breakpoint
|
||||
// on the currently hovered line in the source editor.
|
||||
if (this._editorContextMenuLineNumber >= 0) {
|
||||
DebuggerView.editor.setCursor({ line: this._editorContextMenuLineNumber, ch: 0 });
|
||||
}
|
||||
|
||||
// Avoid placing breakpoints incorrectly when using key shortcuts.
|
||||
this._editorContextMenuLineNumber = -1;
|
||||
|
||||
let url = DebuggerView.Sources.selectedValue;
|
||||
let line = DebuggerView.editor.getCursor().line + 1;
|
||||
let location = { url: url, line: line };
|
||||
@ -902,15 +883,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
||||
* Called when the add conditional breakpoint key sequence was pressed.
|
||||
*/
|
||||
_onCmdAddConditionalBreakpoint: function() {
|
||||
// If this command was executed via the context menu, add the breakpoint
|
||||
// on the currently hovered line in the source editor.
|
||||
if (this._editorContextMenuLineNumber >= 0) {
|
||||
DebuggerView.editor.setCursor({ line: this._editorContextMenuLineNumber, ch: 0 });
|
||||
}
|
||||
|
||||
// Avoid placing breakpoints incorrectly when using key shortcuts.
|
||||
this._editorContextMenuLineNumber = -1;
|
||||
|
||||
let url = DebuggerView.Sources.selectedValue;
|
||||
let line = DebuggerView.editor.getCursor().line + 1;
|
||||
let location = { url: url, line: line };
|
||||
@ -1043,7 +1015,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
||||
_cbPanel: null,
|
||||
_cbTextbox: null,
|
||||
_selectedBreakpointItem: null,
|
||||
_editorContextMenuLineNumber: -1,
|
||||
_conditionalPopupVisible: false
|
||||
});
|
||||
|
||||
|
@ -862,7 +862,7 @@ FilterView.prototype = {
|
||||
_performLineSearch: function(aLine) {
|
||||
// Make sure we're actually searching for a valid line.
|
||||
if (aLine) {
|
||||
DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 });
|
||||
DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 }, "center");
|
||||
}
|
||||
},
|
||||
|
||||
@ -1497,6 +1497,7 @@ FilteredFunctionsView.prototype = Heritage.extend(ResultsPanelContainer.prototyp
|
||||
DebuggerView.setEditorLocation(sourceUrl, actualLocation.start.line, {
|
||||
charOffset: scriptOffset,
|
||||
columnOffset: actualLocation.start.column,
|
||||
align: "center",
|
||||
noDebug: true
|
||||
});
|
||||
}
|
||||
|
@ -457,7 +457,8 @@ let DebuggerView = {
|
||||
}
|
||||
|
||||
if (!aFlags.noCaret) {
|
||||
this.editor.setCursor({ line: aLine -1, ch: aFlags.columnOffset || 0 });
|
||||
this.editor.setCursor({ line: aLine -1, ch: aFlags.columnOffset || 0 },
|
||||
aFlags.align);
|
||||
}
|
||||
|
||||
if (!aFlags.noDebug) {
|
||||
|
@ -16,6 +16,7 @@ support-files =
|
||||
code_math.js
|
||||
code_math.map
|
||||
code_math.min.js
|
||||
code_math_bogus_map.min.js
|
||||
code_script-switching-01.js
|
||||
code_script-switching-02.js
|
||||
code_test-editor-mode
|
||||
@ -43,6 +44,7 @@ support-files =
|
||||
doc_inline-script.html
|
||||
doc_large-array-buffer.html
|
||||
doc_minified.html
|
||||
doc_minified_bogus_map.html
|
||||
doc_pause-exceptions.html
|
||||
doc_pretty-print.html
|
||||
doc_pretty-print-2.html
|
||||
@ -152,6 +154,7 @@ support-files =
|
||||
[browser_dbg_source-maps-01.js]
|
||||
[browser_dbg_source-maps-02.js]
|
||||
[browser_dbg_source-maps-03.js]
|
||||
[browser_dbg_source-maps-04.js]
|
||||
[browser_dbg_sources-cache.js]
|
||||
[browser_dbg_sources-labels.js]
|
||||
[browser_dbg_sources-sorting.js]
|
||||
|
@ -92,21 +92,21 @@ function test() {
|
||||
|
||||
function addBreakpoint2() {
|
||||
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED);
|
||||
setContextPosition(19);
|
||||
setCaretPosition(19);
|
||||
gSources._onCmdAddBreakpoint();
|
||||
return finished;
|
||||
}
|
||||
|
||||
function modBreakpoint2() {
|
||||
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
|
||||
setContextPosition(19);
|
||||
setCaretPosition(19);
|
||||
gSources._onCmdAddConditionalBreakpoint();
|
||||
return finished;
|
||||
}
|
||||
|
||||
function addBreakpoint3() {
|
||||
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED);
|
||||
setContextPosition(20);
|
||||
setCaretPosition(20);
|
||||
gSources._onCmdAddConditionalBreakpoint();
|
||||
return finished;
|
||||
}
|
||||
@ -120,14 +120,14 @@ function test() {
|
||||
|
||||
function addBreakpoint4() {
|
||||
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED);
|
||||
setContextPosition(21);
|
||||
setCaretPosition(21);
|
||||
gSources._onCmdAddBreakpoint();
|
||||
return finished;
|
||||
}
|
||||
|
||||
function delBreakpoint4() {
|
||||
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED);
|
||||
setContextPosition(21);
|
||||
setCaretPosition(21);
|
||||
gSources._onCmdAddBreakpoint();
|
||||
return finished;
|
||||
}
|
||||
@ -184,10 +184,6 @@ function test() {
|
||||
gEditor.setCursor({ line: aLine - 1, ch: 0 });
|
||||
}
|
||||
|
||||
function setContextPosition(aLine) {
|
||||
gSources._editorContextMenuLineNumber = aLine - 1;
|
||||
}
|
||||
|
||||
function clickOnBreakpoint(aIndex) {
|
||||
EventUtils.sendMouseEvent({ type: "click" },
|
||||
gDebugger.document.querySelectorAll(".dbg-breakpoint")[aIndex],
|
||||
|
167
browser/devtools/debugger/test/browser_dbg_source-maps-04.js
Normal file
167
browser/devtools/debugger/test/browser_dbg_source-maps-04.js
Normal file
@ -0,0 +1,167 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Test that bogus source maps don't break debugging.
|
||||
*/
|
||||
|
||||
const TAB_URL = EXAMPLE_URL + "doc_minified_bogus_map.html";
|
||||
const JS_URL = EXAMPLE_URL + "code_math_bogus_map.min.js";
|
||||
|
||||
// This test causes an error to be logged in the console, which appears in TBPL
|
||||
// logs, so we are disabling that here.
|
||||
let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
|
||||
DevToolsUtils.reportingDisabled = true;
|
||||
|
||||
let gPanel, gDebugger, gFrames, gSources, gPrefs, gOptions;
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
gPanel = aPanel;
|
||||
gDebugger = gPanel.panelWin;
|
||||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gSources = gDebugger.DebuggerView.Sources;
|
||||
gPrefs = gDebugger.Prefs;
|
||||
gOptions = gDebugger.DebuggerView.Options;
|
||||
|
||||
is(gPrefs.pauseOnExceptions, false,
|
||||
"The pause-on-exceptions pref should be disabled by default.");
|
||||
isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
|
||||
"The pause-on-exceptions menu item should not be checked.");
|
||||
|
||||
waitForSourceShown(gPanel, JS_URL)
|
||||
.then(checkInitialSource)
|
||||
.then(enablePauseOnExceptions)
|
||||
.then(disableIgnoreCaughtExceptions)
|
||||
.then(testSetBreakpoint)
|
||||
.then(reloadPage)
|
||||
.then(testHitBreakpoint)
|
||||
.then(enableIgnoreCaughtExceptions)
|
||||
.then(disablePauseOnExceptions)
|
||||
.then(() => closeDebuggerAndFinish(gPanel))
|
||||
.then(null, aError => {
|
||||
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkInitialSource() {
|
||||
isnot(gSources.selectedValue.indexOf(".min.js"), -1,
|
||||
"The debugger should show the minified js file.");
|
||||
}
|
||||
|
||||
function enablePauseOnExceptions() {
|
||||
let deferred = promise.defer();
|
||||
|
||||
gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
|
||||
is(gPrefs.pauseOnExceptions, true,
|
||||
"The pause-on-exceptions pref should now be enabled.");
|
||||
|
||||
ok(true, "Pausing on exceptions was enabled.");
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
gOptions._pauseOnExceptionsItem.setAttribute("checked", "true");
|
||||
gOptions._togglePauseOnExceptions();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function disableIgnoreCaughtExceptions() {
|
||||
let deferred = promise.defer();
|
||||
|
||||
gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
|
||||
is(gPrefs.ignoreCaughtExceptions, false,
|
||||
"The ignore-caught-exceptions pref should now be disabled.");
|
||||
|
||||
ok(true, "Ignore caught exceptions was disabled.");
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false");
|
||||
gOptions._toggleIgnoreCaughtExceptions();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function testSetBreakpoint() {
|
||||
let deferred = promise.defer();
|
||||
|
||||
gDebugger.gThreadClient.setBreakpoint({ url: JS_URL, line: 3, column: 61 }, aResponse => {
|
||||
ok(!aResponse.error,
|
||||
"Should be able to set a breakpoint in a js file.");
|
||||
ok(!aResponse.actualLocation,
|
||||
"Should be able to set a breakpoint on line 3 and column 61.");
|
||||
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
let loaded = waitForSourceAndCaret(gPanel, ".js", 3);
|
||||
gDebugger.gClient.activeTab.reload();
|
||||
return loaded.then(() => ok(true, "Page was reloaded and execution resumed."));
|
||||
}
|
||||
|
||||
function testHitBreakpoint() {
|
||||
let deferred = promise.defer();
|
||||
|
||||
gDebugger.gThreadClient.resume(aResponse => {
|
||||
ok(!aResponse.error, "Shouldn't get an error resuming.");
|
||||
is(aResponse.type, "resumed", "Type should be 'resumed'.");
|
||||
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
|
||||
is(gFrames.itemCount, 1, "Should have one frame.");
|
||||
|
||||
gDebugger.gThreadClient.resume(deferred.resolve);
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function enableIgnoreCaughtExceptions() {
|
||||
let deferred = promise.defer();
|
||||
|
||||
gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
|
||||
is(gPrefs.ignoreCaughtExceptions, true,
|
||||
"The ignore-caught-exceptions pref should now be enabled.");
|
||||
|
||||
ok(true, "Ignore caught exceptions was enabled.");
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true");
|
||||
gOptions._toggleIgnoreCaughtExceptions();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function disablePauseOnExceptions() {
|
||||
let deferred = promise.defer();
|
||||
|
||||
gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
|
||||
is(gPrefs.pauseOnExceptions, false,
|
||||
"The pause-on-exceptions pref should now be disabled.");
|
||||
|
||||
ok(true, "Pausing on exceptions was disabled.");
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
gOptions._pauseOnExceptionsItem.setAttribute("checked", "false");
|
||||
gOptions._togglePauseOnExceptions();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
gPanel = null;
|
||||
gDebugger = null;
|
||||
gFrames = null;
|
||||
gSources = null;
|
||||
gPrefs = null;
|
||||
gOptions = null;
|
||||
DevToolsUtils.reportingDisabled = false;
|
||||
});
|
4
browser/devtools/debugger/test/code_math_bogus_map.min.js
vendored
Normal file
4
browser/devtools/debugger/test/code_math_bogus_map.min.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
function stopMe(){throw Error("boom");}try{stopMe();var a=1;a=a*2;}catch(e){};
|
||||
//# sourceMappingURL=bogus.map
|
14
browser/devtools/debugger/test/doc_minified_bogus_map.html
Normal file
14
browser/devtools/debugger/test/doc_minified_bogus_map.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Debugger test page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="code_math_bogus_map.min.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -86,6 +86,8 @@ InspectorPanel.prototype = {
|
||||
_deferredOpen: function(defaultSelection) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
this.outerHTMLEditable = this._target.client.traits.editOuterHTML;
|
||||
|
||||
this.onNewRoot = this.onNewRoot.bind(this);
|
||||
this.walker.on("new-root", this.onNewRoot);
|
||||
|
||||
@ -593,7 +595,8 @@ InspectorPanel.prototype = {
|
||||
let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
|
||||
let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
|
||||
let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
|
||||
if (this.selection.isElementNode()) {
|
||||
let selectionIsElement = this.selection.isElementNode();
|
||||
if (selectionIsElement) {
|
||||
unique.removeAttribute("disabled");
|
||||
copyInnerHTML.removeAttribute("disabled");
|
||||
copyOuterHTML.removeAttribute("disabled");
|
||||
@ -602,6 +605,13 @@ InspectorPanel.prototype = {
|
||||
copyInnerHTML.setAttribute("disabled", "true");
|
||||
copyOuterHTML.setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
|
||||
if (this.outerHTMLEditable && selectionIsElement) {
|
||||
editHTML.removeAttribute("disabled");
|
||||
} else {
|
||||
editHTML.setAttribute("disabled", "true");
|
||||
}
|
||||
},
|
||||
|
||||
_resetNodeMenu: function InspectorPanel_resetNodeMenu() {
|
||||
@ -705,6 +715,19 @@ InspectorPanel.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the outerHTML of the selected Node.
|
||||
*/
|
||||
editHTML: function InspectorPanel_editHTML()
|
||||
{
|
||||
if (!this.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
if (this.markup) {
|
||||
this.markup.beginEditingOuterHTML(this.selection.nodeFront);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy the innerHTML of the selected Node to the clipboard.
|
||||
*/
|
||||
|
@ -33,6 +33,10 @@
|
||||
<popupset id="inspectorPopupSet">
|
||||
<!-- Used by the Markup Panel, the Highlighter and the Breadcrumbs -->
|
||||
<menupopup id="inspector-node-popup">
|
||||
<menuitem id="node-menu-edithtml"
|
||||
label="&inspectorHTMLEdit.label;"
|
||||
accesskey="&inspectorHTMLEdit.accesskey;"
|
||||
oncommand="inspector.editHTML()"/>
|
||||
<menuitem id="node-menu-copyinner"
|
||||
label="&inspectorHTMLCopyInner.label;"
|
||||
accesskey="&inspectorHTMLCopyInner.accesskey;"
|
||||
|
@ -37,6 +37,7 @@ browser.jar:
|
||||
content/browser/devtools/codemirror/htmlmixed.js (sourceeditor/codemirror/htmlmixed.js)
|
||||
content/browser/devtools/codemirror/activeline.js (sourceeditor/codemirror/activeline.js)
|
||||
content/browser/devtools/codemirror/matchbrackets.js (sourceeditor/codemirror/matchbrackets.js)
|
||||
content/browser/devtools/codemirror/closebrackets.js (sourceeditor/codemirror/closebrackets.js)
|
||||
content/browser/devtools/codemirror/comment.js (sourceeditor/codemirror/comment.js)
|
||||
content/browser/devtools/codemirror/searchcursor.js (sourceeditor/codemirror/search/searchcursor.js)
|
||||
content/browser/devtools/codemirror/search.js (sourceeditor/codemirror/search/search.js)
|
||||
|
182
browser/devtools/markupview/html-editor.js
Normal file
182
browser/devtools/markupview/html-editor.js
Normal file
@ -0,0 +1,182 @@
|
||||
/* vim:set ts=2 sw=2 sts=2 et tw=80:
|
||||
* 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";
|
||||
|
||||
const {Cu} = require("chrome");
|
||||
const Editor = require("devtools/sourceeditor/editor");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
|
||||
|
||||
exports.HTMLEditor = HTMLEditor;
|
||||
|
||||
function ctrl(k) {
|
||||
return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
|
||||
}
|
||||
function stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
/**
|
||||
* A wrapper around the Editor component, that allows editing of HTML.
|
||||
*
|
||||
* The main functionality this provides around the Editor is the ability
|
||||
* to show/hide/position an editor inplace. It only appends once to the
|
||||
* body, and uses CSS to position the editor. The reason it is done this
|
||||
* way is that the editor is loaded in an iframe, and calling appendChild
|
||||
* causes it to reload.
|
||||
*
|
||||
* Meant to be embedded inside of an HTML page, as in markup-view.xhtml.
|
||||
*
|
||||
* @param HTMLDocument htmlDocument
|
||||
* The document to attach the editor to. Will also use this
|
||||
* document as a basis for listening resize events.
|
||||
*/
|
||||
function HTMLEditor(htmlDocument)
|
||||
{
|
||||
this.doc = htmlDocument;
|
||||
this.container = this.doc.createElement("div");
|
||||
this.container.className = "html-editor theme-body";
|
||||
this.container.style.display = "none";
|
||||
this.editorInner = this.doc.createElement("div");
|
||||
this.editorInner.className = "html-editor-inner";
|
||||
this.container.appendChild(this.editorInner);
|
||||
|
||||
this.doc.body.appendChild(this.container);
|
||||
this.hide = this.hide.bind(this);
|
||||
this.refresh = this.refresh.bind(this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.doc.defaultView.addEventListener("resize",
|
||||
this.refresh, true);
|
||||
|
||||
let config = {
|
||||
mode: Editor.modes.html,
|
||||
lineWrapping: true,
|
||||
styleActiveLine: false,
|
||||
extraKeys: {},
|
||||
theme: "mozilla markup-view"
|
||||
};
|
||||
|
||||
config.extraKeys[ctrl("Enter")] = this.hide;
|
||||
config.extraKeys["Esc"] = this.hide.bind(this, false);
|
||||
|
||||
this.container.addEventListener("click", this.hide, false);
|
||||
this.editorInner.addEventListener("click", stopPropagation, false);
|
||||
this.editor = new Editor(config);
|
||||
|
||||
this.editor.appendTo(this.editorInner).then(() => {
|
||||
this.hide(false);
|
||||
}).then(null, (err) => console.log(err.message));
|
||||
}
|
||||
|
||||
HTMLEditor.prototype = {
|
||||
|
||||
/**
|
||||
* Need to refresh position by manually setting CSS values, so this will
|
||||
* need to be called on resizes and other sizing changes.
|
||||
*/
|
||||
refresh: function() {
|
||||
let element = this._attachedElement;
|
||||
|
||||
if (element) {
|
||||
this.container.style.top = element.offsetTop + "px";
|
||||
this.container.style.left = element.offsetLeft + "px";
|
||||
this.container.style.width = element.offsetWidth + "px";
|
||||
this.container.style.height = element.parentNode.offsetHeight + "px";
|
||||
this.editor.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Anchor the editor to a particular element.
|
||||
*
|
||||
* @param DOMNode element
|
||||
* The element that the editor will be anchored to.
|
||||
* Should belong to the HTMLDocument passed into the constructor.
|
||||
*/
|
||||
_attach: function(element)
|
||||
{
|
||||
this._detach();
|
||||
this._attachedElement = element;
|
||||
element.classList.add("html-editor-container");
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
/**
|
||||
* Unanchor the editor from an element.
|
||||
*/
|
||||
_detach: function()
|
||||
{
|
||||
if (this._attachedElement) {
|
||||
this._attachedElement.classList.remove("html-editor-container");
|
||||
this._attachedElement = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Anchor the editor to a particular element, and show the editor.
|
||||
*
|
||||
* @param DOMNode element
|
||||
* The element that the editor will be anchored to.
|
||||
* Should belong to the HTMLDocument passed into the constructor.
|
||||
* @param string text
|
||||
* Value to set the contents of the editor to
|
||||
* @param function cb
|
||||
* The function to call when hiding
|
||||
*/
|
||||
show: function(element, text)
|
||||
{
|
||||
if (this._visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._originalValue = text;
|
||||
this.editor.setText(text);
|
||||
this._attach(element);
|
||||
this.container.style.display = "flex";
|
||||
this._visible = true;
|
||||
|
||||
this.editor.refresh();
|
||||
this.editor.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the editor, optionally committing the changes
|
||||
*
|
||||
* @param bool shouldCommit
|
||||
* A change will be committed by default. If this param
|
||||
* strictly equals false, no change will occur.
|
||||
*/
|
||||
hide: function(shouldCommit)
|
||||
{
|
||||
if (!this._visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.style.display = "none";
|
||||
this._detach();
|
||||
|
||||
let newValue = this.editor.getText();
|
||||
let valueHasChanged = this._originalValue !== newValue;
|
||||
let preventCommit = shouldCommit === false || !valueHasChanged;
|
||||
this.emit("popup-hidden", !preventCommit, newValue);
|
||||
this._originalValue = undefined;
|
||||
this._visible = undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy this object and unbind all event handlers
|
||||
*/
|
||||
destroy: function()
|
||||
{
|
||||
this.doc.defaultView.removeEventListener("resize",
|
||||
this.refresh, true);
|
||||
this.container.removeEventListener("click", this.hide, false);
|
||||
this.editorInner.removeEventListener("click", stopPropagation, false);
|
||||
|
||||
this.hide(false);
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
};
|
@ -14,6 +14,32 @@
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.html-editor {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
|
||||
/* Use the same margin/padding trick used by .child tags to ensure that
|
||||
* the editor covers up any content to the left (including expander arrows
|
||||
* and hover effects). */
|
||||
margin-left: -1000em;
|
||||
padding-left: 1000em;
|
||||
}
|
||||
|
||||
.html-editor-inner {
|
||||
border: solid .1px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.html-editor iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.children {
|
||||
@ -36,6 +62,11 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.html-editor-container {
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* This extra element placed in each tag is positioned absolutely to cover the
|
||||
* whole tag line and is used for background styling (when a selection is made
|
||||
* or when the tag is flashing) */
|
||||
|
@ -18,6 +18,7 @@ const CONTAINER_FLASHING_DURATION = 500;
|
||||
const {UndoStack} = require("devtools/shared/undo");
|
||||
const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
|
||||
const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
|
||||
const {HTMLEditor} = require("devtools/markupview/html-editor");
|
||||
const {OutputParser} = require("devtools/output-parser");
|
||||
const promise = require("sdk/core/promise");
|
||||
|
||||
@ -57,6 +58,7 @@ function MarkupView(aInspector, aFrame, aControllerWindow) {
|
||||
this.doc = this._frame.contentDocument;
|
||||
this._elt = this.doc.querySelector("#root");
|
||||
this._outputParser = new OutputParser();
|
||||
this.htmlEditor = new HTMLEditor(this.doc);
|
||||
|
||||
this.layoutHelpers = new LayoutHelpers(this.doc.defaultView);
|
||||
|
||||
@ -149,6 +151,7 @@ MarkupView.prototype = {
|
||||
* Highlight the inspector selected node.
|
||||
*/
|
||||
_onNewSelection: function() {
|
||||
this.htmlEditor.hide();
|
||||
let done = this._inspector.updating("markup-view");
|
||||
if (this._inspector.selection.isNode()) {
|
||||
this.showNode(this._inspector.selection.nodeFront, true).then(() => {
|
||||
@ -336,8 +339,8 @@ MarkupView.prototype = {
|
||||
}
|
||||
|
||||
let node = aContainer.node;
|
||||
this.markNodeAsSelected(node);
|
||||
this._inspector.selection.setNodeFront(node, "treepanel");
|
||||
this.markNodeAsSelected(node, "treepanel");
|
||||
|
||||
// This event won't be fired if the node is the same. But the highlighter
|
||||
// need to lock the node if it wasn't.
|
||||
this._inspector.selection.emit("new-node");
|
||||
@ -390,6 +393,9 @@ MarkupView.prototype = {
|
||||
*/
|
||||
_mutationObserver: function(aMutations) {
|
||||
let requiresLayoutChange = false;
|
||||
let reselectParent;
|
||||
let reselectChildIndex;
|
||||
|
||||
for (let mutation of aMutations) {
|
||||
let type = mutation.type;
|
||||
let target = mutation.target;
|
||||
@ -418,20 +424,51 @@ MarkupView.prototype = {
|
||||
requiresLayoutChange = true;
|
||||
}
|
||||
} else if (type === "childList") {
|
||||
let isFromOuterHTML = mutation.removed.some((n) => {
|
||||
return n === this._outerHTMLNode;
|
||||
});
|
||||
|
||||
// Keep track of which node should be reselected after mutations.
|
||||
if (isFromOuterHTML) {
|
||||
reselectParent = target;
|
||||
reselectChildIndex = this._outerHTMLChildIndex;
|
||||
|
||||
delete this._outerHTMLNode;
|
||||
delete this._outerHTMLChildIndex;
|
||||
}
|
||||
|
||||
container.childrenDirty = true;
|
||||
// Update the children to take care of changes in the DOM
|
||||
// Passing true as the last parameter asks for mutation flashing of the
|
||||
// new nodes
|
||||
this._updateChildren(container, {flash: true});
|
||||
// Update the children to take care of changes in the markup view DOM.
|
||||
this._updateChildren(container, {flash: !isFromOuterHTML});
|
||||
}
|
||||
}
|
||||
|
||||
if (requiresLayoutChange) {
|
||||
this._inspector.immediateLayoutChange();
|
||||
}
|
||||
this._waitForChildren().then(() => {
|
||||
this._waitForChildren().then((nodes) => {
|
||||
this._flashMutatedNodes(aMutations);
|
||||
this._inspector.emit("markupmutation");
|
||||
this._inspector.emit("markupmutation", aMutations);
|
||||
|
||||
// Since the htmlEditor is absolutely positioned, a mutation may change
|
||||
// the location in which it should be shown.
|
||||
this.htmlEditor.refresh();
|
||||
|
||||
// If a node has had its outerHTML set, the parent node will be selected.
|
||||
// Reselect the original node immediately.
|
||||
if (this._inspector.selection.nodeFront === reselectParent) {
|
||||
this.walker.children(reselectParent).then((o) => {
|
||||
let node = o.nodes[reselectChildIndex];
|
||||
let container = this._containers.get(node);
|
||||
if (node && container) {
|
||||
this.markNodeAsSelected(node, "outerhtml");
|
||||
if (container.hasChildren) {
|
||||
this.expandNode(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -551,6 +588,94 @@ MarkupView.prototype = {
|
||||
container.expanded = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the outerHTML for a remote node.
|
||||
* @param aNode The NodeFront to get the outerHTML for.
|
||||
* @returns A promise that will be resolved with the outerHTML.
|
||||
*/
|
||||
getNodeOuterHTML: function(aNode) {
|
||||
let def = promise.defer();
|
||||
this.walker.outerHTML(aNode).then(longstr => {
|
||||
longstr.string().then(outerHTML => {
|
||||
longstr.release().then(null, console.error);
|
||||
def.resolve(outerHTML);
|
||||
});
|
||||
});
|
||||
return def.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the index of a child within its parent's children list.
|
||||
* @param aNode The NodeFront to find the index of.
|
||||
* @returns A promise that will be resolved with the integer index.
|
||||
* If the child cannot be found, returns -1
|
||||
*/
|
||||
getNodeChildIndex: function(aNode) {
|
||||
let def = promise.defer();
|
||||
let parentNode = aNode.parentNode();
|
||||
|
||||
// Node may have been removed from the DOM, instead of throwing an error,
|
||||
// return -1 indicating that it isn't inside of its parent children list.
|
||||
if (!parentNode) {
|
||||
def.resolve(-1);
|
||||
} else {
|
||||
this.walker.children(parentNode).then(children => {
|
||||
def.resolve(children.nodes.indexOf(aNode));
|
||||
});
|
||||
}
|
||||
|
||||
return def.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the index of a child within its parent's children collection.
|
||||
* @param aNode The NodeFront to find the index of.
|
||||
* @param newValue The new outerHTML to set on the node.
|
||||
* @param oldValue The old outerHTML that will be reverted to find the index of.
|
||||
* @returns A promise that will be resolved with the integer index.
|
||||
* If the child cannot be found, returns -1
|
||||
*/
|
||||
updateNodeOuterHTML: function(aNode, newValue, oldValue) {
|
||||
let container = this._containers.get(aNode);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getNodeChildIndex(aNode).then((i) => {
|
||||
this._outerHTMLChildIndex = i;
|
||||
this._outerHTMLNode = aNode;
|
||||
|
||||
container.undo.do(() => {
|
||||
this.walker.setOuterHTML(aNode, newValue);
|
||||
}, () => {
|
||||
this.walker.setOuterHTML(aNode, oldValue);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Open an editor in the UI to allow editing of a node's outerHTML.
|
||||
* @param aNode The NodeFront to edit.
|
||||
*/
|
||||
beginEditingOuterHTML: function(aNode) {
|
||||
this.getNodeOuterHTML(aNode).then((oldValue)=> {
|
||||
let container = this._containers.get(aNode);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
this.htmlEditor.show(container.tagLine, oldValue);
|
||||
this.htmlEditor.once("popup-hidden", (e, aCommit, aValue) => {
|
||||
if (aCommit) {
|
||||
this.updateNodeOuterHTML(aNode, aValue, oldValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark the given node expanded.
|
||||
* @param aNode The NodeFront to mark as expanded.
|
||||
*/
|
||||
setNodeExpanded: function(aNode, aExpanded) {
|
||||
if (aExpanded) {
|
||||
this.expandNode(aNode);
|
||||
@ -560,9 +685,11 @@ MarkupView.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark the given node selected.
|
||||
* Mark the given node selected, and update the inspector.selection
|
||||
* object's NodeFront to keep consistent state between UI and selection.
|
||||
* @param aNode The NodeFront to mark as selected.
|
||||
*/
|
||||
markNodeAsSelected: function(aNode) {
|
||||
markNodeAsSelected: function(aNode, reason) {
|
||||
let container = this._containers.get(aNode);
|
||||
if (this._selectedContainer === container) {
|
||||
return false;
|
||||
@ -575,6 +702,7 @@ MarkupView.prototype = {
|
||||
this._selectedContainer.selected = true;
|
||||
}
|
||||
|
||||
this._inspector.selection.setNodeFront(aNode, reason || "nodeselected");
|
||||
return true;
|
||||
},
|
||||
|
||||
@ -779,6 +907,9 @@ MarkupView.prototype = {
|
||||
destroy: function() {
|
||||
gDevTools.off("pref-changed", this._handlePrefChange);
|
||||
|
||||
this.htmlEditor.destroy();
|
||||
delete this.htmlEditor;
|
||||
|
||||
this.undo.destroy();
|
||||
delete this.undo;
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
<link rel="stylesheet" href="chrome://browser/skin/devtools/markup-view.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
|
||||
|
||||
<script type="application/javascript;version=1.8" src="theme-switching.js"/>
|
||||
<script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"></script>
|
||||
|
||||
</head>
|
||||
<body class="theme-body devtools-monospace" role="application">
|
||||
|
@ -6,6 +6,7 @@ support-files = head.js
|
||||
skip-if = true
|
||||
[browser_inspector_markup_edit.html]
|
||||
[browser_inspector_markup_edit.js]
|
||||
[browser_inspector_markup_edit_outerhtml.js]
|
||||
[browser_inspector_markup_mutation.html]
|
||||
[browser_inspector_markup_mutation.js]
|
||||
[browser_inspector_markup_mutation_flashing.html]
|
||||
|
@ -0,0 +1,295 @@
|
||||
/* Any copyright", " is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
|
||||
function test() {
|
||||
let inspector;
|
||||
let doc;
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
gBrowser.selectedTab = gBrowser.addTab();
|
||||
gBrowser.selectedBrowser.addEventListener("load", function onload() {
|
||||
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
|
||||
doc = content.document;
|
||||
waitForFocus(setupTest, content);
|
||||
}, true);
|
||||
|
||||
let outerHTMLs = [
|
||||
{
|
||||
selector: "#one",
|
||||
oldHTML: '<div id="one">First <em>Div</em></div>',
|
||||
newHTML: '<div id="one">First Div</div>',
|
||||
validate: function(pageNode, selectedNode) {
|
||||
is (pageNode.textContent, "First Div", "New div has expected text content");
|
||||
ok (!doc.querySelector("#one em"), "No em remaining")
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "#removedChildren",
|
||||
oldHTML: '<div id="removedChildren">removedChild <i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>',
|
||||
newHTML: '<div id="removedChildren">removedChild</div>'
|
||||
},
|
||||
{
|
||||
selector: "#addedChildren",
|
||||
oldHTML: '<div id="addedChildren">addedChildren</div>',
|
||||
newHTML: '<div id="addedChildren">addedChildren <i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>'
|
||||
},
|
||||
{
|
||||
selector: "#addedAttribute",
|
||||
oldHTML: '<div id="addedAttribute">addedAttribute</div>',
|
||||
newHTML: '<div id="addedAttribute" class="important" disabled checked>addedAttribute</div>',
|
||||
validate: function(pageNode, selectedNode) {
|
||||
is (pageNode, selectedNode, "Original element is selected");
|
||||
is (pageNode.outerHTML, '<div id="addedAttribute" class="important" disabled="" checked="">addedAttribute</div>',
|
||||
"Attributes have been added");
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "#changedTag",
|
||||
oldHTML: '<div id="changedTag">changedTag</div>',
|
||||
newHTML: '<p id="changedTag" class="important">changedTag</p>'
|
||||
},
|
||||
{
|
||||
selector: "#badMarkup1",
|
||||
oldHTML: '<div id="badMarkup1">badMarkup1</div>',
|
||||
newHTML: '<div id="badMarkup1">badMarkup1</div> hanging</div>',
|
||||
validate: function(pageNode, selectedNode) {
|
||||
is (pageNode, selectedNode, "Original element is selected");
|
||||
|
||||
let textNode = pageNode.nextSibling;
|
||||
|
||||
is (textNode.nodeName, "#text", "Sibling is a text element");
|
||||
is (textNode.data, " hanging", "New text node has expected text content");
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "#badMarkup2",
|
||||
oldHTML: '<div id="badMarkup2">badMarkup2</div>',
|
||||
newHTML: '<div id="badMarkup2">badMarkup2</div> hanging<div></div></div></div></body>',
|
||||
validate: function(pageNode, selectedNode) {
|
||||
is (pageNode, selectedNode, "Original element is selected");
|
||||
|
||||
let textNode = pageNode.nextSibling;
|
||||
|
||||
is (textNode.nodeName, "#text", "Sibling is a text element");
|
||||
is (textNode.data, " hanging", "New text node has expected text content");
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "#badMarkup3",
|
||||
oldHTML: '<div id="badMarkup3">badMarkup3</div>',
|
||||
newHTML: '<div id="badMarkup3">badMarkup3 <em>Emphasized <strong> and strong</div>',
|
||||
validate: function(pageNode, selectedNode) {
|
||||
is (pageNode, selectedNode, "Original element is selected");
|
||||
|
||||
let em = doc.querySelector("#badMarkup3 em");
|
||||
let strong = doc.querySelector("#badMarkup3 strong");
|
||||
|
||||
is (em.textContent, "Emphasized and strong", "<em> was auto created");
|
||||
is (strong.textContent, " and strong", "<strong> was auto created");
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "#badMarkup4",
|
||||
oldHTML: '<div id="badMarkup4">badMarkup4</div>',
|
||||
newHTML: '<div id="badMarkup4">badMarkup4</p>',
|
||||
validate: function(pageNode, selectedNode) {
|
||||
is (pageNode, selectedNode, "Original element is selected");
|
||||
|
||||
let div = doc.querySelector("#badMarkup4");
|
||||
let p = doc.querySelector("#badMarkup4 p");
|
||||
|
||||
is (div.textContent, "badMarkup4", "textContent is correct");
|
||||
is (div.tagName, "DIV", "did not change to <p> tag");
|
||||
is (p.textContent, "", "The <p> tag has no children");
|
||||
is (p.tagName, "P", "Created an empty <p> tag");
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "#badMarkup5",
|
||||
oldHTML: '<p id="badMarkup5">badMarkup5</p>',
|
||||
newHTML: '<p id="badMarkup5">badMarkup5 <div>with a nested div</div></p>',
|
||||
validate: function(pageNode, selectedNode) {
|
||||
is (pageNode, selectedNode, "Original element is selected");
|
||||
|
||||
let p = doc.querySelector("#badMarkup5");
|
||||
let nodiv = doc.querySelector("#badMarkup5 div");
|
||||
let div = doc.querySelector("#badMarkup5 ~ div");
|
||||
|
||||
ok (!nodiv, "The invalid markup got created as a sibling");
|
||||
is (p.textContent, "badMarkup5 ", "The <p> tag does not take in the <div> content");
|
||||
is (p.tagName, "P", "Did not change to a <div> tag");
|
||||
is (div.textContent, "with a nested div", "textContent is correct");
|
||||
is (div.tagName, "DIV", "Did not change to <p> tag");
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "#siblings",
|
||||
oldHTML: '<div id="siblings">siblings</div>',
|
||||
newHTML: '<div id="siblings-before-sibling">before sibling</div>' +
|
||||
'<div id="siblings">siblings (updated)</div>' +
|
||||
'<div id="siblings-after-sibling">after sibling</div>',
|
||||
validate: function(pageNode, selectedNode) {
|
||||
let beforeSiblingNode = doc.querySelector("#siblings-before-sibling");
|
||||
let afterSiblingNode = doc.querySelector("#siblings-after-sibling");
|
||||
|
||||
is (beforeSiblingNode, selectedNode, "Sibling has been selected");
|
||||
is (pageNode.textContent, "siblings (updated)", "New div has expected text content");
|
||||
is (beforeSiblingNode.textContent, "before sibling", "Sibling has been inserted");
|
||||
is (afterSiblingNode.textContent, "after sibling", "Sibling has been inserted");
|
||||
}
|
||||
}
|
||||
];
|
||||
content.location = "data:text/html," +
|
||||
"<!DOCTYPE html>" +
|
||||
"<head><meta charset='utf-8' /></head>" +
|
||||
"<body>" +
|
||||
[outer.oldHTML for (outer of outerHTMLs) ].join("\n") +
|
||||
"</body>" +
|
||||
"</html>";
|
||||
|
||||
function setupTest() {
|
||||
var target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
|
||||
inspector = toolbox.getCurrentPanel();
|
||||
inspector.once("inspector-updated", startTests);
|
||||
});
|
||||
}
|
||||
|
||||
function startTests() {
|
||||
inspector.markup._frame.focus();
|
||||
nextStep(0);
|
||||
}
|
||||
|
||||
function nextStep(cursor) {
|
||||
if (cursor >= outerHTMLs.length) {
|
||||
testBody();
|
||||
return;
|
||||
}
|
||||
|
||||
let currentTestData = outerHTMLs[cursor];
|
||||
let selector = currentTestData.selector;
|
||||
let oldHTML = currentTestData.oldHTML;
|
||||
let newHTML = currentTestData.newHTML;
|
||||
let rawNode = doc.querySelector(selector);
|
||||
|
||||
inspector.selection.once("new-node", () => {
|
||||
|
||||
let oldNodeFront = inspector.selection.nodeFront;
|
||||
|
||||
// markupmutation fires once the outerHTML is set, with a target
|
||||
// as the parent node and a type of "childList".
|
||||
inspector.once("markupmutation", (e, aMutations) => {
|
||||
|
||||
// Check to make the sure the correct mutation has fired, and that the
|
||||
// parent is selected (this will be reset to the child once the mutation is complete.
|
||||
let node = inspector.selection.node;
|
||||
let nodeFront = inspector.selection.nodeFront;
|
||||
let mutation = aMutations[0];
|
||||
let isFromOuterHTML = mutation.removed.some((n) => {
|
||||
return n === oldNodeFront;
|
||||
});
|
||||
|
||||
ok (isFromOuterHTML, "The node is in the 'removed' list of the mutation");
|
||||
is (mutation.type, "childList", "Mutation is a childList after updating outerHTML");
|
||||
is (mutation.target, nodeFront, "Parent node is selected immediately after setting outerHTML");
|
||||
|
||||
// Wait for node to be reselected after outerHTML has been set
|
||||
inspector.selection.once("new-node", () => {
|
||||
|
||||
// Typically selectedNode will === pageNode, but if a new element has been injected in front
|
||||
// of it, this will not be the case. If this happens.
|
||||
let selectedNode = inspector.selection.node;
|
||||
let nodeFront = inspector.selection.nodeFront;
|
||||
let pageNode = doc.querySelector(selector);
|
||||
|
||||
if (currentTestData.validate) {
|
||||
currentTestData.validate(pageNode, selectedNode);
|
||||
} else {
|
||||
is (pageNode, selectedNode, "Original node (grabbed by selector) is selected");
|
||||
is (pageNode.outerHTML, newHTML, "Outer HTML has been updated");
|
||||
}
|
||||
|
||||
nextStep(cursor + 1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
is (inspector.selection.node, rawNode, "Selection is on the correct node");
|
||||
inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront, newHTML, oldHTML);
|
||||
});
|
||||
|
||||
inspector.selection.setNode(rawNode);
|
||||
}
|
||||
|
||||
function testBody() {
|
||||
let body = doc.querySelector("body");
|
||||
let bodyHTML = '<body id="updated"><p></p></body>';
|
||||
let bodyFront = inspector.markup.walker.frontForRawNode(body);
|
||||
inspector.once("markupmutation", (e, aMutations) => {
|
||||
is (doc.querySelector("body").outerHTML, bodyHTML, "<body> HTML has been updated");
|
||||
is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
|
||||
testHead();
|
||||
});
|
||||
inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML);
|
||||
}
|
||||
|
||||
function testHead() {
|
||||
let head = doc.querySelector("head");
|
||||
let headHTML = '<head id="updated"><title>New Title</title><script>window.foo="bar";</script></head>';
|
||||
let headFront = inspector.markup.walker.frontForRawNode(head);
|
||||
inspector.once("markupmutation", (e, aMutations) => {
|
||||
is (doc.title, "New Title", "New title has been added");
|
||||
is (doc.defaultView.foo, undefined, "Script has not been executed");
|
||||
is (doc.querySelector("head").outerHTML, headHTML, "<head> HTML has been updated");
|
||||
is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
|
||||
testDocumentElement();
|
||||
});
|
||||
inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML);
|
||||
}
|
||||
|
||||
function testDocumentElement() {
|
||||
let docElement = doc.documentElement;
|
||||
let docElementHTML = '<html id="updated" foo="bar"><head><title>Updated from document element</title><script>window.foo="bar";</script></head><body><p>Hello</p></body></html>';
|
||||
let docElementFront = inspector.markup.walker.frontForRawNode(docElement);
|
||||
inspector.once("markupmutation", (e, aMutations) => {
|
||||
is (doc.title, "Updated from document element", "New title has been added");
|
||||
is (doc.defaultView.foo, undefined, "Script has not been executed");
|
||||
is (doc.documentElement.id, "updated", "<html> ID has been updated");
|
||||
is (doc.documentElement.className, "", "<html> class has been updated");
|
||||
is (doc.documentElement.getAttribute("foo"), "bar", "<html> attribute has been updated");
|
||||
is (doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
|
||||
is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
|
||||
is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
|
||||
is (doc.body.textContent, "Hello", "document.body.textContent has been updated");
|
||||
testDocumentElement2();
|
||||
});
|
||||
inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
|
||||
}
|
||||
|
||||
function testDocumentElement2() {
|
||||
let docElement = doc.documentElement;
|
||||
let docElementHTML = '<html class="updated" id="somethingelse"><head><title>Updated again from document element</title><script>window.foo="bar";</script></head><body><p>Hello again</p></body></html>';
|
||||
let docElementFront = inspector.markup.walker.frontForRawNode(docElement);
|
||||
inspector.once("markupmutation", (e, aMutations) => {
|
||||
is (doc.title, "Updated again from document element", "New title has been added");
|
||||
is (doc.defaultView.foo, undefined, "Script has not been executed");
|
||||
is (doc.documentElement.id, "somethingelse", "<html> ID has been updated");
|
||||
is (doc.documentElement.className, "updated", "<html> class has been updated");
|
||||
is (doc.documentElement.getAttribute("foo"), null, "<html> attribute has been removed");
|
||||
is (doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
|
||||
is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
|
||||
is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
|
||||
is (doc.body.textContent, "Hello again", "document.body.textContent has been updated");
|
||||
finishUp();
|
||||
});
|
||||
inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
|
||||
}
|
||||
|
||||
function finishUp() {
|
||||
doc = inspector = null;
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ const Cu = Components.utils;
|
||||
|
||||
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
let TargetFactory = devtools.TargetFactory;
|
||||
let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
|
||||
|
||||
// Clear preferences that may be set during the course of tests.
|
||||
function clearUserPrefs() {
|
||||
|
@ -61,13 +61,15 @@ const EVENTS = {
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
|
||||
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm");
|
||||
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
|
||||
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
|
||||
Cu.import("resource:///modules/devtools/VariablesView.jsm");
|
||||
Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
|
||||
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
|
||||
const Editor = require("devtools/sourceeditor/editor");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
|
||||
"resource://gre/modules/PluralForm.jsm");
|
||||
|
||||
|
@ -29,22 +29,22 @@ const CONTENT_MIME_TYPE_ABBREVIATIONS = {
|
||||
"x-javascript": "js"
|
||||
};
|
||||
const CONTENT_MIME_TYPE_MAPPINGS = {
|
||||
"/ecmascript": SourceEditor.MODES.JAVASCRIPT,
|
||||
"/javascript": SourceEditor.MODES.JAVASCRIPT,
|
||||
"/x-javascript": SourceEditor.MODES.JAVASCRIPT,
|
||||
"/html": SourceEditor.MODES.HTML,
|
||||
"/xhtml": SourceEditor.MODES.HTML,
|
||||
"/xml": SourceEditor.MODES.HTML,
|
||||
"/atom": SourceEditor.MODES.HTML,
|
||||
"/soap": SourceEditor.MODES.HTML,
|
||||
"/rdf": SourceEditor.MODES.HTML,
|
||||
"/rss": SourceEditor.MODES.HTML,
|
||||
"/css": SourceEditor.MODES.CSS
|
||||
"/ecmascript": Editor.modes.js,
|
||||
"/javascript": Editor.modes.js,
|
||||
"/x-javascript": Editor.modes.js,
|
||||
"/html": Editor.modes.html,
|
||||
"/xhtml": Editor.modes.html,
|
||||
"/xml": Editor.modes.html,
|
||||
"/atom": Editor.modes.html,
|
||||
"/soap": Editor.modes.html,
|
||||
"/rdf": Editor.modes.css,
|
||||
"/rss": Editor.modes.css,
|
||||
"/css": Editor.modes.css
|
||||
};
|
||||
const DEFAULT_EDITOR_CONFIG = {
|
||||
mode: SourceEditor.MODES.TEXT,
|
||||
mode: Editor.modes.text,
|
||||
readOnly: true,
|
||||
showLineNumbers: true
|
||||
lineNumbers: true
|
||||
};
|
||||
const GENERIC_VARIABLES_VIEW_SETTINGS = {
|
||||
lazyEmpty: true,
|
||||
@ -156,7 +156,7 @@ let NetMonitorView = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Lazily initializes and returns a promise for a SourceEditor instance.
|
||||
* Lazily initializes and returns a promise for a Editor instance.
|
||||
*
|
||||
* @param string aId
|
||||
* The id of the editor placeholder node.
|
||||
@ -175,7 +175,8 @@ let NetMonitorView = {
|
||||
|
||||
// Initialize the source editor and store the newly created instance
|
||||
// in the ether of a resolved promise's value.
|
||||
new SourceEditor().init($(aId), DEFAULT_EDITOR_CONFIG, deferred.resolve);
|
||||
let editor = new Editor(DEFAULT_EDITOR_CONFIG);
|
||||
editor.appendTo($(aId)).then(() => deferred.resolve(editor));
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
@ -1900,7 +1901,7 @@ NetworkDetailsView.prototype = {
|
||||
else {
|
||||
$("#response-content-textarea-box").hidden = false;
|
||||
NetMonitorView.editor("#response-content-textarea").then(aEditor => {
|
||||
aEditor.setMode(SourceEditor.MODES.JAVASCRIPT);
|
||||
aEditor.setMode(Editor.modes.js);
|
||||
aEditor.setText(aString);
|
||||
});
|
||||
let infoHeader = $("#response-content-info-header");
|
||||
@ -1937,7 +1938,7 @@ NetworkDetailsView.prototype = {
|
||||
else {
|
||||
$("#response-content-textarea-box").hidden = false;
|
||||
NetMonitorView.editor("#response-content-textarea").then(aEditor => {
|
||||
aEditor.setMode(SourceEditor.MODES.TEXT);
|
||||
aEditor.setMode(Editor.modes.text);
|
||||
aEditor.setText(aString);
|
||||
|
||||
// Maybe set a more appropriate mode in the Source Editor if possible,
|
||||
|
@ -9,7 +9,7 @@ function test() {
|
||||
initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, L10N, Editor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
@ -130,7 +130,7 @@ function test() {
|
||||
return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
is(aEditor.getText(), "<label value='greeting'>Hello XML!</label>",
|
||||
"The text shown in the source editor is incorrect for the xml request.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.HTML,
|
||||
is(aEditor.getMode(), Editor.modes.html,
|
||||
"The mode active in the source editor is incorrect for the xml request.");
|
||||
});
|
||||
}
|
||||
@ -140,7 +140,7 @@ function test() {
|
||||
return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
is(aEditor.getText(), "body:pre { content: 'Hello CSS!' }",
|
||||
"The text shown in the source editor is incorrect for the xml request.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.CSS,
|
||||
is(aEditor.getMode(), Editor.modes.css,
|
||||
"The mode active in the source editor is incorrect for the xml request.");
|
||||
});
|
||||
}
|
||||
@ -150,7 +150,7 @@ function test() {
|
||||
return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
is(aEditor.getText(), "function() { return 'Hello JS!'; }",
|
||||
"The text shown in the source editor is incorrect for the xml request.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.JAVASCRIPT,
|
||||
is(aEditor.getMode(), Editor.modes.js,
|
||||
"The mode active in the source editor is incorrect for the xml request.");
|
||||
});
|
||||
}
|
||||
@ -188,7 +188,7 @@ function test() {
|
||||
return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
is(aEditor.getText(), "<blink>Not Found</blink>",
|
||||
"The text shown in the source editor is incorrect for the xml request.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.HTML,
|
||||
is(aEditor.getMode(), Editor.modes.html,
|
||||
"The mode active in the source editor is incorrect for the xml request.");
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ function test() {
|
||||
initNetMonitor(CYRILLIC_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, Editor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
@ -29,7 +29,7 @@ function test() {
|
||||
NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
is(aEditor.getText().indexOf("\u044F"), 26, // я
|
||||
"The text shown in the source editor is incorrect.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.TEXT,
|
||||
is(aEditor.getMode(), Editor.modes.text,
|
||||
"The mode active in the source editor is incorrect.");
|
||||
|
||||
teardown(aMonitor).then(finish);
|
||||
|
@ -10,7 +10,7 @@ function test() {
|
||||
initNetMonitor(CYRILLIC_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, Editor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
@ -30,7 +30,7 @@ function test() {
|
||||
NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
is(aEditor.getText().indexOf("\u044F"), 302, // я
|
||||
"The text shown in the source editor is incorrect.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.HTML,
|
||||
is(aEditor.getMode(), Editor.modes.html,
|
||||
"The mode active in the source editor is incorrect.");
|
||||
|
||||
teardown(aMonitor).then(finish);
|
||||
|
@ -13,7 +13,7 @@ function test() {
|
||||
// in a variables view instance. Debug builds are slow.
|
||||
requestLongerTimeout(4);
|
||||
|
||||
let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, L10N, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
@ -9,7 +9,7 @@ function test() {
|
||||
initNetMonitor(JSON_MALFORMED_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, Editor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
@ -59,7 +59,7 @@ function test() {
|
||||
NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
is(aEditor.getText(), "{ \"greeting\": \"Hello malformed JSON!\" },",
|
||||
"The text shown in the source editor is incorrect.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.JAVASCRIPT,
|
||||
is(aEditor.getMode(), Editor.modes.js,
|
||||
"The mode active in the source editor is incorrect.");
|
||||
|
||||
teardown(aMonitor).then(finish);
|
||||
|
@ -9,7 +9,7 @@ function test() {
|
||||
initNetMonitor(JSON_CUSTOM_MIME_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, L10N, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
@ -9,7 +9,7 @@ function test() {
|
||||
initNetMonitor(JSONP_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, L10N, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
@ -13,7 +13,7 @@ function test() {
|
||||
// is going to be requested and displayed in the source editor.
|
||||
requestLongerTimeout(2);
|
||||
|
||||
let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, Editor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
@ -29,7 +29,7 @@ function test() {
|
||||
NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
ok(aEditor.getText().match(/^<p>/),
|
||||
"The text shown in the source editor is incorrect.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.TEXT,
|
||||
is(aEditor.getMode(), Editor.modes.text,
|
||||
"The mode active in the source editor is incorrect.");
|
||||
|
||||
teardown(aMonitor).then(finish);
|
||||
|
@ -9,7 +9,7 @@ function test() {
|
||||
initNetMonitor(POST_DATA_URL).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, L10N, Editor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
@ -141,7 +141,7 @@ function test() {
|
||||
"The text shown in the source editor is incorrect (3.2).");
|
||||
ok(aEditor.getText().contains("Extra data"),
|
||||
"The text shown in the source editor is incorrect (4.2).");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.TEXT,
|
||||
is(aEditor.getMode(), Editor.modes.text,
|
||||
"The mode active in the source editor is incorrect.");
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ function test() {
|
||||
initNetMonitor(SIMPLE_SJS).then(([aTab, aDebuggee, aMonitor]) => {
|
||||
info("Starting test... ");
|
||||
|
||||
let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { document, L10N, Editor, NetMonitorView } = aMonitor.panelWin;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
@ -194,7 +194,7 @@ function test() {
|
||||
return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
|
||||
is(aEditor.getText(), "Hello world!",
|
||||
"The text shown in the source editor is incorrect.");
|
||||
is(aEditor.getMode(), SourceEditor.MODES.TEXT,
|
||||
is(aEditor.getMode(), Editor.modes.text,
|
||||
"The mode active in the source editor is incorrect.");
|
||||
});
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ in the LICENSE file:
|
||||
* dialog/dialog.js
|
||||
* javascript.js
|
||||
* matchbrackets.js
|
||||
* closebrackets.js
|
||||
* search/match-highlighter.js
|
||||
* search/search.js
|
||||
* search/searchcursor.js
|
||||
@ -57,4 +58,4 @@ in the LICENSE file:
|
||||
[2] browser/devtools/sourceeditor/codemirror
|
||||
[3] browser/devtools/sourceeditor/test/browser_codemirror.js
|
||||
[4] browser/devtools/jar.mn
|
||||
[5] browser/devtools/sourceeditor/editor.js
|
||||
[5] browser/devtools/sourceeditor/editor.js
|
||||
|
82
browser/devtools/sourceeditor/codemirror/closebrackets.js
Normal file
82
browser/devtools/sourceeditor/codemirror/closebrackets.js
Normal file
@ -0,0 +1,82 @@
|
||||
(function() {
|
||||
var DEFAULT_BRACKETS = "()[]{}''\"\"";
|
||||
var DEFAULT_EXPLODE_ON_ENTER = "[]{}";
|
||||
var SPACE_CHAR_REGEX = /\s/;
|
||||
|
||||
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
|
||||
if (old != CodeMirror.Init && old)
|
||||
cm.removeKeyMap("autoCloseBrackets");
|
||||
if (!val) return;
|
||||
var pairs = DEFAULT_BRACKETS, explode = DEFAULT_EXPLODE_ON_ENTER;
|
||||
if (typeof val == "string") pairs = val;
|
||||
else if (typeof val == "object") {
|
||||
if (val.pairs != null) pairs = val.pairs;
|
||||
if (val.explode != null) explode = val.explode;
|
||||
}
|
||||
var map = buildKeymap(pairs);
|
||||
if (explode) map.Enter = buildExplodeHandler(explode);
|
||||
cm.addKeyMap(map);
|
||||
});
|
||||
|
||||
function charsAround(cm, pos) {
|
||||
var str = cm.getRange(CodeMirror.Pos(pos.line, pos.ch - 1),
|
||||
CodeMirror.Pos(pos.line, pos.ch + 1));
|
||||
return str.length == 2 ? str : null;
|
||||
}
|
||||
|
||||
function buildKeymap(pairs) {
|
||||
var map = {
|
||||
name : "autoCloseBrackets",
|
||||
Backspace: function(cm) {
|
||||
if (cm.somethingSelected()) return CodeMirror.Pass;
|
||||
var cur = cm.getCursor(), around = charsAround(cm, cur);
|
||||
if (around && pairs.indexOf(around) % 2 == 0)
|
||||
cm.replaceRange("", CodeMirror.Pos(cur.line, cur.ch - 1), CodeMirror.Pos(cur.line, cur.ch + 1));
|
||||
else
|
||||
return CodeMirror.Pass;
|
||||
}
|
||||
};
|
||||
var closingBrackets = "";
|
||||
for (var i = 0; i < pairs.length; i += 2) (function(left, right) {
|
||||
if (left != right) closingBrackets += right;
|
||||
function surround(cm) {
|
||||
var selection = cm.getSelection();
|
||||
cm.replaceSelection(left + selection + right);
|
||||
}
|
||||
function maybeOverwrite(cm) {
|
||||
var cur = cm.getCursor(), ahead = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1));
|
||||
if (ahead != right || cm.somethingSelected()) return CodeMirror.Pass;
|
||||
else cm.execCommand("goCharRight");
|
||||
}
|
||||
map["'" + left + "'"] = function(cm) {
|
||||
if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment")
|
||||
return CodeMirror.Pass;
|
||||
if (cm.somethingSelected()) return surround(cm);
|
||||
if (left == right && maybeOverwrite(cm) != CodeMirror.Pass) return;
|
||||
var cur = cm.getCursor(), ahead = CodeMirror.Pos(cur.line, cur.ch + 1);
|
||||
var line = cm.getLine(cur.line), nextChar = line.charAt(cur.ch), curChar = cur.ch > 0 ? line.charAt(cur.ch - 1) : "";
|
||||
if (left == right && CodeMirror.isWordChar(curChar))
|
||||
return CodeMirror.Pass;
|
||||
if (line.length == cur.ch || closingBrackets.indexOf(nextChar) >= 0 || SPACE_CHAR_REGEX.test(nextChar))
|
||||
cm.replaceSelection(left + right, {head: ahead, anchor: ahead});
|
||||
else
|
||||
return CodeMirror.Pass;
|
||||
};
|
||||
if (left != right) map["'" + right + "'"] = maybeOverwrite;
|
||||
})(pairs.charAt(i), pairs.charAt(i + 1));
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildExplodeHandler(pairs) {
|
||||
return function(cm) {
|
||||
var cur = cm.getCursor(), around = charsAround(cm, cur);
|
||||
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||
cm.operation(function() {
|
||||
var newPos = CodeMirror.Pos(cur.line + 1, 0);
|
||||
cm.replaceSelection("\n\n", {anchor: newPos, head: newPos}, "+input");
|
||||
cm.indentLine(cur.line + 1, null, true);
|
||||
cm.indentLine(cur.line + 2, null, true);
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
@ -23,8 +23,4 @@
|
||||
.breakpoint.debugLocation {
|
||||
background-image: url("chrome://browser/skin/devtools/orion-debug-location.png"),
|
||||
url("chrome://browser/skin/devtools/orion-breakpoint.png");
|
||||
}
|
||||
|
||||
.CodeMirror-activeline-background {
|
||||
background: #e8f2ff;
|
||||
}
|
@ -62,25 +62,27 @@ function getSearchCursor(cm, query, pos) {
|
||||
* Otherwise, creates a new search and selects the first
|
||||
* result.
|
||||
*/
|
||||
function doSearch(cm, rev, query) {
|
||||
function doSearch(ctx, rev, query) {
|
||||
let { cm } = ctx;
|
||||
let state = getSearchState(cm);
|
||||
|
||||
if (state.query)
|
||||
return searchNext(cm, rev);
|
||||
return searchNext(ctx, rev);
|
||||
|
||||
cm.operation(function () {
|
||||
if (state.query) return;
|
||||
|
||||
state.query = query;
|
||||
state.posFrom = state.posTo = { line: 0, ch: 0 };
|
||||
searchNext(cm, rev);
|
||||
searchNext(ctx, rev);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the next result of a saved search.
|
||||
*/
|
||||
function searchNext(cm, rev) {
|
||||
function searchNext(ctx, rev) {
|
||||
let { cm, ed } = ctx;
|
||||
cm.operation(function () {
|
||||
let state = getSearchState(cm)
|
||||
let cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
|
||||
@ -92,6 +94,7 @@ function searchNext(cm, rev) {
|
||||
return;
|
||||
}
|
||||
|
||||
ed.alignLine(cursor.from().line, "center");
|
||||
cm.setSelection(cursor.from(), cursor.to());
|
||||
state.posFrom = cursor.from();
|
||||
state.posTo = cursor.to();
|
||||
@ -236,25 +239,22 @@ function clearDebugLocation(ctx) {
|
||||
* Starts a new search.
|
||||
*/
|
||||
function find(ctx, query) {
|
||||
let { cm } = ctx;
|
||||
clearSearch(cm);
|
||||
doSearch(cm, false, query);
|
||||
clearSearch(ctx.cm);
|
||||
doSearch(ctx, false, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next item based on the currently saved search.
|
||||
*/
|
||||
function findNext(ctx, query) {
|
||||
let { cm } = ctx;
|
||||
doSearch(cm, false, query);
|
||||
doSearch(ctx, false, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the previous item based on the currently saved search.
|
||||
*/
|
||||
function findPrev(ctx, query) {
|
||||
let { cm } = ctx;
|
||||
doSearch(cm, true, query);
|
||||
doSearch(ctx, true, query);
|
||||
}
|
||||
|
||||
|
||||
@ -264,4 +264,4 @@ function findPrev(ctx, query) {
|
||||
initialize, hasBreakpoint, addBreakpoint, removeBreakpoint,
|
||||
getBreakpoints, setDebugLocation, getDebugLocation,
|
||||
clearDebugLocation, find, findNext, findPrev
|
||||
].forEach(function (func) { module.exports[func.name] = func; });
|
||||
].forEach(function (func) { module.exports[func.name] = func; });
|
||||
|
@ -12,6 +12,10 @@ const EXPAND_TAB = "devtools.editor.expandtab";
|
||||
const L10N_BUNDLE = "chrome://browser/locale/devtools/sourceeditor.properties";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
// Maximum allowed margin (in number of lines) from top or bottom of the editor
|
||||
// while shifting to a line which was initially out of view.
|
||||
const MAX_VERTICAL_OFFSET = 3;
|
||||
|
||||
const promise = require("sdk/core/promise");
|
||||
const events = require("devtools/shared/event-emitter");
|
||||
|
||||
@ -23,17 +27,20 @@ const L10N = Services.strings.createBundle(L10N_BUNDLE);
|
||||
// order to initialize a CodeMirror instance.
|
||||
|
||||
const CM_STYLES = [
|
||||
"chrome://browser/skin/devtools/common.css",
|
||||
"chrome://browser/content/devtools/codemirror/codemirror.css",
|
||||
"chrome://browser/content/devtools/codemirror/dialog.css",
|
||||
"chrome://browser/content/devtools/codemirror/mozilla.css"
|
||||
];
|
||||
|
||||
const CM_SCRIPTS = [
|
||||
"chrome://browser/content/devtools/theme-switching.js",
|
||||
"chrome://browser/content/devtools/codemirror/codemirror.js",
|
||||
"chrome://browser/content/devtools/codemirror/dialog.js",
|
||||
"chrome://browser/content/devtools/codemirror/searchcursor.js",
|
||||
"chrome://browser/content/devtools/codemirror/search.js",
|
||||
"chrome://browser/content/devtools/codemirror/matchbrackets.js",
|
||||
"chrome://browser/content/devtools/codemirror/closebrackets.js",
|
||||
"chrome://browser/content/devtools/codemirror/comment.js",
|
||||
"chrome://browser/content/devtools/codemirror/javascript.js",
|
||||
"chrome://browser/content/devtools/codemirror/xml.js",
|
||||
@ -53,13 +60,12 @@ const CM_IFRAME =
|
||||
" </style>" +
|
||||
[ " <link rel='stylesheet' href='" + style + "'>" for (style of CM_STYLES) ].join("\n") +
|
||||
" </head>" +
|
||||
" <body></body>" +
|
||||
" <body class='theme-body devtools-monospace'></body>" +
|
||||
"</html>";
|
||||
|
||||
const CM_MAPPING = [
|
||||
"focus",
|
||||
"hasFocus",
|
||||
"setCursor",
|
||||
"getCursor",
|
||||
"somethingSelected",
|
||||
"setSelection",
|
||||
@ -70,7 +76,8 @@ const CM_MAPPING = [
|
||||
"clearHistory",
|
||||
"openDialog",
|
||||
"cursorCoords",
|
||||
"lineCount"
|
||||
"lineCount",
|
||||
"refresh"
|
||||
];
|
||||
|
||||
const CM_JUMP_DIALOG = [
|
||||
@ -78,6 +85,8 @@ const CM_JUMP_DIALOG = [
|
||||
+ " <input type=text style='width: 10em'/>"
|
||||
];
|
||||
|
||||
const { cssProperties, cssValues, cssColors } = getCSSKeywords();
|
||||
|
||||
const editors = new WeakMap();
|
||||
|
||||
Editor.modes = {
|
||||
@ -126,7 +135,8 @@ function Editor(config) {
|
||||
matchBrackets: true,
|
||||
extraKeys: {},
|
||||
indentWithTabs: useTabs,
|
||||
styleActiveLine: true
|
||||
styleActiveLine: true,
|
||||
theme: "mozilla"
|
||||
};
|
||||
|
||||
// Overwrite default config with user-provided, if needed.
|
||||
@ -176,7 +186,7 @@ Editor.prototype = {
|
||||
let def = promise.defer();
|
||||
let cm = editors.get(this);
|
||||
let doc = el.ownerDocument;
|
||||
let env = doc.createElementNS(XUL_NS, "iframe");
|
||||
let env = doc.createElement("iframe");
|
||||
env.flex = 1;
|
||||
|
||||
if (cm)
|
||||
@ -192,14 +202,27 @@ Editor.prototype = {
|
||||
CM_SCRIPTS.forEach((url) =>
|
||||
Services.scriptloader.loadSubScript(url, win, "utf8"));
|
||||
|
||||
// Create a CodeMirror instance add support for context menus and
|
||||
// Replace the propertyKeywords, colorKeywords and valueKeywords
|
||||
// properties of the CSS MIME type with the values provided by Gecko.
|
||||
let cssSpec = win.CodeMirror.resolveMode("text/css");
|
||||
cssSpec.propertyKeywords = cssProperties;
|
||||
cssSpec.colorKeywords = cssColors;
|
||||
cssSpec.valueKeywords = cssValues;
|
||||
win.CodeMirror.defineMIME("text/css", cssSpec);
|
||||
|
||||
let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
|
||||
scssSpec.propertyKeywords = cssProperties;
|
||||
scssSpec.colorKeywords = cssColors;
|
||||
scssSpec.valueKeywords = cssValues;
|
||||
win.CodeMirror.defineMIME("text/x-scss", scssSpec);
|
||||
|
||||
// Create a CodeMirror instance add support for context menus,
|
||||
// overwrite the default controller (otherwise items in the top and
|
||||
// context menus won't work).
|
||||
|
||||
cm = win.CodeMirror(win.document.body, this.config);
|
||||
cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault();
|
||||
this.emit("contextMenu");
|
||||
this.showContextMenu(doc, ev.screenX, ev.screenY);
|
||||
}, false);
|
||||
|
||||
@ -434,6 +457,67 @@ Editor.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the first visible line number in the editor.
|
||||
*/
|
||||
getFirstVisibleLine: function () {
|
||||
let cm = editors.get(this);
|
||||
return cm.lineAtHeight(0, "local");
|
||||
},
|
||||
|
||||
/**
|
||||
* Scrolls the view such that the given line number is the first visible line.
|
||||
*/
|
||||
setFirstVisibleLine: function (line) {
|
||||
let cm = editors.get(this);
|
||||
let { top } = cm.charCoords({line: line, ch: 0}, "local");
|
||||
cm.scrollTo(0, top);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the cursor to the specified {line, ch} position with an additional
|
||||
* option to align the line at the "top", "center" or "bottom" of the editor
|
||||
* with "top" being default value.
|
||||
*/
|
||||
setCursor: function ({line, ch}, align) {
|
||||
let cm = editors.get(this);
|
||||
this.alignLine(line, align);
|
||||
cm.setCursor({line: line, ch: ch});
|
||||
},
|
||||
|
||||
/**
|
||||
* Aligns the provided line to either "top", "center" or "bottom" of the
|
||||
* editor view with a maximum margin of MAX_VERTICAL_OFFSET lines from top or
|
||||
* bottom.
|
||||
*/
|
||||
alignLine: function(line, align) {
|
||||
let cm = editors.get(this);
|
||||
let from = cm.lineAtHeight(0, "page");
|
||||
let to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page");
|
||||
let linesVisible = to - from;
|
||||
let halfVisible = Math.round(linesVisible/2);
|
||||
|
||||
// If the target line is in view, skip the vertical alignment part.
|
||||
if (line <= to && line >= from) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setting the offset so that the line always falls in the upper half
|
||||
// of visible lines (lower half for bottom aligned).
|
||||
// MAX_VERTICAL_OFFSET is the maximum allowed value.
|
||||
let offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
|
||||
|
||||
let topLine = {
|
||||
"center": Math.max(line - halfVisible, 0),
|
||||
"bottom": Math.max(line - linesVisible + offset, 0),
|
||||
"top": Math.max(line - offset, 0)
|
||||
}[align || "top"] || offset;
|
||||
|
||||
// Bringing down the topLine to total lines in the editor if exceeding.
|
||||
topLine = Math.min(topLine, this.lineCount());
|
||||
this.setFirstVisibleLine(topLine);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.container = null;
|
||||
this.config = null;
|
||||
@ -452,6 +536,44 @@ CM_MAPPING.forEach(function (name) {
|
||||
};
|
||||
});
|
||||
|
||||
// Since Gecko already provide complete and up to date list of CSS property
|
||||
// names, values and color names, we compute them so that they can replace
|
||||
// the ones used in CodeMirror while initiating an editor object. This is done
|
||||
// here instead of the file codemirror/css.js so as to leave that file untouched
|
||||
// and easily upgradable.
|
||||
function getCSSKeywords() {
|
||||
function keySet(array) {
|
||||
var keys = {};
|
||||
for (var i = 0; i < array.length; ++i) {
|
||||
keys[array[i]] = true;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
|
||||
.getService(Ci.inIDOMUtils);
|
||||
let cssProperties = domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES);
|
||||
let cssColors = {};
|
||||
let cssValues = {};
|
||||
cssProperties.forEach(property => {
|
||||
if (property.contains("color")) {
|
||||
domUtils.getCSSValuesForProperty(property).forEach(value => {
|
||||
cssColors[value] = true;
|
||||
});
|
||||
}
|
||||
else {
|
||||
domUtils.getCSSValuesForProperty(property).forEach(value => {
|
||||
cssValues[value] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
cssProperties: keySet(cssProperties),
|
||||
cssValues: cssValues,
|
||||
cssColors: cssColors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a controller object that can be used for
|
||||
* editor-specific commands such as find, jump to line,
|
||||
@ -530,4 +652,4 @@ function controller(ed, view) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Editor;
|
||||
module.exports = Editor;
|
||||
|
@ -73,7 +73,7 @@ StyleEditorUI.prototype = {
|
||||
return true;
|
||||
}
|
||||
return this.editors.some((editor) => {
|
||||
return editor.sourceEditor && editor.sourceEditor.dirty;
|
||||
return editor.sourceEditor && !editor.sourceEditor.isClean();
|
||||
});
|
||||
},
|
||||
|
||||
@ -151,8 +151,8 @@ StyleEditorUI.prototype = {
|
||||
// remember selected sheet and line number for next load
|
||||
if (this.selectedEditor && this.selectedEditor.sourceEditor) {
|
||||
let href = this.selectedEditor.styleSheet.href;
|
||||
let {line, col} = this.selectedEditor.sourceEditor.getCaretPosition();
|
||||
this.selectStyleSheet(href, line, col);
|
||||
let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
|
||||
this.selectStyleSheet(href, line, ch);
|
||||
}
|
||||
|
||||
this._clearStyleSheetEditors();
|
||||
@ -365,7 +365,7 @@ StyleEditorUI.prototype = {
|
||||
col = col || 0;
|
||||
|
||||
editor.getSourceEditor().then(() => {
|
||||
editor.sourceEditor.setCaretPosition(line, col);
|
||||
editor.sourceEditor.setCursor({line: line, ch: col});
|
||||
});
|
||||
|
||||
this._view.activeSummary = editor.summary;
|
||||
@ -387,6 +387,11 @@ StyleEditorUI.prototype = {
|
||||
selectStyleSheet: function(href, line, col)
|
||||
{
|
||||
let alreadyCalled = !!this._styleSheetToSelect;
|
||||
let originalHref;
|
||||
|
||||
if (alreadyCalled) {
|
||||
originalHref = this._styleSheetToSelect.href;
|
||||
}
|
||||
|
||||
this._styleSheetToSelect = {
|
||||
href: href,
|
||||
@ -395,6 +400,14 @@ StyleEditorUI.prototype = {
|
||||
};
|
||||
|
||||
if (alreadyCalled) {
|
||||
// Just switch to the correct line and columns if the editor is already
|
||||
// selected for the requested stylesheet.
|
||||
for each (let editor in this.editors) {
|
||||
if (editor.styleSheet.href == originalHref) {
|
||||
editor.sourceEditor.setCursor({line: line, ch: col})
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -11,21 +11,26 @@ const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
|
||||
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
|
||||
const Editor = require("devtools/sourceeditor/editor");
|
||||
const promise = require("sdk/core/promise");
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
|
||||
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm");
|
||||
Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
|
||||
|
||||
|
||||
const SAVE_ERROR = "error-save";
|
||||
|
||||
// max update frequency in ms (avoid potential typing lag and/or flicker)
|
||||
// @see StyleEditor.updateStylesheet
|
||||
const UPDATE_STYLESHEET_THROTTLE_DELAY = 500;
|
||||
|
||||
function ctrl(k) {
|
||||
return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
|
||||
}
|
||||
|
||||
/**
|
||||
* StyleSheetEditor controls the editor linked to a particular StyleSheet
|
||||
* object.
|
||||
@ -58,7 +63,10 @@ function StyleSheetEditor(styleSheet, win, file, isNew) {
|
||||
|
||||
this._state = { // state to use when inputElement attaches
|
||||
text: "",
|
||||
selection: {start: 0, end: 0},
|
||||
selection: {
|
||||
start: {line: 0, ch: 0},
|
||||
end: {line: 0, ch: 0}
|
||||
},
|
||||
readOnly: false,
|
||||
topIndex: 0, // the first visible line
|
||||
};
|
||||
@ -92,7 +100,7 @@ StyleSheetEditor.prototype = {
|
||||
* Whether there are unsaved changes in the editor
|
||||
*/
|
||||
get unsaved() {
|
||||
return this._sourceEditor && this._sourceEditor.dirty;
|
||||
return this._sourceEditor && !this._sourceEditor.isClean();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -200,21 +208,20 @@ StyleSheetEditor.prototype = {
|
||||
load: function(inputElement) {
|
||||
this._inputElement = inputElement;
|
||||
|
||||
let sourceEditor = new SourceEditor();
|
||||
let config = {
|
||||
initialText: this._state.text,
|
||||
showLineNumbers: true,
|
||||
mode: SourceEditor.MODES.CSS,
|
||||
value: this._state.text,
|
||||
lineNumbers: true,
|
||||
mode: Editor.modes.css,
|
||||
readOnly: this._state.readOnly,
|
||||
keys: this._getKeyBindings()
|
||||
autoCloseBrackets: "{}()[]",
|
||||
extraKeys: this._getKeyBindings()
|
||||
};
|
||||
let sourceEditor = new Editor(config);
|
||||
|
||||
sourceEditor.init(inputElement, config, function onSourceEditorReady() {
|
||||
setupBracketCompletion(sourceEditor);
|
||||
sourceEditor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
|
||||
function onTextChanged(event) {
|
||||
sourceEditor.appendTo(inputElement).then(() => {
|
||||
sourceEditor.on("change", () => {
|
||||
this.updateStyleSheet();
|
||||
}.bind(this));
|
||||
});
|
||||
|
||||
this._sourceEditor = sourceEditor;
|
||||
|
||||
@ -223,15 +230,14 @@ StyleSheetEditor.prototype = {
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
sourceEditor.setTopIndex(this._state.topIndex);
|
||||
sourceEditor.setFirstVisibleLine(this._state.topIndex);
|
||||
sourceEditor.setSelection(this._state.selection.start,
|
||||
this._state.selection.end);
|
||||
|
||||
this.emit("source-editor-load");
|
||||
}.bind(this));
|
||||
});
|
||||
|
||||
sourceEditor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
|
||||
this._onPropertyChange);
|
||||
sourceEditor.on("change", this._onPropertyChange);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -246,7 +252,7 @@ StyleSheetEditor.prototype = {
|
||||
if (this.sourceEditor) {
|
||||
return promise.resolve(this);
|
||||
}
|
||||
this.on("source-editor-load", (event) => {
|
||||
this.on("source-editor-load", () => {
|
||||
deferred.resolve(this);
|
||||
});
|
||||
return deferred.promise;
|
||||
@ -268,7 +274,7 @@ StyleSheetEditor.prototype = {
|
||||
*/
|
||||
onShow: function() {
|
||||
if (this._sourceEditor) {
|
||||
this._sourceEditor.setTopIndex(this._state.topIndex);
|
||||
this._sourceEditor.setFirstVisibleLine(this._state.topIndex);
|
||||
}
|
||||
this.focus();
|
||||
},
|
||||
@ -370,7 +376,7 @@ StyleSheetEditor.prototype = {
|
||||
if (callback) {
|
||||
callback(returnFile);
|
||||
}
|
||||
this.sourceEditor.dirty = false;
|
||||
this.sourceEditor.markClean();
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
@ -384,28 +390,15 @@ StyleSheetEditor.prototype = {
|
||||
* @return {array} key binding objects for the source editor
|
||||
*/
|
||||
_getKeyBindings: function() {
|
||||
let bindings = [];
|
||||
let bindings = {};
|
||||
|
||||
bindings.push({
|
||||
action: "StyleEditor.save",
|
||||
code: _("saveStyleSheet.commandkey"),
|
||||
accel: true,
|
||||
callback: function save() {
|
||||
this.saveToFile(this.savedFile);
|
||||
return true;
|
||||
}.bind(this)
|
||||
});
|
||||
bindings[ctrl(_("saveStyleSheet.commandkey"))] = () => {
|
||||
this.saveToFile(this.savedFile);
|
||||
};
|
||||
|
||||
bindings.push({
|
||||
action: "StyleEditor.saveAs",
|
||||
code: _("saveStyleSheet.commandkey"),
|
||||
accel: true,
|
||||
shift: true,
|
||||
callback: function saveAs() {
|
||||
this.saveToFile();
|
||||
return true;
|
||||
}.bind(this)
|
||||
});
|
||||
bindings["Shift-" + ctrl(_("saveStyleSheet.commandkey"))] = () => {
|
||||
this.saveToFile();
|
||||
};
|
||||
|
||||
return bindings;
|
||||
},
|
||||
@ -426,18 +419,6 @@ const TAB_CHARS = "\t";
|
||||
const OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
|
||||
const LINE_SEPARATOR = OS === "WINNT" ? "\r\n" : "\n";
|
||||
|
||||
/**
|
||||
* Return string that repeats text for aCount times.
|
||||
*
|
||||
* @param string text
|
||||
* @param number aCount
|
||||
* @return string
|
||||
*/
|
||||
function repeat(text, aCount)
|
||||
{
|
||||
return (new Array(aCount + 1)).join(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prettify minified CSS text.
|
||||
* This prettifies CSS code where there is no indentation in usual places while
|
||||
@ -469,7 +450,7 @@ function prettifyCSS(text)
|
||||
parts.push(indent + text.substring(partStart, i));
|
||||
partStart = i;
|
||||
}
|
||||
indent = repeat(TAB_CHARS, --indentLevel);
|
||||
indent = TAB_CHARS.repeat(--indentLevel);
|
||||
/* fallthrough */
|
||||
case ";":
|
||||
case "{":
|
||||
@ -493,58 +474,9 @@ function prettifyCSS(text)
|
||||
}
|
||||
|
||||
if (c == "{") {
|
||||
indent = repeat(TAB_CHARS, ++indentLevel);
|
||||
indent = TAB_CHARS.repeat(++indentLevel);
|
||||
}
|
||||
}
|
||||
return parts.join(LINE_SEPARATOR);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set up bracket completion on a given SourceEditor.
|
||||
* This automatically closes the following CSS brackets: "{", "(", "["
|
||||
*
|
||||
* @param SourceEditor sourceEditor
|
||||
*/
|
||||
function setupBracketCompletion(sourceEditor)
|
||||
{
|
||||
let editorElement = sourceEditor.editorElement;
|
||||
let pairs = {
|
||||
123: { // {
|
||||
closeString: "}",
|
||||
closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
|
||||
},
|
||||
40: { // (
|
||||
closeString: ")",
|
||||
closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_0
|
||||
},
|
||||
91: { // [
|
||||
closeString: "]",
|
||||
closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
|
||||
},
|
||||
};
|
||||
|
||||
editorElement.addEventListener("keypress", function onKeyPress(event) {
|
||||
let pair = pairs[event.charCode];
|
||||
if (!pair || event.ctrlKey || event.metaKey ||
|
||||
event.accelKey || event.altKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We detected an open bracket, sending closing character
|
||||
let keyCode = pair.closeKeyCode;
|
||||
let charCode = pair.closeString.charCodeAt(0);
|
||||
let modifiers = 0;
|
||||
let utils = editorElement.ownerDocument.defaultView.
|
||||
QueryInterface(Ci.nsIInterfaceRequestor).
|
||||
getInterface(Ci.nsIDOMWindowUtils);
|
||||
|
||||
if (utils.sendKeyEvent("keydown", keyCode, 0, modifiers)) {
|
||||
utils.sendKeyEvent("keypress", 0, charCode, modifiers);
|
||||
}
|
||||
utils.sendKeyEvent("keyup", keyCode, 0, modifiers);
|
||||
// and rewind caret
|
||||
sourceEditor.setCaretOffset(sourceEditor.getCaretOffset() - 1);
|
||||
}, false);
|
||||
}
|
||||
|
||||
|
@ -96,21 +96,10 @@ function testEditor(aEditor) {
|
||||
is(computedStyle.backgroundColor, "rgb(255, 255, 255)",
|
||||
"content's background color is initially white");
|
||||
|
||||
EventUtils.synthesizeKey("[", {accelKey: true}, gPanelWindow);
|
||||
is(aEditor.sourceEditor.getText(), "",
|
||||
"Nothing happened as it is a known shortcut in source editor");
|
||||
|
||||
EventUtils.synthesizeKey("]", {accelKey: true}, gPanelWindow);
|
||||
is(aEditor.sourceEditor.getText(), "",
|
||||
"Nothing happened as it is a known shortcut in source editor");
|
||||
|
||||
for each (let c in TESTCASE_CSS_SOURCE) {
|
||||
EventUtils.synthesizeKey(c, {}, gPanelWindow);
|
||||
}
|
||||
|
||||
is(aEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
|
||||
"rule bracket has been auto-closed");
|
||||
|
||||
ok(aEditor.unsaved,
|
||||
"new editor has unsaved flag");
|
||||
|
||||
@ -122,6 +111,9 @@ function testEditor(aEditor) {
|
||||
function onTransitionEnd() {
|
||||
content.removeEventListener("transitionend", onTransitionEnd, false);
|
||||
|
||||
is(gNewEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
|
||||
"rule bracket has been auto-closed");
|
||||
|
||||
let computedStyle = content.getComputedStyle(content.document.body, null);
|
||||
is(computedStyle.backgroundColor, "rgb(255, 0, 0)",
|
||||
"content's background color has been updated to red");
|
||||
|
@ -54,9 +54,9 @@ function testRemembered()
|
||||
{
|
||||
is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
|
||||
|
||||
let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
|
||||
let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
|
||||
is(line, LINE_NO, "correct line selected");
|
||||
is(col, COL_NO, "correct column selected");
|
||||
is(ch, COL_NO, "correct column selected");
|
||||
|
||||
testNewPage();
|
||||
}
|
||||
@ -80,9 +80,9 @@ function testNotRemembered()
|
||||
{
|
||||
is(gUI.selectedEditor, gUI.editors[0], "first editor is selected");
|
||||
|
||||
let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
|
||||
let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
|
||||
is(line, 0, "first line is selected");
|
||||
is(col, 0, "first column is selected");
|
||||
is(ch, 0, "first column is selected");
|
||||
|
||||
gUI = null;
|
||||
finish();
|
||||
@ -96,4 +96,4 @@ function reloadPage()
|
||||
function navigatePage()
|
||||
{
|
||||
gContentWin.location = NEW_URI;
|
||||
}
|
||||
}
|
||||
|
@ -29,13 +29,14 @@ function runTests(aUI)
|
||||
is(aUI.editors.length, 2,
|
||||
"there is 2 stylesheets initially");
|
||||
|
||||
aUI.editors[0].getSourceEditor().then(function onEditorAttached(aEditor) {
|
||||
aUI.editors[0].getSourceEditor().then(aEditor => {
|
||||
executeSoon(function () {
|
||||
waitForFocus(function () {
|
||||
// queue a resize to inverse aspect ratio
|
||||
// this will trigger a detach and reattach (to workaround bug 254144)
|
||||
let originalSourceEditor = aEditor.sourceEditor;
|
||||
aEditor.sourceEditor.setCaretOffset(4); // to check the caret is preserved
|
||||
let editor = aEditor.sourceEditor;
|
||||
editor.setCursor(editor.getPosition(4)); // to check the caret is preserved
|
||||
|
||||
gOriginalWidth = gPanelWindow.outerWidth;
|
||||
gOriginalHeight = gPanelWindow.outerHeight;
|
||||
@ -44,7 +45,8 @@ function runTests(aUI)
|
||||
executeSoon(function () {
|
||||
is(aEditor.sourceEditor, originalSourceEditor,
|
||||
"the editor still references the same SourceEditor instance");
|
||||
is(aEditor.sourceEditor.getCaretOffset(), 4,
|
||||
let editor = aEditor.sourceEditor;
|
||||
is(editor.getOffset(editor.getCursor()), 4,
|
||||
"the caret position has been preserved");
|
||||
|
||||
// queue a resize to original aspect ratio
|
||||
|
@ -234,4 +234,5 @@ support-files =
|
||||
[browser_webconsole_property_provider.js]
|
||||
[browser_webconsole_scratchpad_panel_link.js]
|
||||
[browser_webconsole_view_source.js]
|
||||
[browser_webconsole_reflow.js]
|
||||
[browser_webconsole_log_file_filter.js]
|
||||
|
@ -62,7 +62,7 @@ function consoleOpened(hud)
|
||||
let text = output.textContent;
|
||||
chromeConsole = text.indexOf("bug587757a");
|
||||
contentConsole = text.indexOf("bug587757b");
|
||||
execValue = text.indexOf("webconsole.xul");
|
||||
execValue = text.indexOf("browser.xul");
|
||||
exception = text.indexOf("foobarExceptionBug587757");
|
||||
xhrRequest = text.indexOf("test-console.html");
|
||||
}
|
||||
|
@ -27,7 +27,8 @@ function test()
|
||||
hud.jsterm.execute("Cu = Components.utils;" +
|
||||
"Cu.import('resource://gre/modules/Services.jsm');" +
|
||||
"chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');" +
|
||||
"foobarzTezt = chromeWindow.content.document", onAddVariable);
|
||||
"foobarzTezt = chromeWindow.content.document;" +
|
||||
"delete chromeWindow", onAddVariable);
|
||||
}
|
||||
|
||||
function onAddVariable()
|
||||
|
@ -9,7 +9,8 @@ let prefs = {
|
||||
],
|
||||
"css": [
|
||||
"csserror",
|
||||
"cssparser"
|
||||
"cssparser",
|
||||
"csslog"
|
||||
],
|
||||
"js": [
|
||||
"exception",
|
||||
|
@ -131,7 +131,7 @@ function performLineCheck(aEditor, aLine, aCallback)
|
||||
{
|
||||
function checkForCorrectState()
|
||||
{
|
||||
is(aEditor.sourceEditor.getCaretPosition().line, aLine,
|
||||
is(aEditor.sourceEditor.getCursor().line, aLine,
|
||||
"correct line is selected");
|
||||
is(StyleEditorUI.selectedStyleSheetIndex, aEditor.styleSheet.styleSheetIndex,
|
||||
"correct stylesheet is selected in the editor");
|
||||
|
@ -0,0 +1,35 @@
|
||||
/* vim:set ts=2 sw=2 sts=2 et: */
|
||||
/* 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/. */
|
||||
|
||||
function test()
|
||||
{
|
||||
addTab("data:text/html;charset=utf-8,Web Console test for reflow activity");
|
||||
|
||||
browser.addEventListener("load", function onLoad() {
|
||||
browser.removeEventListener("load", onLoad, true);
|
||||
openConsole(gBrowser.selectedTab, function(hud) {
|
||||
|
||||
function onReflowListenersReady(aType, aPacket) {
|
||||
browser.contentDocument.body.style.display = "none";
|
||||
browser.contentDocument.body.clientTop;
|
||||
}
|
||||
|
||||
Services.prefs.setBoolPref("devtools.webconsole.filter.csslog", true);
|
||||
hud.ui._updateReflowActivityListener(onReflowListenersReady);
|
||||
Services.prefs.clearUserPref("devtools.webconsole.filter.csslog");
|
||||
|
||||
waitForMessages({
|
||||
webconsole: hud,
|
||||
messages: [{
|
||||
text: /reflow: /,
|
||||
category: CATEGORY_CSS,
|
||||
severity: SEVERITY_LOG,
|
||||
}],
|
||||
}).then(() => {
|
||||
finishTest();
|
||||
});
|
||||
});
|
||||
}, true);
|
||||
}
|
@ -106,14 +106,14 @@ const SEVERITY_CLASS_FRAGMENTS = [
|
||||
// Most of these rather idiosyncratic names are historical and predate the
|
||||
// division of message type into "category" and "severity".
|
||||
const MESSAGE_PREFERENCE_KEYS = [
|
||||
// Error Warning Info Log
|
||||
[ "network", "netwarn", null, "networkinfo", ], // Network
|
||||
[ "csserror", "cssparser", null, null, ], // CSS
|
||||
[ "exception", "jswarn", null, "jslog", ], // JS
|
||||
[ "error", "warn", "info", "log", ], // Web Developer
|
||||
[ null, null, null, null, ], // Input
|
||||
[ null, null, null, null, ], // Output
|
||||
[ "secerror", "secwarn", null, null, ], // Security
|
||||
// Error Warning Info Log
|
||||
[ "network", "netwarn", null, "networkinfo", ], // Network
|
||||
[ "csserror", "cssparser", null, "csslog", ], // CSS
|
||||
[ "exception", "jswarn", null, "jslog", ], // JS
|
||||
[ "error", "warn", "info", "log", ], // Web Developer
|
||||
[ null, null, null, null, ], // Input
|
||||
[ null, null, null, null, ], // Output
|
||||
[ "secerror", "secwarn", null, null, ], // Security
|
||||
];
|
||||
|
||||
// A mapping from the console API log event levels to the Web Console
|
||||
@ -563,15 +563,37 @@ WebConsoleFrame.prototype = {
|
||||
*/
|
||||
_initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs()
|
||||
{
|
||||
let prefs = ["network", "networkinfo", "csserror", "cssparser", "exception",
|
||||
"jswarn", "jslog", "error", "info", "warn", "log", "secerror",
|
||||
"secwarn", "netwarn"];
|
||||
let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
|
||||
"exception", "jswarn", "jslog", "error", "info", "warn", "log",
|
||||
"secerror", "secwarn", "netwarn"];
|
||||
for (let pref of prefs) {
|
||||
this.filterPrefs[pref] = Services.prefs
|
||||
.getBoolPref(this._filterPrefsPrefix + pref);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Attach / detach reflow listeners depending on the checked status
|
||||
* of the `CSS > Log` menuitem.
|
||||
*
|
||||
* @param function [aCallback=null]
|
||||
* Optional function to invoke when the listener has been
|
||||
* added/removed.
|
||||
*
|
||||
*/
|
||||
_updateReflowActivityListener:
|
||||
function WCF__updateReflowActivityListener(aCallback)
|
||||
{
|
||||
if (this.webConsoleClient) {
|
||||
let pref = this._filterPrefsPrefix + "csslog";
|
||||
if (Services.prefs.getBoolPref(pref)) {
|
||||
this.webConsoleClient.startListeners(["ReflowActivity"], aCallback);
|
||||
} else {
|
||||
this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the events for the filter input field.
|
||||
* @private
|
||||
@ -787,6 +809,7 @@ WebConsoleFrame.prototype = {
|
||||
this.filterPrefs[aToggleType] = aState;
|
||||
this.adjustVisibilityForMessageType(aToggleType, aState);
|
||||
Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState);
|
||||
this._updateReflowActivityListener();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1534,6 +1557,42 @@ WebConsoleFrame.prototype = {
|
||||
this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the reflow activity messages coming from the remote Web Console.
|
||||
*
|
||||
* @param object aMessage
|
||||
* An object holding information about a reflow batch.
|
||||
*/
|
||||
logReflowActivity: function WCF_logReflowActivity(aMessage)
|
||||
{
|
||||
let {start, end, sourceURL, sourceLine} = aMessage;
|
||||
let duration = Math.round((end - start) * 100) / 100;
|
||||
let node = this.document.createElementNS(XHTML_NS, "span");
|
||||
if (sourceURL) {
|
||||
node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]);
|
||||
let a = this.document.createElementNS(XHTML_NS, "a");
|
||||
a.href = "#";
|
||||
a.draggable = "false";
|
||||
let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL);
|
||||
let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction");
|
||||
a.textContent = l10n.getFormatStr("reflow.messageLinkText",
|
||||
[functionName, filename, sourceLine]);
|
||||
this._addMessageLinkCallback(a, () => {
|
||||
this.owner.viewSourceInDebugger(sourceURL, sourceLine);
|
||||
});
|
||||
node.appendChild(a);
|
||||
} else {
|
||||
node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]);
|
||||
}
|
||||
return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node);
|
||||
},
|
||||
|
||||
|
||||
handleReflowActivity: function WCF_handleReflowActivity(aMessage)
|
||||
{
|
||||
this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Inform user that the window.console API has been replaced by a script
|
||||
* in a content page.
|
||||
@ -2349,8 +2408,10 @@ WebConsoleFrame.prototype = {
|
||||
|
||||
// Add the message repeats node only when needed.
|
||||
let repeatNode = null;
|
||||
if (aCategory != CATEGORY_INPUT && aCategory != CATEGORY_OUTPUT &&
|
||||
aCategory != CATEGORY_NETWORK) {
|
||||
if (aCategory != CATEGORY_INPUT &&
|
||||
aCategory != CATEGORY_OUTPUT &&
|
||||
aCategory != CATEGORY_NETWORK &&
|
||||
!(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) {
|
||||
repeatNode = this.document.createElementNS(XHTML_NS, "span");
|
||||
repeatNode.setAttribute("value", "1");
|
||||
repeatNode.className = "repeats";
|
||||
@ -4656,6 +4717,7 @@ function WebConsoleConnectionProxy(aWebConsole, aTarget)
|
||||
this._onNetworkEvent = this._onNetworkEvent.bind(this);
|
||||
this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
|
||||
this._onFileActivity = this._onFileActivity.bind(this);
|
||||
this._onReflowActivity = this._onReflowActivity.bind(this);
|
||||
this._onTabNavigated = this._onTabNavigated.bind(this);
|
||||
this._onAttachConsole = this._onAttachConsole.bind(this);
|
||||
this._onCachedMessages = this._onCachedMessages.bind(this);
|
||||
@ -4762,6 +4824,7 @@ WebConsoleConnectionProxy.prototype = {
|
||||
client.addListener("networkEvent", this._onNetworkEvent);
|
||||
client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
|
||||
client.addListener("fileActivity", this._onFileActivity);
|
||||
client.addListener("reflowActivity", this._onReflowActivity);
|
||||
client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
|
||||
this.target.on("will-navigate", this._onTabNavigated);
|
||||
this.target.on("navigate", this._onTabNavigated);
|
||||
@ -4827,6 +4890,8 @@ WebConsoleConnectionProxy.prototype = {
|
||||
|
||||
let msgs = ["PageError", "ConsoleAPI"];
|
||||
this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
|
||||
|
||||
this.owner._updateReflowActivityListener();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -4964,6 +5029,13 @@ WebConsoleConnectionProxy.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
_onReflowActivity: function WCCP__onReflowActivity(aType, aPacket)
|
||||
{
|
||||
if (this.owner && aPacket.from == this._consoleActor) {
|
||||
this.owner.handleReflowActivity(aPacket);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The "lastPrivateContextExited" message type handler. When this message is
|
||||
* received the Web Console UI is cleared.
|
||||
@ -5039,6 +5111,7 @@ WebConsoleConnectionProxy.prototype = {
|
||||
this.client.removeListener("networkEvent", this._onNetworkEvent);
|
||||
this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
|
||||
this.client.removeListener("fileActivity", this._onFileActivity);
|
||||
this.client.removeListener("reflowActivity", this._onReflowActivity);
|
||||
this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
|
||||
this.target.off("will-navigate", this._onTabNavigated);
|
||||
this.target.off("navigate", this._onTabNavigated);
|
||||
|
@ -109,6 +109,8 @@ function goUpdateConsoleCommands() {
|
||||
prefKey="csserror"/>
|
||||
<menuitem label="&btnConsoleWarnings;" type="checkbox"
|
||||
autocheck="false" prefKey="cssparser"/>
|
||||
<menuitem label="&btnConsoleLog;" type="checkbox"
|
||||
autocheck="false" prefKey="csslog"/>
|
||||
</menupopup>
|
||||
</toolbarbutton>
|
||||
<toolbarbutton label="&btnPageJS.label;" type="menu-button"
|
||||
|
@ -19,18 +19,24 @@ exclude_files = \
|
||||
$(FINAL_TARGET)/chrome/pdfjs.manifest: $(GLOBAL_DEPS)
|
||||
printf "manifest pdfjs/chrome.manifest" > $@
|
||||
|
||||
$(FINAL_TARGET)/chrome/shumway.manifest: $(GLOBAL_DEPS)
|
||||
printf "manifest shumway/chrome.manifest" > $@
|
||||
|
||||
libs:: $(FINAL_TARGET)/chrome/pdfjs.manifest $(FINAL_TARGET)/chrome/shumway.manifest
|
||||
libs:: $(FINAL_TARGET)/chrome/pdfjs.manifest
|
||||
$(PYTHON) $(topsrcdir)/config/nsinstall.py \
|
||||
$(srcdir)/pdfjs \
|
||||
$(foreach exclude,$(exclude_files), -X $(srcdir)/pdfjs/$(exclude)) \
|
||||
$(FINAL_TARGET)/chrome
|
||||
$(call py_action,buildlist,$(FINAL_TARGET)/chrome.manifest "manifest chrome/pdfjs.manifest")
|
||||
|
||||
ifdef NIGHTLY_BUILD
|
||||
$(FINAL_TARGET)/chrome/shumway.manifest: $(GLOBAL_DEPS)
|
||||
printf "manifest shumway/chrome.manifest" > $@
|
||||
|
||||
libs:: $(FINAL_TARGET)/chrome/shumway.manifest
|
||||
$(PYTHON) $(topsrcdir)/config/nsinstall.py \
|
||||
$(srcdir)/shumway \
|
||||
$(foreach exclude,$(exclude_files), -X $(srcdir)/shumway/$(exclude)) \
|
||||
$(FINAL_TARGET)/chrome
|
||||
$(call py_action,buildlist,$(FINAL_TARGET)/chrome.manifest "manifest chrome/pdfjs.manifest")
|
||||
$(call py_action,buildlist,$(FINAL_TARGET)/chrome.manifest "manifest chrome/shumway.manifest")
|
||||
endif
|
||||
|
||||
ifdef MOZ_METRO
|
||||
$(DIST)/bin/metro/chrome/pdfjs.manifest: $(GLOBAL_DEPS)
|
||||
|
@ -573,8 +573,10 @@
|
||||
@BINPATH@/browser/chrome/browser.manifest
|
||||
@BINPATH@/browser/chrome/pdfjs.manifest
|
||||
@BINPATH@/browser/chrome/pdfjs/*
|
||||
#ifdef NIGHTLY_BUILD
|
||||
@BINPATH@/browser/chrome/shumway.manifest
|
||||
@BINPATH@/browser/chrome/shumway/*
|
||||
#endif
|
||||
@BINPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf
|
||||
@BINPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/icon.png
|
||||
@BINPATH@/chrome/toolkit@JAREXT@
|
||||
|
@ -74,6 +74,9 @@
|
||||
<!ENTITY projects.hostedManifestPlaceHolder2 "http://example.com/app/manifest.webapp">
|
||||
<!ENTITY projects.noProjects "No projects. Add a new packaged app below (local directory) or a hosted app (link to a manifest file).">
|
||||
<!ENTITY projects.manifestEditor "Manifest Editor">
|
||||
<!ENTITY projects.manifestEditorTooltip "Edit your app's manifest in the panel below. The Update button will save your changes and update the app.">
|
||||
<!ENTITY projects.manifestViewer "Manifest Viewer">
|
||||
<!ENTITY projects.manifestViewerTooltip "Examine your app's manifest in the panel below.">
|
||||
|
||||
<!ENTITY help.title "App Manager">
|
||||
<!ENTITY help.close "Close">
|
||||
|
@ -1,3 +1,6 @@
|
||||
<!ENTITY inspectorHTMLEdit.label "Edit As HTML">
|
||||
<!ENTITY inspectorHTMLEdit.accesskey "E">
|
||||
|
||||
<!ENTITY inspectorHTMLCopyInner.label "Copy Inner HTML">
|
||||
<!ENTITY inspectorHTMLCopyInner.accesskey "I">
|
||||
|
||||
|
@ -89,6 +89,17 @@ scratchpad.linkText=Shift+RETURN - Open in Scratchpad
|
||||
# Parameters: %S is the object type.
|
||||
gcliterm.instanceLabel=Instance of %S
|
||||
|
||||
# LOCALIZATION NOTE (reflow.*): the console displays reflow activity.
|
||||
# We can get 2 kind of lines: with JS link or without JS link. It looks like
|
||||
# that:
|
||||
# reflow: 12ms
|
||||
# reflow: 12ms function foobar, file.js line 42
|
||||
# The 2nd line, from "function" to the end of the line, is a link to the
|
||||
# JavaScript debugger.
|
||||
reflow.messageWithNoLink=reflow: %Sms
|
||||
reflow.messageWithLink=reflow: %Sms\u0020
|
||||
reflow.messageLinkText=function %1$S, %2$S line %3$S
|
||||
|
||||
# LOCALIZATION NOTE (stacktrace.anonymousFunction): this string is used to
|
||||
# display JavaScript functions that have no given name - they are said to be
|
||||
# anonymous. See also stacktrace.outputMessage.
|
||||
|
@ -368,7 +368,8 @@ var ContextCommands = {
|
||||
|
||||
// prefered save location
|
||||
Task.spawn(function() {
|
||||
picker.displayDirectory = yield Downloads.getPreferredDownloadsDirectory();
|
||||
let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
|
||||
picker.displayDirectory = new FileUtils.File(preferredDir);
|
||||
|
||||
try {
|
||||
let lastDir = Services.prefs.getComplexValue("browser.download.lastDir", Ci.nsILocalFile);
|
||||
|
@ -61,13 +61,14 @@ var APZCObserver = {
|
||||
case 'TabOpen': {
|
||||
let browser = aEvent.originalTarget.linkedBrowser;
|
||||
browser.addEventListener("pageshow", this, true);
|
||||
browser.messageManager.addMessageListener("scroll", this);
|
||||
// Register for notifications from content about scroll actions.
|
||||
browser.messageManager.addMessageListener("Browser:ContentScroll", this);
|
||||
break;
|
||||
}
|
||||
case 'TabClose': {
|
||||
let browser = aEvent.originalTarget.linkedBrowser;
|
||||
browser.removeEventListener("pageshow", this, true);
|
||||
browser.messageManager.removeMessageListener("scroll", this);
|
||||
browser.messageManager.removeMessageListener("Browser:ContentScroll", this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -99,7 +100,7 @@ var APZCObserver = {
|
||||
getInterface(Ci.nsIDOMWindowUtils);
|
||||
windowUtils.setScrollPositionClampingScrollPortSize(compositedRect.width,
|
||||
compositedRect.height);
|
||||
Browser.selectedBrowser.messageManager.sendAsyncMessage("Content:SetCacheViewport", {
|
||||
Browser.selectedBrowser.messageManager.sendAsyncMessage("Content:SetDisplayPort", {
|
||||
scrollX: scrollTo.x,
|
||||
scrollY: scrollTo.y,
|
||||
x: displayPort.x + scrollTo.x,
|
||||
@ -133,7 +134,12 @@ var APZCObserver = {
|
||||
receiveMessage: function(aMessage) {
|
||||
let json = aMessage.json;
|
||||
switch (aMessage.name) {
|
||||
case "scroll": {
|
||||
// Content notifies us here (syncronously) if it has scrolled
|
||||
// independent of the apz. This can happen in a lot of
|
||||
// cases: keyboard shortcuts, scroll wheel, or content script.
|
||||
// Let the apz know about this change so that it can update
|
||||
// its scroll offset data.
|
||||
case "Browser:ContentScroll": {
|
||||
let data = json.viewId + " " + json.presShellId + " (" + json.scrollOffset.x + ", " + json.scrollOffset.y + ")";
|
||||
Services.obs.notifyObservers(null, "scroll-offset-changed", data);
|
||||
break;
|
||||
|
@ -552,7 +552,7 @@ let ContentScroll = {
|
||||
_scrollOffset: { x: 0, y: 0 },
|
||||
|
||||
init: function() {
|
||||
addMessageListener("Content:SetCacheViewport", this);
|
||||
addMessageListener("Content:SetDisplayPort", this);
|
||||
addMessageListener("Content:SetWindowSize", this);
|
||||
|
||||
if (Services.prefs.getBoolPref("layers.async-pan-zoom.enabled")) {
|
||||
@ -587,7 +587,9 @@ let ContentScroll = {
|
||||
receiveMessage: function(aMessage) {
|
||||
let json = aMessage.json;
|
||||
switch (aMessage.name) {
|
||||
case "Content:SetCacheViewport": {
|
||||
// Sent to us from chrome when the the apz has requested that the
|
||||
// display port be updated and that content should repaint.
|
||||
case "Content:SetDisplayPort": {
|
||||
// Set resolution for root view
|
||||
let rootCwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
if (json.id == 1) {
|
||||
@ -659,7 +661,7 @@ let ContentScroll = {
|
||||
break;
|
||||
|
||||
case "scroll": {
|
||||
this.sendScroll(aEvent.target);
|
||||
this.notifyChromeAboutContentScroll(aEvent.target);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -686,7 +688,13 @@ let ContentScroll = {
|
||||
}
|
||||
},
|
||||
|
||||
sendScroll: function sendScroll(target) {
|
||||
/*
|
||||
* DOM scroll handler - if we receive this, content or the dom scrolled
|
||||
* content without going through the apz. This can happen in a lot of
|
||||
* cases, keyboard shortcuts, scroll wheel, or content script. Messages
|
||||
* chrome via a sync call which messages the apz about the update.
|
||||
*/
|
||||
notifyChromeAboutContentScroll: function (target) {
|
||||
let isRoot = false;
|
||||
if (target instanceof Ci.nsIDOMDocument) {
|
||||
var window = target.defaultView;
|
||||
@ -713,11 +721,13 @@ let ContentScroll = {
|
||||
let presShellId = {};
|
||||
utils.getPresShellId(presShellId);
|
||||
let viewId = utils.getViewId(element);
|
||||
|
||||
sendAsyncMessage("scroll", { presShellId: presShellId.value,
|
||||
viewId: viewId,
|
||||
scrollOffset: scrollOffset,
|
||||
isRoot: isRoot });
|
||||
// Must be synchronous to prevent redraw getting out of sync from
|
||||
// composition.
|
||||
sendSyncMessage("Browser:ContentScroll",
|
||||
{ presShellId: presShellId.value,
|
||||
viewId: viewId,
|
||||
scrollOffset: scrollOffset,
|
||||
isRoot: isRoot });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -16,6 +16,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
|
||||
"resource://gre/modules/FormHistory.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
|
||||
"resource://gre/modules/PageThumbs.jsm");
|
||||
|
||||
|
@ -458,6 +458,15 @@ strong {
|
||||
.manifest-editor > h2 {
|
||||
font-size: 18px;
|
||||
margin: 1em 30px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
[type="packaged"] > .editable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[type="hosted"] > .viewable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.variables-view {
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
.theme-body {
|
||||
background: #131c26;
|
||||
color: #8fa1b2
|
||||
color: #8fa1b2;
|
||||
}
|
||||
|
||||
.theme-twisty {
|
||||
@ -47,7 +47,8 @@
|
||||
background: #26394D;
|
||||
}
|
||||
|
||||
.theme-bg-darker {
|
||||
.theme-bg-darker,
|
||||
.cm-s-mozilla .CodeMirror-gutters {
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
@ -55,11 +56,14 @@
|
||||
background: #a18650;
|
||||
}
|
||||
|
||||
.theme-link { /* blue */
|
||||
.theme-link,
|
||||
.cm-s-mozilla .cm-link { /* blue */
|
||||
color: #3689b2;
|
||||
}
|
||||
|
||||
.theme-comment { /* grey */
|
||||
.theme-comment,
|
||||
.cm-s-mozilla .cm-meta,
|
||||
.cm-s-mozilla .cm-hr { /* grey */
|
||||
color: #5c6773;
|
||||
}
|
||||
|
||||
@ -73,31 +77,51 @@
|
||||
border-color: #303b47;
|
||||
}
|
||||
|
||||
.theme-fg-color1 { /* green */
|
||||
.theme-fg-color1,
|
||||
.cm-s-mozilla .cm-variable-2,
|
||||
.cm-s-mozilla .cm-quote,
|
||||
.cm-s-mozilla .CodeMirror-matchingbracket { /* green */
|
||||
color: #5c9966;
|
||||
}
|
||||
|
||||
.theme-fg-color2 { /* blue */
|
||||
.theme-fg-color2,
|
||||
.cm-s-mozilla .cm-attribute,
|
||||
.cm-s-mozilla .cm-builtin,
|
||||
.cm-s-mozilla .cm-variable,
|
||||
.cm-s-mozilla .cm-def,
|
||||
.cm-s-mozilla .cm-variable-3,
|
||||
.cm-s-mozilla .cm-property,
|
||||
.cm-s-mozilla .cm-qualifier { /* blue */
|
||||
color: #3689b2;
|
||||
}
|
||||
|
||||
.theme-fg-color3 { /* pink/lavender */
|
||||
.theme-fg-color3,
|
||||
.cm-s-mozilla .cm-tag,
|
||||
.cm-s-mozilla .cm-header { /* pink/lavender */
|
||||
color: #a673bf;
|
||||
}
|
||||
|
||||
.theme-fg-color4 { /* purple/violet */
|
||||
.theme-fg-color4,
|
||||
.cm-s-mozilla .cm-comment { /* purple/violet */
|
||||
color: #6270b2;
|
||||
}
|
||||
|
||||
.theme-fg-color5 { /* Yellow */
|
||||
.theme-fg-color5,
|
||||
.cm-s-mozilla .cm-bracket,
|
||||
.cm-s-mozilla .cm-atom,
|
||||
.cm-s-mozilla .cm-keyword { /* Yellow */
|
||||
color: #a18650;
|
||||
}
|
||||
|
||||
.theme-fg-color6 { /* Orange */
|
||||
.theme-fg-color6,
|
||||
.cm-s-mozilla .cm-string { /* Orange */
|
||||
color: #b26b47;
|
||||
}
|
||||
|
||||
.theme-fg-color7 { /* Red */
|
||||
.theme-fg-color7,
|
||||
.cm-s-mozilla .CodeMirror-nonmatchingbracket,
|
||||
.cm-s-mozilla .cm-string-2,
|
||||
.cm-s-mozilla .cm-error { /* Red */
|
||||
color: #bf5656;
|
||||
}
|
||||
|
||||
@ -110,3 +134,41 @@
|
||||
.markupview-colorswatch {
|
||||
box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* CodeMirror specific styles.
|
||||
* Best effort to match the existing theme, some of the colors
|
||||
* are duplicated here to prevent weirdness in the main theme. */
|
||||
|
||||
.CodeMirror { /* Inherit platform specific font sizing and styles */
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.CodeMirror pre,
|
||||
.cm-s-mozilla .cm-operator,
|
||||
.cm-s-mozilla .cm-special,
|
||||
.cm-s-mozilla .cm-number { /* theme-body color */
|
||||
color: #8fa1b2;
|
||||
}
|
||||
|
||||
.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor {
|
||||
border-left: solid 1px #fff;
|
||||
}
|
||||
|
||||
.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */
|
||||
background: rgb(185, 215, 253);
|
||||
}
|
||||
|
||||
.dcm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */
|
||||
background: rgb(176, 176, 176);
|
||||
}
|
||||
|
||||
.CodeMirror-activeline-background { /* selected color with alpha */
|
||||
background: rgba(185, 215, 253, .05);
|
||||
}
|
||||
|
||||
.cm-s-markup-view pre {
|
||||
line-height: 1.4em;
|
||||
min-height: 1.4em;
|
||||
}
|
||||
|
@ -20,10 +20,9 @@
|
||||
/* Highlighter - Node Infobar */
|
||||
|
||||
.highlighter-nodeinfobar {
|
||||
color: hsl(200, 100%, 65%);
|
||||
border: 1px solid hsla(210, 19%, 63%, .5);
|
||||
color: hsl(216,33%,97%);
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(hsl(209, 18%, 30%), hsl(210, 24%, 16%)) no-repeat padding-box;
|
||||
background: hsl(214,13%,24%) no-repeat padding-box;
|
||||
}
|
||||
|
||||
/* Highlighter - Node Infobar - text */
|
||||
@ -35,22 +34,22 @@
|
||||
}
|
||||
|
||||
html|*.highlighter-nodeinfobar-tagname {
|
||||
color: white;
|
||||
color: hsl(285,100%,75%);
|
||||
}
|
||||
|
||||
html|*.highlighter-nodeinfobar-id {
|
||||
color: hsl(90, 79%, 52%);
|
||||
color: hsl(103,46%,54%);
|
||||
}
|
||||
|
||||
html|*.highlighter-nodeinfobar-classes,
|
||||
html|*.highlighter-nodeinfobar-pseudo-classes {
|
||||
color: hsl(20, 100%, 70%);
|
||||
color: hsl(200,74%,57%);
|
||||
}
|
||||
|
||||
/* Highlighter - Node Infobar - buttons */
|
||||
|
||||
.highlighter-nodeinfobar-button {
|
||||
-moz-appearance: none;
|
||||
border: 0 solid hsla(210,8%,5%,.45);
|
||||
padding: 0;
|
||||
width: 26px;
|
||||
min-height: 26px;
|
||||
@ -60,32 +59,20 @@ html|*.highlighter-nodeinfobar-pseudo-classes {
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-inspectbutton {
|
||||
-moz-border-end-width: 1px;
|
||||
box-shadow: 1px 0 0 hsla(210,16%,76%,.15), -1px 0 0 hsla(210,16%,76%,.15) inset;
|
||||
-moz-margin-end: 6px;
|
||||
list-style-image: url("chrome://browser/skin/devtools/inspect-button.png");
|
||||
-moz-image-region: rect(0px 16px 16px 0px);
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-inspectbutton:-moz-locale-dir(rtl) {
|
||||
box-shadow: -1px 0 0 hsla(210,16%,76%,.15), 1px 0 0 hsla(210,16%,76%,.15) inset;
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-inspectbutton:active:hover,
|
||||
.highlighter-nodeinfobar-container:not([locked]) > .highlighter-nodeinfobar > .highlighter-nodeinfobar-inspectbutton {
|
||||
-moz-image-region: rect(0px 32px 16px 16px);
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-menu {
|
||||
-moz-border-start-width: 1px;
|
||||
box-shadow: -1px 0 0 hsla(210,16%,76%,.15), 1px 0 0 hsla(210,16%,76%,.15) inset;
|
||||
-moz-margin-start: 6px;
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-menu:-moz-locale-dir(rtl) {
|
||||
box-shadow: 1px 0 0 hsla(210,16%,76%,.15), -1px 0 0 hsla(210,16%,76%,.15) inset;
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-menu > .toolbarbutton-menu-dropmarker {
|
||||
-moz-appearance: none !important;
|
||||
list-style-image: url("chrome://browser/skin/devtools/dropmarker.png");
|
||||
@ -100,7 +87,6 @@ html|*.highlighter-nodeinfobar-pseudo-classes {
|
||||
height: 14px;
|
||||
-moz-margin-start: calc(50% - 7px);
|
||||
transform: rotate(-45deg);
|
||||
border: 1px solid transparent;
|
||||
background-clip: padding-box;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@ -108,22 +94,13 @@ html|*.highlighter-nodeinfobar-pseudo-classes {
|
||||
.highlighter-nodeinfobar-arrow-top {
|
||||
margin-bottom: -8px;
|
||||
margin-top: 8px;
|
||||
border-right-color: hsla(210, 19%, 63%, .5);
|
||||
border-top-color: hsla(210, 19%, 63%, .5);
|
||||
background-image: linear-gradient(to top right, transparent 50%, hsl(209, 18%, 30%) 50%);
|
||||
background-image: linear-gradient(to top right, transparent 50%, hsl(210,2%,22%) 50%);
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-arrow-bottom {
|
||||
margin-top: -8px;
|
||||
margin-bottom: 8px;
|
||||
border-left-color: hsla(210, 19%, 63%, .5);
|
||||
border-bottom-color: hsla(210, 19%, 63%, .5);
|
||||
background-image: linear-gradient(to bottom left, transparent 50%, hsl(210, 24%, 16%) 50%);
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-container[position="top"] > .highlighter-nodeinfobar,
|
||||
.highlighter-nodeinfobar-container[position="overlap"] > .highlighter-nodeinfobar {
|
||||
box-shadow: 0 1px 0 hsla(0, 0%, 100%, .1) inset;
|
||||
background-image: linear-gradient(to bottom left, transparent 50%, hsl(210,2%,22%) 50%);
|
||||
}
|
||||
|
||||
.highlighter-nodeinfobar-container[hide-arrow] > .highlighter-nodeinfobar {
|
||||
|
@ -47,7 +47,8 @@
|
||||
background-color: #CCC;
|
||||
}
|
||||
|
||||
.theme-bg-darker {
|
||||
.theme-bg-darker,
|
||||
.cm-s-mozilla .CodeMirror-gutters {
|
||||
background: #EFEFEF;
|
||||
}
|
||||
|
||||
@ -55,11 +56,14 @@
|
||||
background: #a18650;
|
||||
}
|
||||
|
||||
.theme-link { /* blue */
|
||||
.theme-link,
|
||||
.cm-s-mozilla .cm-link { /* blue */
|
||||
color: hsl(208,56%,40%);
|
||||
}
|
||||
|
||||
.theme-comment { /* grey */
|
||||
.theme-comment,
|
||||
.cm-s-mozilla .cm-meta,
|
||||
.cm-s-mozilla .cm-hr { /* grey */
|
||||
color: hsl(90,2%,46%);
|
||||
}
|
||||
|
||||
@ -73,31 +77,51 @@
|
||||
border-color: #cddae5;
|
||||
}
|
||||
|
||||
.theme-fg-color1 { /* green */
|
||||
.theme-fg-color1,
|
||||
.cm-s-mozilla .cm-variable-2,
|
||||
.cm-s-mozilla .cm-quote,
|
||||
.cm-s-mozilla .CodeMirror-matchingbracket { /* green */
|
||||
color: hsl(72,100%,27%);
|
||||
}
|
||||
|
||||
.theme-fg-color2 { /* blue */
|
||||
.theme-fg-color2,
|
||||
.cm-s-mozilla .cm-attribute,
|
||||
.cm-s-mozilla .cm-builtin,
|
||||
.cm-s-mozilla .cm-variable,
|
||||
.cm-s-mozilla .cm-def,
|
||||
.cm-s-mozilla .cm-variable-3,
|
||||
.cm-s-mozilla .cm-property,
|
||||
.cm-s-mozilla .cm-qualifier { /* blue */
|
||||
color: hsl(208,56%,40%);
|
||||
}
|
||||
|
||||
.theme-fg-color3 { /* dark blue */
|
||||
.theme-fg-color3,
|
||||
.cm-s-mozilla .cm-tag,
|
||||
.cm-s-mozilla .cm-header { /* dark blue */
|
||||
color: hsl(208,81%,21%)
|
||||
}
|
||||
|
||||
.theme-fg-color4 { /* Orange */
|
||||
.theme-fg-color4,
|
||||
.cm-s-mozilla .cm-comment { /* Orange */
|
||||
color: hsl(24,85%,39%);
|
||||
}
|
||||
|
||||
.theme-fg-color5 { /* Yellow */
|
||||
.theme-fg-color5,
|
||||
.cm-s-mozilla .cm-bracket,
|
||||
.cm-s-mozilla .cm-keyword,
|
||||
.cm-s-mozilla .cm-atom { /* Yellow */
|
||||
color: #a18650;
|
||||
}
|
||||
|
||||
.theme-fg-color6 { /* Orange */
|
||||
.theme-fg-color6,
|
||||
.cm-s-mozilla .cm-string { /* Orange */
|
||||
color: hsl(24,85%,39%);
|
||||
}
|
||||
|
||||
.theme-fg-color7 { /* Red */
|
||||
.theme-fg-color7,
|
||||
.cm-s-mozilla .CodeMirror-nonmatchingbracket,
|
||||
.cm-s-mozilla .cm-string-2,
|
||||
.cm-s-mozilla .cm-error { /* Red */
|
||||
color: #bf5656;
|
||||
}
|
||||
|
||||
@ -110,3 +134,41 @@
|
||||
.markupview-colorswatch {
|
||||
box-shadow: 0 0 0 1px #EFEFEF;
|
||||
}
|
||||
|
||||
/* CodeMirror specific styles.
|
||||
* Best effort to match the existing theme, some of the colors
|
||||
* are duplicated here to prevent weirdness in the main theme. */
|
||||
|
||||
.CodeMirror { /* Inherit platform specific font sizing and styles */
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.CodeMirror pre,
|
||||
.cm-s-mozilla .cm-operator,
|
||||
.cm-s-mozilla .cm-special,
|
||||
.cm-s-mozilla .cm-number { /* theme-body color */
|
||||
color: black;
|
||||
}
|
||||
|
||||
.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor {
|
||||
border-left: solid 1px black;
|
||||
}
|
||||
|
||||
.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */
|
||||
background: rgb(185, 215, 253);
|
||||
}
|
||||
|
||||
.cm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */
|
||||
background: rgb(176, 176, 176);
|
||||
}
|
||||
|
||||
.CodeMirror-activeline-background { /* selected color with alpha */
|
||||
background: rgba(185, 215, 253, .4);
|
||||
}
|
||||
|
||||
.cm-s-markup-view pre {
|
||||
line-height: 1.4em;
|
||||
min-height: 1.4em;
|
||||
}
|
||||
|
@ -172,7 +172,6 @@ abstract public class GeckoApp
|
||||
public static int mOrientation;
|
||||
protected boolean mIsRestoringActivity;
|
||||
private String mCurrentResponse = "";
|
||||
public static boolean sIsUsingCustomProfile = false;
|
||||
|
||||
private ContactService mContactService;
|
||||
private PromptService mPromptService;
|
||||
@ -1175,7 +1174,7 @@ abstract public class GeckoApp
|
||||
if (profileName == null)
|
||||
profileName = "default";
|
||||
}
|
||||
GeckoApp.sIsUsingCustomProfile = true;
|
||||
GeckoProfile.sIsUsingCustomProfile = true;
|
||||
}
|
||||
|
||||
if (profileName != null || profilePath != null) {
|
||||
|
@ -35,6 +35,7 @@ public final class GeckoProfile {
|
||||
private final String mName;
|
||||
private File mMozDir;
|
||||
private File mDir;
|
||||
public static boolean sIsUsingCustomProfile = false;
|
||||
|
||||
// Constants to cache whether or not a profile is "locked".
|
||||
private enum LockState {
|
||||
@ -60,7 +61,13 @@ public final class GeckoProfile {
|
||||
}
|
||||
|
||||
public static GeckoProfile get(Context context) {
|
||||
if (context instanceof GeckoApp) {
|
||||
boolean isGeckoApp = false;
|
||||
try {
|
||||
isGeckoApp = context instanceof GeckoApp;
|
||||
} catch (NoClassDefFoundError ex) {}
|
||||
|
||||
|
||||
if (isGeckoApp) {
|
||||
// Check for a cached profile on this context already
|
||||
// TODO: We should not be caching profile information on the Activity context
|
||||
if (((GeckoApp)context).mProfile != null) {
|
||||
@ -74,7 +81,7 @@ public final class GeckoProfile {
|
||||
return guest;
|
||||
}
|
||||
|
||||
if (context instanceof GeckoApp) {
|
||||
if (isGeckoApp) {
|
||||
// Otherwise, get the default profile for the Activity
|
||||
return get(context, ((GeckoApp)context).getDefaultProfileName());
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ public class GeckoThread extends Thread implements GeckoEventListener {
|
||||
if (args == null || !args.contains(BrowserApp.GUEST_BROWSING_ARG)) {
|
||||
guest = " " + BrowserApp.GUEST_BROWSING_ARG;
|
||||
}
|
||||
} else if (!GeckoApp.sIsUsingCustomProfile) {
|
||||
} else if (!GeckoProfile.sIsUsingCustomProfile) {
|
||||
// If nothing was passed in in the intent, force Gecko to use the default profile for
|
||||
// for this activity
|
||||
profile = " -P " + GeckoAppShell.getGeckoInterface().getProfile().getName();
|
||||
|
@ -46,7 +46,12 @@ public class GeckoView extends LayerView
|
||||
|
||||
// If running outside of a GeckoActivity (eg, from a library project),
|
||||
// load the native code and disable content providers
|
||||
if (!(context instanceof GeckoActivity)) {
|
||||
boolean isGeckoActivity = false;
|
||||
try {
|
||||
isGeckoActivity = context instanceof GeckoActivity;
|
||||
} catch (NoClassDefFoundError ex) {}
|
||||
|
||||
if (!isGeckoActivity) {
|
||||
// Set the GeckoInterface if the context is an activity and the GeckoInterface
|
||||
// has not already been set
|
||||
if (context instanceof Activity && getGeckoInterface() == null) {
|
||||
@ -59,7 +64,7 @@ public class GeckoView extends LayerView
|
||||
|
||||
GeckoLoader.loadMozGlue();
|
||||
BrowserDB.setEnableContentProviders(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (url != null) {
|
||||
GeckoThread.setUri(url);
|
||||
|
@ -896,16 +896,20 @@ public class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
|
||||
}
|
||||
|
||||
private void setShadowVisibility() {
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (BrowserApp.mBrowserToolbar == null) {
|
||||
return;
|
||||
try {
|
||||
if (BrowserApp.mBrowserToolbar == null) // this will throw if we don't have BrowserApp
|
||||
return;
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (BrowserApp.mBrowserToolbar == null) {
|
||||
return;
|
||||
}
|
||||
ImmutableViewportMetrics m = mViewportMetrics;
|
||||
BrowserApp.mBrowserToolbar.setShadowVisibility(m.viewportRectTop >= m.pageRectTop);
|
||||
}
|
||||
ImmutableViewportMetrics m = mViewportMetrics;
|
||||
BrowserApp.mBrowserToolbar.setShadowVisibility(m.viewportRectTop >= m.pageRectTop);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (NoClassDefFoundError ex) {}
|
||||
}
|
||||
|
||||
/** Implementation of PanZoomTarget */
|
||||
|
@ -75,7 +75,11 @@ var HelperApps = {
|
||||
|
||||
// Query for apps that can/can't handle the mimetype
|
||||
let msg = this._getMessage("Intent:GetHandlers", uri, flags);
|
||||
let apps = this._parseApps(this._sendMessage(msg).apps);
|
||||
let data = this._sendMessage(msg);
|
||||
if (!data)
|
||||
return [];
|
||||
|
||||
let apps = this._parseApps(data.apps);
|
||||
|
||||
if (flags.filterHttp) {
|
||||
apps = apps.filter(function(app) {
|
||||
@ -132,7 +136,6 @@ var HelperApps = {
|
||||
},
|
||||
|
||||
_sendMessage: function(msg) {
|
||||
Services.console.logStringMessage("Sending: " + JSON.stringify(msg));
|
||||
let res = Services.androidBridge.handleGeckoMessage(JSON.stringify(msg));
|
||||
return JSON.parse(res);
|
||||
},
|
||||
|
@ -824,6 +824,11 @@ Connection::internalClose()
|
||||
leafName.get()));
|
||||
#endif
|
||||
|
||||
// Set the property to null before closing the connection, otherwise the other
|
||||
// functions in the module may try to use the connection after it is closed.
|
||||
sqlite3 *dbConn = mDBConn;
|
||||
mDBConn = nullptr;
|
||||
|
||||
// At this stage, we may still have statements that need to be
|
||||
// finalized. Attempt to close the database connection. This will
|
||||
// always disconnect any virtual tables and cleanly finalize their
|
||||
@ -831,13 +836,13 @@ Connection::internalClose()
|
||||
// unfinalized client statements, in which case we need to finalize
|
||||
// these statements and close again.
|
||||
|
||||
int srv = sqlite3_close(mDBConn);
|
||||
int srv = sqlite3_close(dbConn);
|
||||
|
||||
if (srv == SQLITE_BUSY) {
|
||||
// We still have non-finalized statements. Finalize them.
|
||||
|
||||
sqlite3_stmt *stmt = nullptr;
|
||||
while ((stmt = ::sqlite3_next_stmt(mDBConn, stmt))) {
|
||||
while ((stmt = ::sqlite3_next_stmt(dbConn, stmt))) {
|
||||
PR_LOG(gStorageLog, PR_LOG_NOTICE,
|
||||
("Auto-finalizing SQL statement '%s' (%x)",
|
||||
::sqlite3_sql(stmt),
|
||||
@ -871,8 +876,8 @@ Connection::internalClose()
|
||||
}
|
||||
|
||||
// Now that all statements have been finalized, we
|
||||
// shoudl be able to close.
|
||||
srv = ::sqlite3_close(mDBConn);
|
||||
// should be able to close.
|
||||
srv = ::sqlite3_close(dbConn);
|
||||
|
||||
}
|
||||
|
||||
@ -881,7 +886,6 @@ Connection::internalClose()
|
||||
"sqlite3_close failed. There are probably outstanding statements that are listed above!");
|
||||
}
|
||||
|
||||
mDBConn = nullptr;
|
||||
return convertResultCode(srv);
|
||||
}
|
||||
|
||||
|
@ -185,7 +185,7 @@ class MochitestRunner(MozbuildObject):
|
||||
def run_desktop_test(self, suite=None, test_file=None, debugger=None,
|
||||
debugger_args=None, shuffle=False, keep_open=False, rerun_failures=False,
|
||||
no_autorun=False, repeat=0, run_until_failure=False, slow=False,
|
||||
chunk_by_dir=0, total_chunks=None, this_chunk=None):
|
||||
chunk_by_dir=0, total_chunks=None, this_chunk=None, jsdebugger=False):
|
||||
"""Runs a mochitest.
|
||||
|
||||
test_file is a path to a test file. It can be a relative path from the
|
||||
@ -285,6 +285,7 @@ class MochitestRunner(MozbuildObject):
|
||||
options.chunkByDir = chunk_by_dir
|
||||
options.totalChunks = total_chunks
|
||||
options.thisChunk = this_chunk
|
||||
options.jsdebugger = jsdebugger
|
||||
|
||||
options.failureFile = failure_file_path
|
||||
|
||||
@ -404,6 +405,10 @@ def MochitestCommand(func):
|
||||
help='If running tests by chunks, the number of the chunk to run.')
|
||||
func = this_chunk(func)
|
||||
|
||||
jsdebugger = CommandArgument('--jsdebugger', action='store_true',
|
||||
help='Start the browser JS debugger before running the test. Implies --no-autorun.')
|
||||
func = jsdebugger(func)
|
||||
|
||||
path = CommandArgument('test_file', default=None, nargs='?',
|
||||
metavar='TEST',
|
||||
help='Test to run. Can be specified as a single file, a ' \
|
||||
|
@ -320,6 +320,12 @@ class MochitestOptions(optparse.OptionParser):
|
||||
"metavar": "PREF=VALUE",
|
||||
"help": "defines an extra user preference",
|
||||
}],
|
||||
[["--jsdebugger"],
|
||||
{ "action": "store_true",
|
||||
"default": False,
|
||||
"dest": "jsdebugger",
|
||||
"help": "open the browser debugger",
|
||||
}],
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@ -413,6 +419,15 @@ class MochitestOptions(optparse.OptionParser):
|
||||
if options.webapprtContent and options.webapprtChrome:
|
||||
self.error("Only one of --webapprt-content and --webapprt-chrome may be given.")
|
||||
|
||||
if options.jsdebugger:
|
||||
options.extraPrefs += [
|
||||
"devtools.debugger.remote-enabled=true",
|
||||
"devtools.debugger.chrome-enabled=true",
|
||||
"devtools.chrome.enabled=true",
|
||||
"devtools.debugger.prompt-connection=false"
|
||||
]
|
||||
options.autorun = False
|
||||
|
||||
# Try to guess the testing modules directory.
|
||||
# This somewhat grotesque hack allows the buildbot machines to find the
|
||||
# modules directory without having to configure the buildbot hosts. This
|
||||
|
@ -975,6 +975,9 @@ class Mochitest(MochitestUtilsMixin):
|
||||
options.browserArgs.extend(('-firefoxpath', options.app))
|
||||
options.app = self.immersiveHelperPath
|
||||
|
||||
if options.jsdebugger:
|
||||
options.browserArgs.extend(['-jsdebugger'])
|
||||
|
||||
# Remove the leak detection file so it can't "leak" to the tests run.
|
||||
# The file is not there if leak logging was not enabled in the application build.
|
||||
if os.path.exists(self.leak_report_file):
|
||||
|
@ -106,7 +106,7 @@ this.DownloadImport.prototype = {
|
||||
|
||||
let autoResume = false;
|
||||
try {
|
||||
autoResume = row.getResultByName("autoResume");
|
||||
autoResume = (row.getResultByName("autoResume") == 1);
|
||||
} catch (ex) {
|
||||
// autoResume wasn't present in schema version 7
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ this.DownloadIntegration = {
|
||||
* Returns the system downloads directory asynchronously.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The nsIFile of downloads directory.
|
||||
* @resolves The downloads directory string path.
|
||||
*/
|
||||
getSystemDownloadsDirectory: function DI_getSystemDownloadsDirectory() {
|
||||
return Task.spawn(function() {
|
||||
@ -267,41 +267,40 @@ this.DownloadIntegration = {
|
||||
throw new Task.Result(this._downloadsDirectory);
|
||||
}
|
||||
|
||||
let directory = null;
|
||||
let directoryPath = null;
|
||||
#ifdef XP_MACOSX
|
||||
directory = this._getDirectory("DfltDwnld");
|
||||
directoryPath = this._getDirectory("DfltDwnld");
|
||||
#elifdef XP_WIN
|
||||
// For XP/2K, use My Documents/Downloads. Other version uses
|
||||
// the default Downloads directory.
|
||||
let version = parseFloat(Services.sysinfo.getProperty("version"));
|
||||
if (version < 6) {
|
||||
directory = yield this._createDownloadsDirectory("Pers");
|
||||
directoryPath = yield this._createDownloadsDirectory("Pers");
|
||||
} else {
|
||||
directory = this._getDirectory("DfltDwnld");
|
||||
directoryPath = this._getDirectory("DfltDwnld");
|
||||
}
|
||||
#elifdef XP_UNIX
|
||||
#ifdef ANDROID
|
||||
// Android doesn't have a $HOME directory, and by default we only have
|
||||
// write access to /data/data/org.mozilla.{$APP} and /sdcard
|
||||
let directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
|
||||
directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
|
||||
if (!directoryPath) {
|
||||
throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
|
||||
Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
|
||||
}
|
||||
directory = new FileUtils.File(directoryPath);
|
||||
#else
|
||||
// For Linux, use XDG download dir, with a fallback to Home/Downloads
|
||||
// if the XDG user dirs are disabled.
|
||||
try {
|
||||
directory = this._getDirectory("DfltDwnld");
|
||||
directoryPath = this._getDirectory("DfltDwnld");
|
||||
} catch(e) {
|
||||
directory = yield this._createDownloadsDirectory("Home");
|
||||
directoryPath = yield this._createDownloadsDirectory("Home");
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
directory = yield this._createDownloadsDirectory("Home");
|
||||
directoryPath = yield this._createDownloadsDirectory("Home");
|
||||
#endif
|
||||
this._downloadsDirectory = directory;
|
||||
this._downloadsDirectory = directoryPath;
|
||||
throw new Task.Result(this._downloadsDirectory);
|
||||
}.bind(this));
|
||||
},
|
||||
@ -311,11 +310,11 @@ this.DownloadIntegration = {
|
||||
* Returns the user downloads directory asynchronously.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The nsIFile of downloads directory.
|
||||
* @resolves The downloads directory string path.
|
||||
*/
|
||||
getPreferredDownloadsDirectory: function DI_getPreferredDownloadsDirectory() {
|
||||
return Task.spawn(function() {
|
||||
let directory = null;
|
||||
let directoryPath = null;
|
||||
let prefValue = 1;
|
||||
|
||||
try {
|
||||
@ -324,25 +323,26 @@ this.DownloadIntegration = {
|
||||
|
||||
switch(prefValue) {
|
||||
case 0: // Desktop
|
||||
directory = this._getDirectory("Desk");
|
||||
directoryPath = this._getDirectory("Desk");
|
||||
break;
|
||||
case 1: // Downloads
|
||||
directory = yield this.getSystemDownloadsDirectory();
|
||||
directoryPath = yield this.getSystemDownloadsDirectory();
|
||||
break;
|
||||
case 2: // Custom
|
||||
try {
|
||||
directory = Services.prefs.getComplexValue("browser.download.dir",
|
||||
Ci.nsIFile);
|
||||
yield OS.File.makeDir(directory.path, { ignoreExisting: true });
|
||||
let directory = Services.prefs.getComplexValue("browser.download.dir",
|
||||
Ci.nsIFile);
|
||||
directoryPath = directory.path;
|
||||
yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
|
||||
} catch(ex) {
|
||||
// Either the preference isn't set or the directory cannot be created.
|
||||
directory = yield this.getSystemDownloadsDirectory();
|
||||
directoryPath = yield this.getSystemDownloadsDirectory();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
directory = yield this.getSystemDownloadsDirectory();
|
||||
directoryPath = yield this.getSystemDownloadsDirectory();
|
||||
}
|
||||
throw new Task.Result(directory);
|
||||
throw new Task.Result(directoryPath);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
@ -350,25 +350,25 @@ this.DownloadIntegration = {
|
||||
* Returns the temporary downloads directory asynchronously.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The nsIFile of downloads directory.
|
||||
* @resolves The downloads directory string path.
|
||||
*/
|
||||
getTemporaryDownloadsDirectory: function DI_getTemporaryDownloadsDirectory() {
|
||||
return Task.spawn(function() {
|
||||
let directory = null;
|
||||
let directoryPath = null;
|
||||
#ifdef XP_MACOSX
|
||||
directory = yield this.getPreferredDownloadsDirectory();
|
||||
directoryPath = yield this.getPreferredDownloadsDirectory();
|
||||
#elifdef ANDROID
|
||||
directory = yield this.getSystemDownloadsDirectory();
|
||||
directoryPath = yield this.getSystemDownloadsDirectory();
|
||||
#else
|
||||
// For Metro mode on Windows 8, we want searchability for documents
|
||||
// that the user chose to open with an external application.
|
||||
if (this._isImmersiveProcess()) {
|
||||
directory = yield this.getSystemDownloadsDirectory();
|
||||
directoryPath = yield this.getSystemDownloadsDirectory();
|
||||
} else {
|
||||
directory = this._getDirectory("TmpD");
|
||||
directoryPath = this._getDirectory("TmpD");
|
||||
}
|
||||
#endif
|
||||
throw new Task.Result(directory);
|
||||
throw new Task.Result(directoryPath);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
@ -649,20 +649,19 @@ this.DownloadIntegration = {
|
||||
* nsIFile for the downloads directory.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The nsIFile directory.
|
||||
* @resolves The directory string path.
|
||||
*/
|
||||
_createDownloadsDirectory: function DI_createDownloadsDirectory(aName) {
|
||||
let directory = this._getDirectory(aName);
|
||||
|
||||
// We read the name of the directory from the list of translated strings
|
||||
// that is kept by the UI helper module, even if this string is not strictly
|
||||
// displayed in the user interface.
|
||||
directory.append(DownloadUIHelper.strings.downloadsFolder);
|
||||
let directoryPath = OS.Path.join(this._getDirectory(aName),
|
||||
DownloadUIHelper.strings.downloadsFolder);
|
||||
|
||||
// Create the Downloads folder and ignore if it already exists.
|
||||
return OS.File.makeDir(directory.path, { ignoreExisting: true }).
|
||||
return OS.File.makeDir(directoryPath, { ignoreExisting: true }).
|
||||
then(function() {
|
||||
return directory;
|
||||
return directoryPath;
|
||||
});
|
||||
},
|
||||
|
||||
@ -670,10 +669,10 @@ this.DownloadIntegration = {
|
||||
* Calls the directory service and returns an nsIFile for the requested
|
||||
* location name.
|
||||
*
|
||||
* @return The nsIFile directory.
|
||||
* @return The directory string path.
|
||||
*/
|
||||
_getDirectory: function DI_getDirectory(aName) {
|
||||
return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile);
|
||||
return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile).path;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -260,7 +260,7 @@ this.Downloads = {
|
||||
* standard downloads directory i.e. /sdcard
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The nsIFile of downloads directory.
|
||||
* @resolves The downloads directory string path.
|
||||
*/
|
||||
getSystemDownloadsDirectory: function D_getSystemDownloadsDirectory() {
|
||||
return DownloadIntegration.getSystemDownloadsDirectory();
|
||||
@ -271,7 +271,7 @@ this.Downloads = {
|
||||
* in the current profile asynchronously.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The nsIFile of downloads directory.
|
||||
* @resolves The downloads directory string path.
|
||||
*/
|
||||
getPreferredDownloadsDirectory: function D_getPreferredDownloadsDirectory() {
|
||||
return DownloadIntegration.getPreferredDownloadsDirectory();
|
||||
@ -284,7 +284,7 @@ this.Downloads = {
|
||||
* directory, based on the platform asynchronously.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The nsIFile of downloads directory.
|
||||
* @resolves The downloads directory string path.
|
||||
*/
|
||||
getTemporaryDownloadsDirectory: function D_getTemporaryDownloadsDirectory() {
|
||||
return DownloadIntegration.getTemporaryDownloadsDirectory();
|
||||
|
@ -1625,9 +1625,10 @@ add_task(function test_platform_integration()
|
||||
// temporary directory or in the Downloads directory (such as setting
|
||||
// the Windows searchable attribute, and the Mac Downloads icon bouncing),
|
||||
// so use the system Downloads directory for the target file.
|
||||
let targetFile = yield DownloadIntegration.getSystemDownloadsDirectory();
|
||||
targetFile = targetFile.clone();
|
||||
targetFile.append("test" + (Math.floor(Math.random() * 1000000)));
|
||||
let targetFilePath = yield DownloadIntegration.getSystemDownloadsDirectory();
|
||||
targetFilePath = OS.Path.join(targetFilePath,
|
||||
"test" + (Math.floor(Math.random() * 1000000)));
|
||||
let targetFile = new FileUtils.File(targetFilePath);
|
||||
downloadFiles.push(targetFile);
|
||||
|
||||
let download;
|
||||
|
@ -465,6 +465,31 @@ function promiseStartExternalHelperAppServiceDownload(aSourceUrl) {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a download to finish, in case it has not finished already.
|
||||
*
|
||||
* @param aDownload
|
||||
* The Download object to wait upon.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the download has finished successfully.
|
||||
* @rejects JavaScript exception if the download failed.
|
||||
*/
|
||||
function promiseDownloadStopped(aDownload) {
|
||||
if (!aDownload.stopped) {
|
||||
// The download is in progress, wait for the current attempt to finish and
|
||||
// report any errors that may occur.
|
||||
return aDownload.start();
|
||||
}
|
||||
|
||||
if (aDownload.succeeded) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// The download failed or was canceled.
|
||||
return Promise.reject(aDownload.error || new Error("Download canceled."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a download to reach half of its progress, in case it has not
|
||||
* reached the expected progress already.
|
||||
|
701
toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
Normal file
701
toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
Normal file
@ -0,0 +1,701 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests the DownloadImport object.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||
"resource://gre/modules/Sqlite.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
|
||||
"resource://gre/modules/DownloadImport.jsm");
|
||||
|
||||
// Importable states
|
||||
const DOWNLOAD_NOTSTARTED = -1;
|
||||
const DOWNLOAD_DOWNLOADING = 0;
|
||||
const DOWNLOAD_PAUSED = 4;
|
||||
const DOWNLOAD_QUEUED = 5;
|
||||
|
||||
// Non importable states
|
||||
const DOWNLOAD_FAILED = 2;
|
||||
const DOWNLOAD_CANCELED = 3;
|
||||
const DOWNLOAD_BLOCKED_PARENTAL = 6;
|
||||
const DOWNLOAD_SCANNING = 7;
|
||||
const DOWNLOAD_DIRTY = 8;
|
||||
const DOWNLOAD_BLOCKED_POLICY = 9;
|
||||
|
||||
// The TEST_DATA_TAINTED const is a version of TEST_DATA_SHORT in which the
|
||||
// beginning of the data was changed (with the TEST_DATA_REPLACEMENT value).
|
||||
// We use this to test that the entityID is properly imported and the download
|
||||
// can be resumed from where it was paused.
|
||||
// For simplification purposes, the test requires that TEST_DATA_SHORT and
|
||||
// TEST_DATA_TAINTED have the same length.
|
||||
const TEST_DATA_REPLACEMENT = "-changed- ";
|
||||
const TEST_DATA_TAINTED = TEST_DATA_REPLACEMENT +
|
||||
TEST_DATA_SHORT.substr(TEST_DATA_REPLACEMENT.length);
|
||||
const TEST_DATA_LENGTH = TEST_DATA_SHORT.length;
|
||||
|
||||
// The length of the partial file that we'll write to disk as an existing
|
||||
// ongoing download.
|
||||
const TEST_DATA_PARTIAL_LENGTH = TEST_DATA_REPLACEMENT.length;
|
||||
|
||||
// The value of the "maxBytes" column stored in the DB about the downloads.
|
||||
// It's intentionally different than TEST_DATA_LENGTH to test that each value
|
||||
// is seen when expected.
|
||||
const MAXBYTES_IN_DB = TEST_DATA_LENGTH - 10;
|
||||
|
||||
let gDownloadsRowToImport;
|
||||
let gDownloadsRowNonImportable;
|
||||
|
||||
/**
|
||||
* Creates a database with an empty moz_downloads table and leaves an
|
||||
* open connection to it.
|
||||
*
|
||||
* @param aPath
|
||||
* String containing the path of the database file to be created.
|
||||
* @param aSchemaVersion
|
||||
* Number with the version of the database schema to set.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The open connection to the database.
|
||||
* @rejects If an error occurred during the database creation.
|
||||
*/
|
||||
function promiseEmptyDatabaseConnection({aPath, aSchemaVersion}) {
|
||||
return Task.spawn(function () {
|
||||
let connection = yield Sqlite.openConnection({ path: aPath });
|
||||
|
||||
yield connection.execute("CREATE TABLE moz_downloads ("
|
||||
+ "id INTEGER PRIMARY KEY,"
|
||||
+ "name TEXT,"
|
||||
+ "source TEXT,"
|
||||
+ "target TEXT,"
|
||||
+ "tempPath TEXT,"
|
||||
+ "startTime INTEGER,"
|
||||
+ "endTime INTEGER,"
|
||||
+ "state INTEGER,"
|
||||
+ "referrer TEXT,"
|
||||
+ "entityID TEXT,"
|
||||
+ "currBytes INTEGER NOT NULL DEFAULT 0,"
|
||||
+ "maxBytes INTEGER NOT NULL DEFAULT -1,"
|
||||
+ "mimeType TEXT,"
|
||||
+ "preferredApplication TEXT,"
|
||||
+ "preferredAction INTEGER NOT NULL DEFAULT 0,"
|
||||
+ "autoResume INTEGER NOT NULL DEFAULT 0,"
|
||||
+ "guid TEXT)");
|
||||
|
||||
yield connection.setSchemaVersion(aSchemaVersion);
|
||||
|
||||
throw new Task.Result(connection);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new entry in the database with the given columns' values.
|
||||
*
|
||||
* @param aConnection
|
||||
* The database connection.
|
||||
* @param aDownloadRow
|
||||
* An object representing the values for each column of the row
|
||||
* being inserted.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation completes.
|
||||
* @rejects If there's an error inserting the row.
|
||||
*/
|
||||
function promiseInsertRow(aConnection, aDownloadRow) {
|
||||
// We can't use the aDownloadRow obj directly in the execute statement
|
||||
// because the obj bind code in Sqlite.jsm doesn't allow objects
|
||||
// with extra properties beyond those being binded. So we might as well
|
||||
// use an array as it is simpler.
|
||||
let values = [
|
||||
aDownloadRow.source, aDownloadRow.target, aDownloadRow.tempPath,
|
||||
aDownloadRow.startTime.getTime() * 1000, aDownloadRow.state,
|
||||
aDownloadRow.referrer, aDownloadRow.entityID, aDownloadRow.maxBytes,
|
||||
aDownloadRow.mimeType, aDownloadRow.preferredApplication,
|
||||
aDownloadRow.preferredAction, aDownloadRow.autoResume
|
||||
];
|
||||
|
||||
return aConnection.execute("INSERT INTO moz_downloads ("
|
||||
+ "name, source, target, tempPath, startTime,"
|
||||
+ "endTime, state, referrer, entityID, currBytes,"
|
||||
+ "maxBytes, mimeType, preferredApplication,"
|
||||
+ "preferredAction, autoResume, guid)"
|
||||
+ "VALUES ("
|
||||
+ "'', ?, ?, ?, ?, " //name,
|
||||
+ "0, ?, ?, ?, 0, " //endTime, currBytes
|
||||
+ " ?, ?, ?, " //
|
||||
+ " ?, ?, '')", //and guid are not imported
|
||||
values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of rows in the moz_downloads table of the
|
||||
* database.
|
||||
*
|
||||
* @param aConnection
|
||||
* The database connection.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves With the number of rows.
|
||||
* @rejects Never.
|
||||
*/
|
||||
function promiseTableCount(aConnection) {
|
||||
return aConnection.execute("SELECT COUNT(*) FROM moz_downloads")
|
||||
.then(res => res[0].getResultByName("COUNT(*)"))
|
||||
.then(null, Cu.reportError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Briefly opens a network channel to a given URL to retrieve
|
||||
* the entityID of this url, as generated by the network code.
|
||||
*
|
||||
* @param aUrl
|
||||
* The URL to retrieve the entityID.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The EntityID of the given URL.
|
||||
* @rejects When there's a problem accessing the URL.
|
||||
*/
|
||||
function promiseEntityID(aUrl) {
|
||||
let deferred = Promise.defer();
|
||||
let entityID = "";
|
||||
let channel = NetUtil.newChannel(NetUtil.newURI(aUrl));
|
||||
|
||||
channel.asyncOpen({
|
||||
onStartRequest: function (aRequest) {
|
||||
if (aRequest instanceof Ci.nsIResumableChannel) {
|
||||
entityID = aRequest.entityID;
|
||||
}
|
||||
aRequest.cancel(Cr.NS_BINDING_ABORTED);
|
||||
},
|
||||
|
||||
onStopRequest: function (aRequest, aContext, aStatusCode) {
|
||||
if (aStatusCode == Cr.NS_BINDING_ABORTED) {
|
||||
deferred.resolve(entityID);
|
||||
} else {
|
||||
deferred.reject("Unexpected status code received");
|
||||
}
|
||||
},
|
||||
|
||||
onDataAvailable: function () {}
|
||||
}, null);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a file path to a temporary writeable download target, in the
|
||||
* correct format as expected to be stored in the downloads database,
|
||||
* which is file:///absolute/path/to/file
|
||||
*
|
||||
* @param aLeafName
|
||||
* A hint leaf name for the file.
|
||||
*
|
||||
* @return String The path to the download target.
|
||||
*/
|
||||
function getDownloadTarget(aLeafName) {
|
||||
return NetUtil.newURI(getTempFile(aLeafName)).spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a temporary partial file to use as an in-progress
|
||||
* download. The file is written to disk with a part of the total expected
|
||||
* download content pre-written.
|
||||
*
|
||||
* @param aLeafName
|
||||
* A hint leaf name for the file.
|
||||
* @param aTainted
|
||||
* A boolean value. When true, the partial content of the file
|
||||
* will be different from the expected content of the original source
|
||||
* file. See the declaration of TEST_DATA_TAINTED for more information.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation completes, and returns a string with the path
|
||||
* to the generated file.
|
||||
* @rejects If there's an error writing the file.
|
||||
*/
|
||||
function getPartialFile(aLeafName, aTainted = false) {
|
||||
let tempDownload = getTempFile(aLeafName);
|
||||
let partialContent = aTainted
|
||||
? TEST_DATA_TAINTED.substr(0, TEST_DATA_PARTIAL_LENGTH)
|
||||
: TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH);
|
||||
|
||||
return OS.File.writeAtomic(tempDownload.path, partialContent,
|
||||
{ tmpPath: tempDownload.path + ".tmp",
|
||||
flush: true })
|
||||
.then(() => tempDownload.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Date object to be used as the startTime for the download rows
|
||||
* in the DB. A date that is obviously different from the current time is
|
||||
* generated to make sure this stored data and a `new Date()` can't collide.
|
||||
*
|
||||
* @param aOffset
|
||||
* A offset from the base generated date is used to differentiate each
|
||||
* row in the database.
|
||||
*
|
||||
* @return A Date object.
|
||||
*/
|
||||
function getStartTime(aOffset) {
|
||||
return new Date(1000000 + (aOffset * 10000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs various checks on an imported Download object to make sure
|
||||
* all properties are properly set as expected from the import procedure.
|
||||
*
|
||||
* @param aDownload
|
||||
* The Download object to be checked.
|
||||
* @param aDownloadRow
|
||||
* An object that represents a row from the original database table,
|
||||
* with extra properties describing expected values that are not
|
||||
* explictly part of the database.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation completes
|
||||
* @rejects Never
|
||||
*/
|
||||
function checkDownload(aDownload, aDownloadRow) {
|
||||
return Task.spawn(function() {
|
||||
do_check_eq(aDownload.source.url, aDownloadRow.source);
|
||||
do_check_eq(aDownload.source.referrer, aDownloadRow.referrer);
|
||||
|
||||
do_check_eq(aDownload.target.path,
|
||||
NetUtil.newURI(aDownloadRow.target)
|
||||
.QueryInterface(Ci.nsIFileURL).file.path);
|
||||
|
||||
do_check_eq(aDownload.target.partFilePath, aDownloadRow.tempPath);
|
||||
|
||||
if (aDownloadRow.expectedResume) {
|
||||
do_check_true(!aDownload.stopped || aDownload.succeeded);
|
||||
yield promiseDownloadStopped(aDownload);
|
||||
|
||||
do_check_true(aDownload.succeeded);
|
||||
do_check_eq(aDownload.progress, 100);
|
||||
// If the download has resumed, a new startTime will be set.
|
||||
// By calling toJSON we're also testing that startTime is a Date object.
|
||||
do_check_neq(aDownload.startTime.toJSON(),
|
||||
aDownloadRow.startTime.toJSON());
|
||||
} else {
|
||||
do_check_false(aDownload.succeeded);
|
||||
do_check_eq(aDownload.startTime.toJSON(),
|
||||
aDownloadRow.startTime.toJSON());
|
||||
}
|
||||
|
||||
do_check_eq(aDownload.stopped, true);
|
||||
|
||||
let serializedSaver = aDownload.saver.toSerializable();
|
||||
if (typeof(serializedSaver) == "object") {
|
||||
do_check_eq(serializedSaver.type, "copy");
|
||||
} else {
|
||||
do_check_eq(serializedSaver, "copy");
|
||||
}
|
||||
|
||||
if (aDownloadRow.entityID) {
|
||||
do_check_eq(aDownload.saver.entityID, aDownloadRow.entityID);
|
||||
}
|
||||
|
||||
do_check_eq(aDownload.currentBytes, aDownloadRow.expectedCurrentBytes);
|
||||
do_check_eq(aDownload.totalBytes, aDownloadRow.expectedTotalBytes);
|
||||
|
||||
if (aDownloadRow.expectedContent) {
|
||||
let fileToCheck = aDownloadRow.expectedResume
|
||||
? aDownload.target.path
|
||||
: aDownload.target.partFilePath;
|
||||
yield promiseVerifyContents(fileToCheck, aDownloadRow.expectedContent);
|
||||
}
|
||||
|
||||
do_check_eq(aDownload.contentType, aDownloadRow.expectedContentType);
|
||||
do_check_eq(aDownload.launcherPath, aDownloadRow.preferredApplication);
|
||||
|
||||
do_check_eq(aDownload.launchWhenSucceeded,
|
||||
aDownloadRow.preferredAction != Ci.nsIMIMEInfo.saveToDisk);
|
||||
});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Preparation tasks
|
||||
|
||||
/**
|
||||
* Prepares the list of downloads to be added to the database that should
|
||||
* be imported by the import procedure.
|
||||
*/
|
||||
add_task(function prepareDownloadsToImport() {
|
||||
|
||||
let sourceUrl = httpUrl("source.txt");
|
||||
let sourceEntityId = yield promiseEntityID(sourceUrl);
|
||||
|
||||
gDownloadsRowToImport = [
|
||||
// Paused download with autoResume and a partial file. By
|
||||
// setting the correct entityID the download can resume from
|
||||
// where it stopped, and to test that this works properly we
|
||||
// intentionally set different data in the beginning of the
|
||||
// partial file to make sure it was not replaced.
|
||||
{
|
||||
source: sourceUrl,
|
||||
target: getDownloadTarget("inprogress1.txt"),
|
||||
tempPath: yield getPartialFile("inprogress1.txt.part", true),
|
||||
startTime: getStartTime(1),
|
||||
state: DOWNLOAD_PAUSED,
|
||||
referrer: httpUrl("referrer1"),
|
||||
entityID: sourceEntityId,
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType1",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication1",
|
||||
autoResume: 1,
|
||||
|
||||
// Even though the information stored in the DB said
|
||||
// maxBytes was MAXBYTES_IN_DB, the download turned out to be
|
||||
// a different length. Here we make sure the totalBytes property
|
||||
// was correctly set with the actual value. The same consideration
|
||||
// applies to the contentType.
|
||||
expectedCurrentBytes: TEST_DATA_LENGTH,
|
||||
expectedTotalBytes: TEST_DATA_LENGTH,
|
||||
expectedResume: true,
|
||||
expectedContentType: "text/plain",
|
||||
expectedContent: TEST_DATA_TAINTED,
|
||||
},
|
||||
|
||||
// Paused download with autoResume and a partial file,
|
||||
// but missing entityID. This means that the download will
|
||||
// start from beginning, and the entire original content of the
|
||||
// source file should replace the different data that was stored
|
||||
// in the partial file.
|
||||
{
|
||||
source: sourceUrl,
|
||||
target: getDownloadTarget("inprogress2.txt"),
|
||||
tempPath: yield getPartialFile("inprogress2.txt.part", true),
|
||||
startTime: getStartTime(2),
|
||||
state: DOWNLOAD_PAUSED,
|
||||
referrer: httpUrl("referrer2"),
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType2",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication2",
|
||||
autoResume: 1,
|
||||
|
||||
expectedCurrentBytes: TEST_DATA_LENGTH,
|
||||
expectedTotalBytes: TEST_DATA_LENGTH,
|
||||
expectedResume: true,
|
||||
expectedContentType: "text/plain",
|
||||
expectedContent: TEST_DATA_SHORT
|
||||
},
|
||||
|
||||
// Paused download with no autoResume and a partial file.
|
||||
{
|
||||
source: sourceUrl,
|
||||
target: getDownloadTarget("inprogress3.txt"),
|
||||
tempPath: yield getPartialFile("inprogress3.txt.part"),
|
||||
startTime: getStartTime(3),
|
||||
state: DOWNLOAD_PAUSED,
|
||||
referrer: httpUrl("referrer3"),
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType3",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication3",
|
||||
autoResume: 0,
|
||||
|
||||
// Since this download has not been resumed, the actual data
|
||||
// about its total size and content type is not known.
|
||||
// Therefore, we're going by the information imported from the DB.
|
||||
expectedCurrentBytes: TEST_DATA_PARTIAL_LENGTH,
|
||||
expectedTotalBytes: MAXBYTES_IN_DB,
|
||||
expectedResume: false,
|
||||
expectedContentType: "mimeType3",
|
||||
expectedContent: TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH),
|
||||
},
|
||||
|
||||
// Paused download with autoResume and no partial file.
|
||||
{
|
||||
source: sourceUrl,
|
||||
target: getDownloadTarget("inprogress4.txt"),
|
||||
tempPath: "",
|
||||
startTime: getStartTime(4),
|
||||
state: DOWNLOAD_PAUSED,
|
||||
referrer: httpUrl("referrer4"),
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "text/plain",
|
||||
preferredAction: Ci.nsIMIMEInfo.useHelperApp,
|
||||
preferredApplication: "prerredApplication4",
|
||||
autoResume: 1,
|
||||
|
||||
expectedCurrentBytes: TEST_DATA_LENGTH,
|
||||
expectedTotalBytes: TEST_DATA_LENGTH,
|
||||
expectedResume: true,
|
||||
expectedContentType: "text/plain",
|
||||
expectedContent: TEST_DATA_SHORT
|
||||
},
|
||||
|
||||
// Paused download with no autoResume and no partial file.
|
||||
{
|
||||
source: sourceUrl,
|
||||
target: getDownloadTarget("inprogress5.txt"),
|
||||
tempPath: "",
|
||||
startTime: getStartTime(5),
|
||||
state: DOWNLOAD_PAUSED,
|
||||
referrer: httpUrl("referrer4"),
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "text/plain",
|
||||
preferredAction: Ci.nsIMIMEInfo.useSystemDefault,
|
||||
preferredApplication: "prerredApplication5",
|
||||
autoResume: 0,
|
||||
|
||||
expectedCurrentBytes: 0,
|
||||
expectedTotalBytes: MAXBYTES_IN_DB,
|
||||
expectedResume: false,
|
||||
expectedContentType: "text/plain",
|
||||
},
|
||||
|
||||
// Queued download with no autoResume and no partial file.
|
||||
// Even though autoResume=0, queued downloads always autoResume.
|
||||
{
|
||||
source: sourceUrl,
|
||||
target: getDownloadTarget("inprogress6.txt"),
|
||||
tempPath: "",
|
||||
startTime: getStartTime(6),
|
||||
state: DOWNLOAD_QUEUED,
|
||||
referrer: httpUrl("referrer6"),
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "text/plain",
|
||||
preferredAction: Ci.nsIMIMEInfo.useHelperApp,
|
||||
preferredApplication: "prerredApplication6",
|
||||
autoResume: 0,
|
||||
|
||||
expectedCurrentBytes: TEST_DATA_LENGTH,
|
||||
expectedTotalBytes: TEST_DATA_LENGTH,
|
||||
expectedResume: true,
|
||||
expectedContentType: "text/plain",
|
||||
expectedContent: TEST_DATA_SHORT
|
||||
},
|
||||
|
||||
// Notstarted download with no autoResume and no partial file.
|
||||
// Even though autoResume=0, notstarted downloads always autoResume.
|
||||
{
|
||||
source: sourceUrl,
|
||||
target: getDownloadTarget("inprogress7.txt"),
|
||||
tempPath: "",
|
||||
startTime: getStartTime(7),
|
||||
state: DOWNLOAD_NOTSTARTED,
|
||||
referrer: httpUrl("referrer7"),
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "text/plain",
|
||||
preferredAction: Ci.nsIMIMEInfo.useHelperApp,
|
||||
preferredApplication: "prerredApplication7",
|
||||
autoResume: 0,
|
||||
|
||||
expectedCurrentBytes: TEST_DATA_LENGTH,
|
||||
expectedTotalBytes: TEST_DATA_LENGTH,
|
||||
expectedResume: true,
|
||||
expectedContentType: "text/plain",
|
||||
expectedContent: TEST_DATA_SHORT
|
||||
},
|
||||
|
||||
// Downloading download with no autoResume and a partial file.
|
||||
// Even though autoResume=0, downloading downloads always autoResume.
|
||||
{
|
||||
source: sourceUrl,
|
||||
target: getDownloadTarget("inprogress8.txt"),
|
||||
tempPath: yield getPartialFile("inprogress8.txt.part", true),
|
||||
startTime: getStartTime(8),
|
||||
state: DOWNLOAD_DOWNLOADING,
|
||||
referrer: httpUrl("referrer8"),
|
||||
entityID: sourceEntityId,
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "text/plain",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication8",
|
||||
autoResume: 0,
|
||||
|
||||
expectedCurrentBytes: TEST_DATA_LENGTH,
|
||||
expectedTotalBytes: TEST_DATA_LENGTH,
|
||||
expectedResume: true,
|
||||
expectedContentType: "text/plain",
|
||||
expectedContent: TEST_DATA_TAINTED
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Prepares the list of downloads to be added to the database that should
|
||||
* *not* be imported by the import procedure.
|
||||
*/
|
||||
add_task(function prepareNonImportableDownloads()
|
||||
{
|
||||
gDownloadsRowNonImportable = [
|
||||
// Download with no source (should never happen in normal circumstances).
|
||||
{
|
||||
source: "",
|
||||
target: "nonimportable1.txt",
|
||||
tempPath: "",
|
||||
startTime: getStartTime(1),
|
||||
state: DOWNLOAD_PAUSED,
|
||||
referrer: "",
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType1",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication1",
|
||||
autoResume: 1
|
||||
},
|
||||
|
||||
// state = DOWNLOAD_FAILED
|
||||
{
|
||||
source: httpUrl("source.txt"),
|
||||
target: "nonimportable2.txt",
|
||||
tempPath: "",
|
||||
startTime: getStartTime(2),
|
||||
state: DOWNLOAD_FAILED,
|
||||
referrer: "",
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType2",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication2",
|
||||
autoResume: 1
|
||||
},
|
||||
|
||||
// state = DOWNLOAD_CANCELED
|
||||
{
|
||||
source: httpUrl("source.txt"),
|
||||
target: "nonimportable3.txt",
|
||||
tempPath: "",
|
||||
startTime: getStartTime(3),
|
||||
state: DOWNLOAD_CANCELED,
|
||||
referrer: "",
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType3",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication3",
|
||||
autoResume: 1
|
||||
},
|
||||
|
||||
// state = DOWNLOAD_BLOCKED_PARENTAL
|
||||
{
|
||||
source: httpUrl("source.txt"),
|
||||
target: "nonimportable4.txt",
|
||||
tempPath: "",
|
||||
startTime: getStartTime(4),
|
||||
state: DOWNLOAD_BLOCKED_PARENTAL,
|
||||
referrer: "",
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType4",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication4",
|
||||
autoResume: 1
|
||||
},
|
||||
|
||||
// state = DOWNLOAD_SCANNING
|
||||
{
|
||||
source: httpUrl("source.txt"),
|
||||
target: "nonimportable5.txt",
|
||||
tempPath: "",
|
||||
startTime: getStartTime(5),
|
||||
state: DOWNLOAD_SCANNING,
|
||||
referrer: "",
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType5",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication5",
|
||||
autoResume: 1
|
||||
},
|
||||
|
||||
// state = DOWNLOAD_DIRTY
|
||||
{
|
||||
source: httpUrl("source.txt"),
|
||||
target: "nonimportable6.txt",
|
||||
tempPath: "",
|
||||
startTime: getStartTime(6),
|
||||
state: DOWNLOAD_DIRTY,
|
||||
referrer: "",
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType6",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication6",
|
||||
autoResume: 1
|
||||
},
|
||||
|
||||
// state = DOWNLOAD_BLOCKED_POLICY
|
||||
{
|
||||
source: httpUrl("source.txt"),
|
||||
target: "nonimportable7.txt",
|
||||
tempPath: "",
|
||||
startTime: getStartTime(7),
|
||||
state: DOWNLOAD_BLOCKED_POLICY,
|
||||
referrer: "",
|
||||
entityID: "",
|
||||
maxBytes: MAXBYTES_IN_DB,
|
||||
mimeType: "mimeType7",
|
||||
preferredAction: Ci.nsIMIMEInfo.saveToDisk,
|
||||
preferredApplication: "prerredApplication7",
|
||||
autoResume: 1
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Test
|
||||
|
||||
/**
|
||||
* Creates a temporary Sqlite database with download data and perform an
|
||||
* import of that data to the new Downloads API to verify that the import
|
||||
* worked correctly.
|
||||
*/
|
||||
add_task(function test_downloadImport()
|
||||
{
|
||||
let connection = null;
|
||||
let downloadsSqlite = getTempFile("downloads.sqlite").path;
|
||||
|
||||
try {
|
||||
// Set up the database.
|
||||
connection = yield promiseEmptyDatabaseConnection({
|
||||
aPath: downloadsSqlite,
|
||||
aSchemaVersion: 9
|
||||
});
|
||||
|
||||
// Insert both the importable and non-importable
|
||||
// downloads together.
|
||||
for (let downloadRow of gDownloadsRowToImport) {
|
||||
yield promiseInsertRow(connection, downloadRow);
|
||||
}
|
||||
|
||||
for (let downloadRow of gDownloadsRowNonImportable) {
|
||||
yield promiseInsertRow(connection, downloadRow);
|
||||
}
|
||||
|
||||
// Check that every item was inserted.
|
||||
do_check_eq((yield promiseTableCount(connection)),
|
||||
gDownloadsRowToImport.length +
|
||||
gDownloadsRowNonImportable.length);
|
||||
} finally {
|
||||
// Close the connection so that DownloadImport can open it.
|
||||
yield connection.close();
|
||||
}
|
||||
|
||||
// Import items.
|
||||
let list = yield promiseNewList(false);
|
||||
yield new DownloadImport(list, downloadsSqlite).import();
|
||||
let items = yield list.getAll();
|
||||
|
||||
do_check_eq(items.length, gDownloadsRowToImport.length);
|
||||
|
||||
for (let i = 0; i < gDownloadsRowToImport.length; i++) {
|
||||
yield checkDownload(items[i], gDownloadsRowToImport[i]);
|
||||
}
|
||||
})
|
@ -65,8 +65,8 @@ XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that the getSystemDownloadsDirectory returns a valid nsFile
|
||||
* download directory object.
|
||||
* Tests that the getSystemDownloadsDirectory returns a valid download
|
||||
* directory string path.
|
||||
*/
|
||||
add_task(function test_getSystemDownloadsDirectory()
|
||||
{
|
||||
@ -88,11 +88,10 @@ add_task(function test_getSystemDownloadsDirectory()
|
||||
(Services.appinfo.OS == "WINNT" &&
|
||||
parseFloat(Services.sysinfo.getProperty("version")) >= 6)) {
|
||||
downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_eq(downloadDir.path, tempDir.path);
|
||||
do_check_true(yield OS.File.exists(downloadDir.path));
|
||||
do_check_eq(downloadDir, tempDir.path);
|
||||
do_check_true(yield OS.File.exists(downloadDir));
|
||||
|
||||
let info = yield OS.File.stat(downloadDir.path);
|
||||
let info = yield OS.File.stat(downloadDir);
|
||||
do_check_true(info.isDir);
|
||||
} else {
|
||||
let targetPath = OS.Path.join(tempDir.path,
|
||||
@ -101,10 +100,10 @@ add_task(function test_getSystemDownloadsDirectory()
|
||||
yield OS.File.removeEmptyDir(targetPath);
|
||||
} catch(e) {}
|
||||
downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
|
||||
do_check_eq(downloadDir.path, targetPath);
|
||||
do_check_true(yield OS.File.exists(downloadDir.path));
|
||||
do_check_eq(downloadDir, targetPath);
|
||||
do_check_true(yield OS.File.exists(downloadDir));
|
||||
|
||||
let info = yield OS.File.stat(downloadDir.path);
|
||||
let info = yield OS.File.stat(downloadDir);
|
||||
do_check_true(info.isDir);
|
||||
yield OS.File.removeEmptyDir(targetPath);
|
||||
}
|
||||
@ -112,12 +111,12 @@ add_task(function test_getSystemDownloadsDirectory()
|
||||
let downloadDirBefore = yield DownloadIntegration.getSystemDownloadsDirectory();
|
||||
cleanup();
|
||||
let downloadDirAfter = yield DownloadIntegration.getSystemDownloadsDirectory();
|
||||
do_check_false(downloadDirBefore.equals(downloadDirAfter));
|
||||
do_check_neq(downloadDirBefore, downloadDirAfter);
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that the getPreferredDownloadsDirectory returns a valid nsFile
|
||||
* download directory object.
|
||||
* Tests that the getPreferredDownloadsDirectory returns a valid download
|
||||
* directory string path.
|
||||
*/
|
||||
add_task(function test_getPreferredDownloadsDirectory()
|
||||
{
|
||||
@ -133,21 +132,21 @@ add_task(function test_getPreferredDownloadsDirectory()
|
||||
Services.prefs.setIntPref(folderListPrefName, 1);
|
||||
let systemDir = yield DownloadIntegration.getSystemDownloadsDirectory();
|
||||
let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_eq(downloadDir.path, systemDir.path);
|
||||
do_check_neq(downloadDir, "");
|
||||
do_check_eq(downloadDir, systemDir);
|
||||
|
||||
// Should return the desktop directory.
|
||||
Services.prefs.setIntPref(folderListPrefName, 0);
|
||||
downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_eq(downloadDir.path, Services.dirsvc.get("Desk", Ci.nsIFile).path);
|
||||
do_check_neq(downloadDir, "");
|
||||
do_check_eq(downloadDir, Services.dirsvc.get("Desk", Ci.nsIFile).path);
|
||||
|
||||
// Should return the system downloads directory because the dir preference
|
||||
// is not set.
|
||||
Services.prefs.setIntPref(folderListPrefName, 2);
|
||||
let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_eq(downloadDir.path, systemDir.path);
|
||||
do_check_neq(downloadDir, "");
|
||||
do_check_eq(downloadDir, systemDir);
|
||||
|
||||
// Should return the directory which is listed in the dir preference.
|
||||
let time = (new Date()).getTime();
|
||||
@ -155,9 +154,9 @@ add_task(function test_getPreferredDownloadsDirectory()
|
||||
tempDir.append(time);
|
||||
Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
|
||||
downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_eq(downloadDir.path, tempDir.path);
|
||||
do_check_true(yield OS.File.exists(downloadDir.path));
|
||||
do_check_neq(downloadDir, "");
|
||||
do_check_eq(downloadDir, tempDir.path);
|
||||
do_check_true(yield OS.File.exists(downloadDir));
|
||||
yield OS.File.removeEmptyDir(tempDir.path);
|
||||
|
||||
// Should return the system downloads directory beacause the path is invalid
|
||||
@ -167,32 +166,32 @@ add_task(function test_getPreferredDownloadsDirectory()
|
||||
tempDir.append(time);
|
||||
Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
|
||||
downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
|
||||
do_check_eq(downloadDir.path, systemDir.path);
|
||||
do_check_eq(downloadDir, systemDir);
|
||||
|
||||
// Should return the system downloads directory because the folderList
|
||||
// preference is invalid
|
||||
Services.prefs.setIntPref(folderListPrefName, 999);
|
||||
let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
|
||||
do_check_eq(downloadDir.path, systemDir.path);
|
||||
do_check_eq(downloadDir, systemDir);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that the getTemporaryDownloadsDirectory returns a valid nsFile
|
||||
* download directory object.
|
||||
* Tests that the getTemporaryDownloadsDirectory returns a valid download
|
||||
* directory string path.
|
||||
*/
|
||||
add_task(function test_getTemporaryDownloadsDirectory()
|
||||
{
|
||||
let downloadDir = yield DownloadIntegration.getTemporaryDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_neq(downloadDir, "");
|
||||
|
||||
if ("nsILocalFileMac" in Ci) {
|
||||
let preferredDownloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
|
||||
do_check_eq(downloadDir.path, preferredDownloadDir.path);
|
||||
do_check_eq(downloadDir, preferredDownloadDir);
|
||||
} else {
|
||||
let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
||||
do_check_eq(downloadDir.path, tempDir.path);
|
||||
do_check_eq(downloadDir, tempDir.path);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -136,33 +136,33 @@ add_task(function test_getSummary()
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that the getSystemDownloadsDirectory returns a valid nsFile
|
||||
* download directory object.
|
||||
* Tests that the getSystemDownloadsDirectory returns a non-empty download
|
||||
* directory string.
|
||||
*/
|
||||
add_task(function test_getSystemDownloadsDirectory()
|
||||
{
|
||||
let downloadDir = yield Downloads.getSystemDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_neq(downloadDir, "");
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that the getPreferredDownloadsDirectory returns a valid nsFile
|
||||
* download directory object.
|
||||
* Tests that the getPreferredDownloadsDirectory returns a non-empty download
|
||||
* directory string.
|
||||
*/
|
||||
add_task(function test_getPreferredDownloadsDirectory()
|
||||
{
|
||||
let downloadDir = yield Downloads.getPreferredDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_neq(downloadDir, "");
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that the getTemporaryDownloadsDirectory returns a valid nsFile
|
||||
* download directory object.
|
||||
* Tests that the getTemporaryDownloadsDirectory returns a non-empty download
|
||||
* directory string.
|
||||
*/
|
||||
add_task(function test_getTemporaryDownloadsDirectory()
|
||||
{
|
||||
let downloadDir = yield Downloads.getTemporaryDownloadsDirectory();
|
||||
do_check_true(downloadDir instanceof Ci.nsIFile);
|
||||
do_check_neq(downloadDir, "");
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -1,12 +1,15 @@
|
||||
[DEFAULT]
|
||||
head = head.js
|
||||
tail =
|
||||
|
||||
# Note: The "tail.js" file is not defined in the "tail" key because it calls
|
||||
# the "add_test_task" function, that does not work properly in tail files.
|
||||
support-files =
|
||||
common_test_Download.js
|
||||
# tail.js should quite possibly be in the tail key.
|
||||
tail.js
|
||||
|
||||
[test_DownloadCore.js]
|
||||
[test_DownloadImport.js]
|
||||
[test_DownloadIntegration.js]
|
||||
[test_DownloadLegacy.js]
|
||||
[test_DownloadList.js]
|
||||
|
@ -71,6 +71,11 @@ function _Worker(browserPromise, options) {
|
||||
this.options = options;
|
||||
this.ports = new Map();
|
||||
browserPromise.then(browser => {
|
||||
browser.addEventListener("oop-browser-crashed", () => {
|
||||
Cu.reportError("FrameWorker remote process crashed");
|
||||
notifyWorkerError(options.origin);
|
||||
});
|
||||
|
||||
let mm = browser.messageManager;
|
||||
// execute the content script and send the message to bootstrap the content
|
||||
// side of the world.
|
||||
|
@ -2576,13 +2576,6 @@
|
||||
"n_buckets": 20,
|
||||
"description": "Firefox: Time in ms spent on switching tabs in response to a tab click"
|
||||
},
|
||||
"FX_IDENTITY_POPUP_OPEN_MS": {
|
||||
"kind": "exponential",
|
||||
"high": "1000",
|
||||
"n_buckets": 10,
|
||||
"extended_statistics_ok": true,
|
||||
"description": "Firefox: Time taken by the identity popup to open in milliseconds"
|
||||
},
|
||||
"FX_APP_MENU_OPEN_MS": {
|
||||
"kind": "exponential",
|
||||
"high": "1000",
|
||||
|
@ -23,5 +23,6 @@ this.DevToolsUtils = {
|
||||
reportException: reportException,
|
||||
makeInfallible: makeInfallible,
|
||||
yieldingEach: yieldingEach,
|
||||
reportingDisabled: false , // Used by tests.
|
||||
defineLazyPrototypeGetter: defineLazyPrototypeGetter
|
||||
};
|
||||
|
@ -24,7 +24,8 @@ Cu.import("resource://gre/modules/devtools/SourceMap.jsm", SourceMap);
|
||||
let loader = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}).Loader;
|
||||
let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools"];
|
||||
this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider",
|
||||
"SrcdirProvider"];
|
||||
|
||||
/**
|
||||
* Providers are different strategies for loading the devtools.
|
||||
@ -104,6 +105,7 @@ var SrcdirProvider = {
|
||||
let appActorURI = this.fileURI(OS.Path.join(toolkitDir, "apps", "app-actor-front.js"));
|
||||
let cssLogicURI = this.fileURI(OS.Path.join(toolkitDir, "styleinspector", "css-logic"));
|
||||
let cssColorURI = this.fileURI(OS.Path.join(toolkitDir, "css-color"));
|
||||
let outputParserURI = this.fileURI(OS.Path.join(toolkitDir, "output-parser"));
|
||||
let touchEventsURI = this.fileURI(OS.Path.join(toolkitDir, "touch-events"));
|
||||
let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client"));
|
||||
let escodegenURI = this.fileURI(OS.Path.join(toolkitDir, "escodegen"));
|
||||
@ -122,6 +124,7 @@ var SrcdirProvider = {
|
||||
"devtools/app-actor-front": appActorURI,
|
||||
"devtools/styleinspector/css-logic": cssLogicURI,
|
||||
"devtools/css-color": cssColorURI,
|
||||
"devtools/output-parser": outputParserURI,
|
||||
"devtools/touch-events": touchEventsURI,
|
||||
"devtools/client": clientURI,
|
||||
"escodegen": escodegenURI,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user