Merge Fx-Team to Mozilla-Central

This commit is contained in:
Carsten "Tomcat" Book 2013-10-25 11:21:15 +02:00
commit d7e4c57fbb
127 changed files with 3638 additions and 647 deletions

View File

@ -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).

View File

@ -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();

View File

@ -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();
}

View File

@ -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]

View 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);
}
}

View File

@ -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();

View File

@ -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();

View File

@ -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);
}
}
};

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -2,5 +2,6 @@
support-files =
head.js
hosted_app.manifest
manifest.webapp
[browser_manifest_editor.js]

View File

@ -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();

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
{
"name": "My packaged app"
}

View File

@ -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 {

View File

@ -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
});

View File

@ -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
});
}

View File

@ -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) {

View File

@ -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]

View File

@ -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],

View 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;
});

View 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

View 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>

View File

@ -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.
*/

View File

@ -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;"

View File

@ -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)

View 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);
}
};

View File

@ -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) */

View File

@ -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;

View File

@ -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">

View File

@ -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]

View File

@ -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();
}
}

View File

@ -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() {

View File

@ -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");

View File

@ -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,

View File

@ -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.");
});
}

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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.");
});
}

View File

@ -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.");
});
}

View File

@ -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

View 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);
});
};
}
})();

View File

@ -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;
}

View File

@ -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; });

View File

@ -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;

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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");

View File

@ -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;
}
}

View File

@ -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

View File

@ -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]

View File

@ -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");
}

View File

@ -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()

View File

@ -9,7 +9,8 @@ let prefs = {
],
"css": [
"csserror",
"cssparser"
"cssparser",
"csslog"
],
"js": [
"exception",

View File

@ -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");

View File

@ -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);
}

View File

@ -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);

View File

@ -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"

View File

@ -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)

View File

@ -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@

View File

@ -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">

View File

@ -1,3 +1,6 @@
<!ENTITY inspectorHTMLEdit.label "Edit As HTML">
<!ENTITY inspectorHTMLEdit.accesskey "E">
<!ENTITY inspectorHTMLCopyInner.label "Copy Inner HTML">
<!ENTITY inspectorHTMLCopyInner.accesskey "I">

View File

@ -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.

View File

@ -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);

View File

@ -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;

View File

@ -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 });
}
};

View File

@ -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");

View File

@ -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 {

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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) {

View File

@ -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());
}

View File

@ -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();

View File

@ -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);

View File

@ -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 */

View File

@ -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);
},

View File

@ -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);
}

View File

@ -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 ' \

View File

@ -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

View File

@ -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):

View 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
}

View File

@ -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;
},
/**

View File

@ -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();

View File

@ -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;

View File

@ -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.

View 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]);
}
})

View File

@ -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);
}
});

View File

@ -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, "");
});
////////////////////////////////////////////////////////////////////////////////

View File

@ -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]

View File

@ -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.

View File

@ -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",

View File

@ -23,5 +23,6 @@ this.DevToolsUtils = {
reportException: reportException,
makeInfallible: makeInfallible,
yieldingEach: yieldingEach,
reportingDisabled: false , // Used by tests.
defineLazyPrototypeGetter: defineLazyPrototypeGetter
};

View File

@ -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