Merge m-c to inbound, a=merge

MozReview-Commit-ID: 5AQXGbI0ke2
This commit is contained in:
Wes Kocher 2016-04-21 15:02:19 -07:00
commit e80ed17c41
197 changed files with 4180 additions and 1411 deletions

View File

@ -46,7 +46,7 @@ pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCAL
pref("extensions.getAddons.getWithPerformance.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/guid:%IDS%?src=firefox&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/firefox/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%?src=firefox");
pref("extensions.webservice.discoverURL", "https://services.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%/%COMPATIBILITY_MODE%");
pref("extensions.webservice.discoverURL", "https://discovery.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%/%COMPATIBILITY_MODE%");
pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/%APP%/api/%API_VERSION%/list/recommended/all/%MAX_RESULTS%/%OS%/%VERSION%?src=firefox");
pref("extensions.getAddons.link.url", "https://addons.mozilla.org/%LOCALE%/firefox/");
@ -68,6 +68,12 @@ pref("services.kinto.changes.path", "/buckets/monitor/collections/changes/record
pref("services.kinto.bucket", "blocklists");
pref("services.kinto.onecrl.collection", "certificates");
pref("services.kinto.onecrl.checked", 0);
pref("services.kinto.addons.collection", "addons");
pref("services.kinto.addons.checked", 0);
pref("services.kinto.plugins.collection", "plugins");
pref("services.kinto.plugins.checked", 0);
pref("services.kinto.gfx.collection", "gfx");
pref("services.kinto.gfx.checked", 0);
// for now, let's keep kinto update out of the release channel
#ifdef RELEASE_BUILD

View File

@ -7,6 +7,7 @@
@namespace svg url("http://www.w3.org/2000/svg");
:root {
--identity-popup-expander-width: 38px;
--panelui-subview-transition-duration: 150ms;
}

View File

@ -6487,10 +6487,13 @@ var gIdentityHandler = {
delete this._identityBox;
return this._identityBox = document.getElementById("identity-box");
},
get _identityPopupContentHost () {
delete this._identityPopupContentHost;
return this._identityPopupContentHost =
document.getElementById("identity-popup-content-host");
get _identityPopupContentHosts () {
delete this._identityPopupContentHosts;
return this._identityPopupContentHosts = [...document.querySelectorAll(".identity-popup-headline.host")];
},
get _identityPopupContentHostless () {
delete this._identityPopupContentHostless;
return this._identityPopupContentHostless = [...document.querySelectorAll(".identity-popup-headline.hostless")];
},
get _identityPopupContentOwner () {
delete this._identityPopupContentOwner;
@ -6933,7 +6936,7 @@ var gIdentityHandler = {
let verifier = "";
let host = "";
let owner = "";
let crop = "start";
let hostless = false;
try {
host = this.getEffectiveHost();
@ -6946,7 +6949,7 @@ var gIdentityHandler = {
host = this._uri.specIgnoringRef;
// Special URIs without a host (eg, about:) should crop the end so
// the protocol can be seen.
crop = "end";
hostless = true;
}
// Fill in the CA name if we have a valid TLS certificate.
@ -6956,8 +6959,6 @@ var gIdentityHandler = {
// Fill in organization information if we have a valid EV certificate.
if (this._isEV) {
crop = "end";
let iData = this.getIdentityData();
host = owner = iData.subjectOrg;
verifier = this._identityBox.tooltipText;
@ -6974,11 +6975,15 @@ var gIdentityHandler = {
supplemental += iData.country;
}
// Push the appropriate strings out to the UI. Need to use |value| for the
// host as it's a <label> that will be cropped if too long. Using
// |textContent| would simply wrap the value.
this._identityPopupContentHost.setAttribute("crop", crop);
this._identityPopupContentHost.setAttribute("value", host);
// Push the appropriate strings out to the UI.
this._identityPopupContentHosts.forEach((el) => {
el.textContent = host;
el.hidden = hostless;
});
this._identityPopupContentHostless.forEach((el) => {
el.setAttribute("value", host);
el.hidden = !hostless;
});
this._identityPopupContentOwner.textContent = owner;
this._identityPopupContentSupp.textContent = supplemental;
this._identityPopupContentVerif.textContent = verifier;

View File

@ -854,7 +854,7 @@
<menuitem label="&recentBookmarks.label;"
id="BMB_recentBookmarks"
disabled="true"
class="subviewbutton"/>
class="menuitem-iconic subviewbutton"/>
<menuseparator/>
<menu id="BMB_bookmarksToolbar"
class="menu-iconic bookmark-item subviewbutton"

View File

@ -26,7 +26,7 @@
onclick="checkForMiddleClick(this, event);"/>
<menuitem id="menu_HelpPopup_reportPhishingErrortoolmenu"
label="&safeb.palm.notdeceptive.label;"
accesskey="&reportDeceptiveSiteMenu.accesskey;"
accesskey="&safeb.palm.notdeceptive.accesskey;"
insertbefore="aboutSeparator"
observes="reportPhishingErrorBroadcaster"
oncommand="openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');"

View File

@ -21,6 +21,7 @@ support-files =
browser_tab_dragdrop2_frame1.xul
browser_web_channel.html
browser_web_channel_iframe.html
bug1262648_string_with_newlines.dtd
bug592338.html
bug792517-2.html
bug792517.html

View File

@ -127,19 +127,32 @@ add_task(function* checkAllTheProperties() {
}
});
var checkDTD = Task.async(function* (aURISpec) {
let rawContents = yield fetchFile(aURISpec);
// The regular expression below is adapted from:
// https://hg.mozilla.org/mozilla-central/file/68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8/python/compare-locales/compare_locales/parser.py#l233
let entities = rawContents.match(/<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/g);
for (let entity of entities) {
let [, key, str] = entity.match(/<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/);
// The matched string includes the enclosing quotation marks,
// we need to slice them off.
str = str.slice(1, -1);
testForErrors(aURISpec, key, str);
}
});
add_task(function* checkAllTheDTDs() {
let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
let uris = yield generateURIsFromDirTree(appDir, [".dtd"]);
ok(uris.length, `Found ${uris.length} .dtd files to scan for misused characters`);
for (let uri of uris) {
let rawContents = yield fetchFile(uri.spec);
let entities = rawContents.match(/ENTITY\s+([\w\.]*)\s+["'](.*)["']/g);
for (let entity of entities) {
let [, key, str] = entity.match(/ENTITY\s+([\w\.]*)\s+["'](.*)["']/);
testForErrors(uri.spec, key, str);
}
yield checkDTD(uri.spec);
}
// This support DTD file supplies a string with a newline to make sure
// the regex in checkDTD works correctly for that case.
let dtdLocation = gTestPath.replace(/\/[^\/]*$/i, "/bug1262648_string_with_newlines.dtd");
yield checkDTD(dtdLocation);
});
add_task(function* ensureWhiteListIsEmpty() {

View File

@ -2,19 +2,18 @@
* 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/. */
var {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", null);
function createTemporarySaveDirectory() {
var saveDir = Cc["@mozilla.org/file/directory_service;1"]
.getService(Ci.nsIProperties)
.get("TmpD", Ci.nsIFile);
saveDir.append("testsavedir");
if (!saveDir.exists())
saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
return saveDir;
}
function test() {
// initialization
waitForExplicitFinish();
let windowsToClose = [];
let testURI = "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.html";
let fileName;
let MockFilePicker = SpecialPowers.MockFilePicker;
let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
.getService(Ci.nsICacheStorageService);
function checkDiskCacheFor(filename, goon) {
function promiseNoCacheEntry(filename) {
return new Promise((resolve, reject) => {
Visitor.prototype = {
onCacheStorageInfo: function(num, consumption)
{
@ -22,118 +21,95 @@ function test() {
},
onCacheEntryInfo: function(uri)
{
var urispec = uri.asciiSpec;
let urispec = uri.asciiSpec;
info(urispec);
is(urispec.includes(filename), false, "web content present in disk cache");
},
onCacheEntryVisitCompleted: function()
{
goon();
resolve();
}
};
function Visitor() {}
var storage = cache.diskCacheStorage(LoadContextInfo.default, false);
let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
.getService(Ci.nsICacheStorageService);
let {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", null);
let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
storage.asyncVisitStorage(new Visitor(), true /* Do walk entries */);
}
function onTransferComplete(downloadSuccess) {
ok(downloadSuccess, "Image file should have been downloaded successfully");
// Give the request a chance to finish and create a cache entry
executeSoon(function() {
checkDiskCacheFor(fileName, finish);
mockTransferCallback = null;
});
}
function createTemporarySaveDirectory() {
var saveDir = Cc["@mozilla.org/file/directory_service;1"]
.getService(Ci.nsIProperties)
.get("TmpD", Ci.nsIFile);
saveDir.append("testsavedir");
if (!saveDir.exists())
saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
return saveDir;
}
function doTest(aIsPrivateMode, aWindow, aCallback) {
function contextMenuOpened(event) {
cache.clear();
aWindow.document.removeEventListener("popupshown", contextMenuOpened);
// Create the folder the image will be saved into.
var destDir = createTemporarySaveDirectory();
var destFile = destDir.clone();
MockFilePicker.displayDirectory = destDir;
MockFilePicker.showCallback = function(fp) {
fileName = fp.defaultString;
destFile.append (fileName);
MockFilePicker.returnFiles = [destFile];
MockFilePicker.filterIndex = 1; // kSaveAsType_URL
};
mockTransferCallback = onTransferComplete;
mockTransferRegisterer.register();
registerCleanupFunction(function () {
mockTransferRegisterer.unregister();
MockFilePicker.cleanup();
destDir.remove(true);
});
// Select "Save Image As" option from context menu
var saveVideoCommand = aWindow.document.getElementById("context-saveimage");
saveVideoCommand.doCommand();
event.target.hidePopup();
}
aWindow.gBrowser.addEventListener("pageshow", function pageShown(event) {
// If data: --url PAC file isn't loaded soon enough, we may get about:privatebrowsing loaded
if (event.target.location == "about:blank" ||
event.target.location == "about:privatebrowsing") {
aWindow.gBrowser.selectedBrowser.loadURI(testURI);
return;
}
aWindow.gBrowser.removeEventListener("pageshow", pageShown);
waitForFocus(function () {
aWindow.document.addEventListener("popupshown", contextMenuOpened, false);
var img = aWindow.gBrowser.selectedBrowser.contentDocument.getElementById("img");
EventUtils.synthesizeMouseAtCenter(img,
{ type: "contextmenu", button: 2 },
aWindow.gBrowser.contentWindow);
}, aWindow.gBrowser.selectedBrowser);
});
}
function testOnWindow(aOptions, aCallback) {
whenNewWindowLoaded(aOptions, function(aWin) {
windowsToClose.push(aWin);
// execute should only be called when need, like when you are opening
// web pages on the test. If calling executeSoon() is not necesary, then
// call whenNewWindowLoaded() instead of testOnWindow() on your test.
executeSoon(() => aCallback(aWin));
});
};
// this function is called after calling finish() on the test.
registerCleanupFunction(function() {
windowsToClose.forEach(function(aWin) {
aWin.close();
});
});
MockFilePicker.init(window);
// then test when on private mode
testOnWindow({private: true}, function(aWin) {
doTest(true, aWin, finish);
});
}
function promiseImageDownloaded() {
return new Promise((resolve, reject) => {
let fileName;
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init(window);
function onTransferComplete(downloadSuccess) {
ok(downloadSuccess, "Image file should have been downloaded successfully " + fileName);
// Give the request a chance to finish and create a cache entry
resolve(fileName);
}
// Create the folder the image will be saved into.
var destDir = createTemporarySaveDirectory();
var destFile = destDir.clone();
MockFilePicker.displayDirectory = destDir;
MockFilePicker.showCallback = function(fp) {
fileName = fp.defaultString;
destFile.append (fileName);
MockFilePicker.returnFiles = [destFile];
MockFilePicker.filterIndex = 1; // kSaveAsType_URL
};
mockTransferCallback = onTransferComplete;
mockTransferRegisterer.register();
registerCleanupFunction(function () {
mockTransferCallback = null;
mockTransferRegisterer.unregister();
MockFilePicker.cleanup();
destDir.remove(true);
});
});
}
add_task(function* () {
let testURI = "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.html";
let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
let tab = yield BrowserTestUtils.openNewForegroundTab(privateWindow.gBrowser, testURI);
let contextMenu = privateWindow.document.getElementById("contentAreaContextMenu");
let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
let popupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
yield BrowserTestUtils.synthesizeMouseAtCenter("#img", {
type: "contextmenu",
button: 2
}, tab.linkedBrowser);
yield popupShown;
let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
.getService(Ci.nsICacheStorageService);
cache.clear();
let imageDownloaded = promiseImageDownloaded();
// Select "Save Image As" option from context menu
privateWindow.document.getElementById("context-saveimage").doCommand();
contextMenu.hidePopup();
yield popupHidden;
// wait for image download
let fileName = yield imageDownloaded;
yield promiseNoCacheEntry(fileName);
yield BrowserTestUtils.closeWindow(privateWindow);
});
Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader)
.loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",

View File

@ -0,0 +1,3 @@
<!ENTITY foo.bar "This string
contains
newlines!">

View File

@ -611,9 +611,19 @@ function promiseTabLoadEvent(tab, url)
let deferred = Promise.defer();
info("Wait tab event: load");
function handle(loadedUrl) {
if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
info(`Skipping spurious load event for ${loadedUrl}`);
return false;
}
info("Tab event received: load");
return true;
}
// Create two promises: one resolved from the content process when the page
// loads and one that is rejected if we take too long to load the url.
let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url);
let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
let timeout = setTimeout(() => {
deferred.reject(new Error("Timed out while waiting for a 'load' event"));

View File

@ -10,7 +10,6 @@
orient="vertical">
<broadcasterset>
<broadcaster id="identity-popup-content-host" class="identity-popup-headline" crop="start"/>
<broadcaster id="identity-popup-mcb-learn-more" class="text-link plain" value="&identity.learnMore;"/>
<broadcaster id="identity-popup-insecure-login-forms-learn-more" class="text-link plain" value="&identity.learnMore;"/>
</broadcasterset>
@ -22,7 +21,10 @@
<!-- Security Section -->
<hbox id="identity-popup-security" class="identity-popup-section">
<vbox id="identity-popup-security-content" flex="1">
<label observes="identity-popup-content-host"/>
<label class="plain">
<label class="identity-popup-headline host"></label>
<label class="identity-popup-headline hostless" crop="end"/>
</label>
<description class="identity-popup-connection-not-secure"
value="&identity.connectionNotSecure;"
when-connection="not-secure secure-cert-user-overridden"/>
@ -96,7 +98,10 @@
<!-- Security SubView -->
<panelview id="identity-popup-securityView" flex="1">
<vbox id="identity-popup-securityView-header">
<label observes="identity-popup-content-host"/>
<label class="plain">
<label class="identity-popup-headline host"></label>
<label class="identity-popup-headline hostless" crop="end"/>
</label>
<description class="identity-popup-connection-not-secure"
value="&identity.connectionNotSecure;"
when-connection="not-secure secure-cert-user-overridden"/>

View File

@ -462,7 +462,7 @@ this.DownloadsCommon = {
if (!shouldLaunch) {
return;
}
// Actually open the file.
try {
if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
@ -470,7 +470,7 @@ this.DownloadsCommon = {
return;
}
} catch (ex) { }
// If either we don't have the mime info, or the preferred action failed,
// attempt to launch the file directly.
try {
@ -521,49 +521,83 @@ this.DownloadsCommon = {
* Displays an alert message box which asks the user if they want to
* unblock the downloaded file or not.
*
* @param aVerdict
* The detailed reason why the download was blocked, according to the
* "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown reason is
* specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is assumed.
* @param aOwnerWindow
* The window with which this action is associated.
* @param options
* An object with the following properties:
* {
* verdict:
* The detailed reason why the download was blocked, according to
* the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
* reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
* assumed.
* window:
* The window with which this action is associated.
* dialogType:
* String that determines which actions are available:
* - "unblock" to offer just "unblock".
* - "chooseUnblock" to offer "unblock" and "confirmBlock".
* - "chooseOpen" to offer "open" and "confirmBlock".
* }
*
* @return {Promise}
* @resolves String representing the action that should be executed:
* - "open" to allow the download and open the file.
* - "unblock" to allow the download without opening the file.
* - "confirmBlock" to delete the blocked data permanently.
* - "cancel" to do nothing and cancel the operation.
*/
confirmUnblockDownload: Task.async(function* (aVerdict, aOwnerWindow) {
confirmUnblockDownload: Task.async(function* ({ verdict, window,
dialogType }) {
let s = DownloadsCommon.strings;
let title = s.unblockHeader;
let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
(Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1);
let type = "";
let message = s.unblockTip;
let unblockButton = s.unblockButtonContinue;
let confirmBlockButton = s.unblockButtonCancel;
switch (aVerdict) {
case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
type = s.unblockTypeUncommon;
buttonFlags += (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
break;
case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
type = s.unblockTypePotentiallyUnwanted;
buttonFlags += (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
break;
default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
type = s.unblockTypeMalware;
// All the dialogs have an action button and a cancel button, while only
// some of them have an additonal button to remove the file. The cancel
// button must always be the one at BUTTON_POS_1 because this is the value
// returned by confirmEx when using ESC or closing the dialog (bug 345067).
let title = s.unblockHeaderUnblock;
let firstButtonText = s.unblockButtonUnblock;
let firstButtonAction = "unblock";
let buttonFlags =
(Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
(Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1);
switch (dialogType) {
case "unblock":
// Use only the unblock action. The default is to cancel.
buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
break;
case "chooseUnblock":
// Use the unblock and remove file actions. The default is remove file.
buttonFlags +=
(Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
break;
case "chooseOpen":
// Use the unblock and open file actions. The default is open file.
title = s.unblockHeaderOpen;
firstButtonText = s.unblockButtonOpen;
firstButtonAction = "open";
buttonFlags +=
(Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
break;
default:
Cu.reportError("Unexpected dialog type: " + dialogType);
return "cancel";
}
if (type) {
message = type + "\n\n" + message;
let message;
switch (verdict) {
case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
message = s.unblockTypeUncommon;
break;
case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
message = s.unblockTypePotentiallyUnwanted;
break;
default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
message = s.unblockTypeMalware;
break;
}
message += "\n\n" + s.unblockTip;
Services.ww.registerNotification(function onOpen(subj, topic) {
if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
@ -584,12 +618,10 @@ this.DownloadsCommon = {
}
});
// The ordering of the ok/cancel buttons is used this way to allow "cancel"
// to have the same result as hitting the ESC or Close button (see bug 345067).
let rv = Services.prompt.confirmEx(aOwnerWindow, title, message, buttonFlags,
unblockButton, null, confirmBlockButton,
null, {});
return ["unblock", "cancel", "confirmBlock"][rv];
let rv = Services.prompt.confirmEx(window, title, message, buttonFlags,
firstButtonText, null,
s.unblockButtonConfirmBlock, null, {});
return [firstButtonAction, "cancel", "confirmBlock"][rv];
}),
};

View File

@ -237,7 +237,7 @@ this.DownloadsViewUI.DownloadElementShell.prototype = {
} else if (this.download.error.becauseBlockedByReputationCheck) {
switch (this.download.error.reputationCheckVerdict) {
case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
stateLabel = s.blockedUncommon;
stateLabel = s.blockedUncommon2;
break;
case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
stateLabel = s.blockedPotentiallyUnwanted;
@ -271,11 +271,18 @@ this.DownloadsViewUI.DownloadElementShell.prototype = {
*
* @param window
* The window to which the dialog should be anchored.
* @param dialogType
* Can be "unblock", "chooseUnblock", or "chooseOpen".
*/
confirmUnblock(window) {
let verdict = this.download.error.reputationCheckVerdict;
DownloadsCommon.confirmUnblockDownload(verdict, window).then(action => {
if (action == "unblock") {
confirmUnblock(window, dialogType) {
DownloadsCommon.confirmUnblockDownload({
verdict: this.download.error.reputationCheckVerdict,
window,
dialogType,
}).then(action => {
if (action == "open") {
return this.download.unblock().then(() => this.downloadsCmd_open());
} else if (action == "unblock") {
return this.download.unblock();
} else if (action == "confirmBlock") {
return this.download.confirmBlock();
@ -323,6 +330,8 @@ this.DownloadsViewUI.DownloadElementShell.prototype = {
case "downloadsCmd_openReferrer":
return !!this.download.source.referrer;
case "downloadsCmd_confirmBlock":
case "downloadsCmd_chooseUnblock":
case "downloadsCmd_chooseOpen":
case "downloadsCmd_unblock":
return this.download.hasBlockedData;
}

View File

@ -377,7 +377,15 @@ HistoryDownloadElementShell.prototype = {
},
downloadsCmd_unblock() {
this.confirmUnblock(window);
this.confirmUnblock(window, "unblock");
},
downloadsCmd_chooseUnblock() {
this.confirmUnblock(window, "chooseUnblock");
},
downloadsCmd_chooseOpen() {
this.confirmUnblock(window, "chooseOpen");
},
// Returns whether or not the download handled by this shell should

View File

@ -63,6 +63,10 @@
oncommand="goDoCommand('downloadsCmd_cancel')"/>
<command id="downloadsCmd_unblock"
oncommand="goDoCommand('downloadsCmd_unblock')"/>
<command id="downloadsCmd_chooseUnblock"
oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/>
<command id="downloadsCmd_chooseOpen"
oncommand="goDoCommand('downloadsCmd_chooseOpen')"/>
<command id="downloadsCmd_confirmBlock"
oncommand="goDoCommand('downloadsCmd_confirmBlock')"/>
<command id="downloadsCmd_open"
@ -92,8 +96,8 @@
accesskey="&cmd.cancel.accesskey;"/>
<menuitem command="downloadsCmd_unblock"
class="downloadUnblockMenuItem"
label="&cmd.unblock.label;"
accesskey="&cmd.unblock.accesskey;"/>
label="&cmd.unblock2.label;"
accesskey="&cmd.unblock2.accesskey;"/>
<menuitem command="cmd_delete"
class="downloadRemoveFromHistoryMenuItem"
label="&cmd.removeFromHistory.label;"

View File

@ -63,9 +63,12 @@
<xul:button class="downloadButton downloadConfirmBlock downloadIconCancel"
tooltiptext="&cmd.removeFile.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_confirmBlock');"/>
<xul:button class="downloadButton downloadUnblock downloadIconShow"
tooltiptext="&cmd.unblock.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_unblock');"/>
<xul:button class="downloadButton downloadChooseUnblock downloadIconShow"
tooltiptext="&cmd.chooseUnblock.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseUnblock');"/>
<xul:button class="downloadButton downloadChooseOpen downloadIconShow"
tooltiptext="&cmd.chooseOpen.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseOpen');"/>
</xul:stack>
</content>
</binding>

View File

@ -127,13 +127,22 @@ richlistitem.download button {
.downloadConfirmBlock,
/* Blocked (dirty) downloads that have not been confirmed and
have temporary data, for cases other than Malware. */
have temporary data, for the Potentially Unwanted case. */
.download-state:not( [state="8"] /* Blocked (dirty) */)
.downloadUnblock,
.downloadChooseUnblock,
.download-state[state="8"]:not(.temporary-block)
.downloadUnblock,
.download-state[state="8"].temporary-block[verdict="Malware"]
.downloadUnblock,
.downloadChooseUnblock,
.download-state[state="8"].temporary-block:not([verdict="PotentiallyUnwanted"])
.downloadChooseUnblock,
/* Blocked (dirty) downloads that have not been confirmed and
have temporary data, for the Uncommon case. */
.download-state:not( [state="8"] /* Blocked (dirty) */)
.downloadChooseOpen,
.download-state[state="8"]:not(.temporary-block)
.downloadChooseOpen,
.download-state[state="8"].temporary-block:not([verdict="Uncommon"])
.downloadChooseOpen,
.download-state:not(:-moz-any([state="2"], /* Failed */
[state="3"]) /* Canceled */)

View File

@ -1092,7 +1092,17 @@ DownloadsViewItem.prototype = {
downloadsCmd_unblock() {
DownloadsPanel.hidePanel();
this.confirmUnblock(window);
this.confirmUnblock(window, "unblock");
},
downloadsCmd_chooseUnblock() {
DownloadsPanel.hidePanel();
this.confirmUnblock(window, "chooseUnblock");
},
downloadsCmd_chooseOpen() {
DownloadsPanel.hidePanel();
this.confirmUnblock(window, "chooseOpen");
},
downloadsCmd_open() {

View File

@ -23,6 +23,10 @@
oncommand="goDoCommand('downloadsCmd_cancel')"/>
<command id="downloadsCmd_unblock"
oncommand="goDoCommand('downloadsCmd_unblock')"/>
<command id="downloadsCmd_chooseUnblock"
oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/>
<command id="downloadsCmd_chooseOpen"
oncommand="goDoCommand('downloadsCmd_chooseOpen')"/>
<command id="downloadsCmd_confirmBlock"
oncommand="goDoCommand('downloadsCmd_confirmBlock')"/>
<command id="downloadsCmd_open"
@ -71,8 +75,8 @@
accesskey="&cmd.cancel.accesskey;"/>
<menuitem command="downloadsCmd_unblock"
class="downloadUnblockMenuItem"
label="&cmd.unblock.label;"
accesskey="&cmd.unblock.accesskey;"/>
label="&cmd.unblock2.label;"
accesskey="&cmd.unblock2.accesskey;"/>
<menuitem command="cmd_delete"
class="downloadRemoveFromHistoryMenuItem"
label="&cmd.removeFromHistory.label;"

View File

@ -31,16 +31,86 @@ function addDialogOpenObserver(buttonAction) {
});
}
add_task(function* test_confirm_unblock_dialog_unblock() {
addDialogOpenObserver("accept");
let result = yield DownloadsCommon.confirmUnblockDownload(Downloads.Error.BLOCK_VERDICT_MALWARE,
window);
is(result, "unblock");
function* assertDialogResult({ args, buttonToClick, expectedResult }) {
addDialogOpenObserver(buttonToClick);
is(yield DownloadsCommon.confirmUnblockDownload(args), expectedResult);
}
/**
* Tests the "unblock" dialog, for each of the possible verdicts.
*/
add_task(function* test_unblock_dialog_unblock() {
for (let verdict of [Downloads.Error.BLOCK_VERDICT_MALWARE,
Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
Downloads.Error.BLOCK_VERDICT_UNCOMMON]) {
let args = { verdict, window, dialogType: "unblock" };
// Test both buttons.
yield assertDialogResult({
args,
buttonToClick: "accept",
expectedResult: "unblock",
});
yield assertDialogResult({
args,
buttonToClick: "cancel",
expectedResult: "cancel",
});
}
});
add_task(function* test_confirm_unblock_dialog_keep_safe() {
addDialogOpenObserver("cancel");
let result = yield DownloadsCommon.confirmUnblockDownload(Downloads.Error.BLOCK_VERDICT_MALWARE,
window);
is(result, "cancel");
/**
* Tests the "chooseUnblock" dialog for potentially unwanted downloads.
*/
add_task(function* test_chooseUnblock_dialog() {
let args = {
verdict: Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
window,
dialogType: "chooseUnblock",
};
// Test each of the three buttons.
yield assertDialogResult({
args,
buttonToClick: "accept",
expectedResult: "unblock",
});
yield assertDialogResult({
args,
buttonToClick: "cancel",
expectedResult: "cancel",
});
yield assertDialogResult({
args,
buttonToClick: "extra1",
expectedResult: "confirmBlock",
});
});
/**
* Tests the "chooseOpen" dialog for uncommon downloads.
*/
add_task(function* test_chooseOpen_dialog() {
let args = {
verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
window,
dialogType: "chooseOpen",
};
// Test each of the three buttons.
yield assertDialogResult({
args,
buttonToClick: "accept",
expectedResult: "open",
});
yield assertDialogResult({
args,
buttonToClick: "cancel",
expectedResult: "cancel",
});
yield assertDialogResult({
args,
buttonToClick: "extra1",
expectedResult: "confirmBlock",
});
});

View File

@ -66,18 +66,27 @@
<!ENTITY cmd.clearList.accesskey "a">
<!ENTITY cmd.clearDownloads.label "Clear Downloads">
<!ENTITY cmd.clearDownloads.accesskey "D">
<!-- LOCALIZATION NOTE (cmd.unblock.label):
This command may be shown in the context menu, as a menu button item, or as
a text link when malware or potentially unwanted downloads are blocked.
<!-- LOCALIZATION NOTE (cmd.unblock2.label):
This command is shown in the context menu when downloads are blocked.
-->
<!ENTITY cmd.unblock.label "Unblock">
<!ENTITY cmd.unblock.accesskey "U">
<!ENTITY cmd.unblock2.label "Allow Download">
<!ENTITY cmd.unblock2.accesskey "o">
<!-- LOCALIZATION NOTE (cmd.removeFile.label):
This command may be shown in the context menu or as a menu button label
when malware or potentially unwanted downloads are blocked.
This is the tooltip of the action button shown when malware is blocked.
-->
<!ENTITY cmd.removeFile.label "Remove File">
<!ENTITY cmd.removeFile.accesskey "m">
<!-- LOCALIZATION NOTE (cmd.chooseUnblock.tooltip):
This is the tooltip of the action button shown when potentially unwanted
downloads are blocked. This opens a dialog where the user can choose
whether to unblock or remove the download. Removing is the default option.
-->
<!ENTITY cmd.chooseUnblock.label "Remove File or Allow Download">
<!-- LOCALIZATION NOTE (cmd.chooseOpen.tooltip):
This is the tooltip of the action button shown when uncommon downloads are
blocked.This opens a dialog where the user can choose whether to open the
file or remove the download. Opening is the default option.
-->
<!ENTITY cmd.chooseOpen.label "Open or Remove File">
<!-- LOCALIZATION NOTE (blocked.label):
Shown as a tag before the file name for some types of blocked downloads.

View File

@ -39,7 +39,7 @@ stateBlockedPolicy=Blocked by your security zone policy
stateDirty=Blocked: May contain a virus or spyware
# LOCALIZATION NOTE (blockedMalware, blockedPotentiallyUnwanted,
# blockedUncommon):
# blockedUncommon2):
# These strings are shown in the panel for some types of blocked downloads, and
# are immediately followed by the "Learn More" link, thus they must end with a
# period. You may need to adjust "downloadDetails.width" in "downloads.dtd" if
@ -47,23 +47,25 @@ stateDirty=Blocked: May contain a virus or spyware
# Note: These strings don't exist in the UI yet. See bug 1053890.
blockedMalware=This file contains a virus or malware.
blockedPotentiallyUnwanted=This file may harm your computer.
blockedUncommon=This file may not be safe to open.
blockedUncommon2=This file is not commonly downloaded.
# LOCALIZATION NOTE (unblockHeader, unblockTypeMalware,
# unblockTypePotentiallyUnwanted, unblockTypeUncommon,
# unblockTip, unblockButtonContinue, unblockButtonCancel):
# LOCALIZATION NOTE (unblockHeaderUnblock, unblockHeaderOpen,
# unblockTypeMalware, unblockTypePotentiallyUnwanted,
# unblockTypeUncommon, unblockTip, unblockButtonOpen,
# unblockButtonUnblock, unblockButtonConfirmBlock):
# These strings are displayed in the dialog shown when the user asks a blocked
# download to be unblocked. The severity of the threat is expressed in
# descending order by the unblockType strings, it is higher for files detected
# as malware and lower for uncommon downloads.
# Note: These strings don't exist in the UI yet. See bug 1053890.
unblockHeader=Are you sure you want to unblock this file?
unblockHeaderUnblock=Are you sure you want to allow this download?
unblockHeaderOpen=Are you sure you want to open this file?
unblockTypeMalware=This file contains a virus or other malware that will harm your computer.
unblockTypePotentiallyUnwanted=This file, disguised as a helpful download, will make unexpected changes to your programs and settings.
unblockTypeUncommon=This file has been downloaded from an unfamiliar and potentially dangerous website and may not be safe to open.
unblockTip=You can search for an alternate download source or try to download the file again later.
unblockButtonContinue=Unblock anyway
unblockButtonCancel=Keep me safe
unblockButtonOpen=Open
unblockButtonUnblock=Allow download
unblockButtonConfirmBlock=Remove file
# LOCALIZATION NOTE (sizeWithUnits):
# %1$S is replaced with the size number, and %2$S with the measurement unit.

View File

@ -4,8 +4,15 @@
<!ENTITY safeb.palm.accept.label "Get me out of here!">
<!ENTITY safeb.palm.decline.label "Ignore this warning">
<!-- Localization note (safeb.palm.notdeceptive.label) - Label of the Help menu item. -->
<!-- Localization note (safeb.palm.notdeceptive.label) - Label of the Help menu
item. Either this or reportDeceptiveSiteMenu.label from report-phishing.dtd is
shown. -->
<!ENTITY safeb.palm.notdeceptive.label "This isnt a deceptive site…">
<!-- Localization note (safeb.palm.notdeceptive.accesskey) - Because
safeb.palm.notdeceptive.label and reportDeceptiveSiteMenu.title from
report-phishing.dtd are never shown at the same time, the same accesskey can
be used for them. -->
<!ENTITY safeb.palm.notdeceptive.accesskey "d">
<!ENTITY safeb.palm.reportPage.label "Why was this page blocked?">
<!ENTITY safeb.palm.whyForbidden.label "Why was this page blocked?">

View File

@ -2,5 +2,12 @@
- 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/. -->
<!-- Localization note (reportDeceptiveSiteMenu.title) - Label of the Help menu
item. Either this or safeb.palm.notdeceptive.label from
phishing-afterload-warning-message.dtd is shown. -->
<!ENTITY reportDeceptiveSiteMenu.title "Report deceptive site…">
<!-- Localization note (reportDeceptiveSiteMenu.accesskey) - Because
safeb.palm.notdeceptive.label from phishing-afterload-warning-message.dtd and
reportDeceptiveSiteMenu.title are never shown at the same time, the same
accesskey can be used for them. -->
<!ENTITY reportDeceptiveSiteMenu.accesskey "D">

View File

@ -103,6 +103,7 @@
#identity-popup-permissions-content,
#tracking-protection-content {
padding: 0.5em 0 1em;
/* .identity-popup-headline.host depends on this width */
-moz-padding-start: calc(2em + 24px);
-moz-padding-end: 1em;
}
@ -120,7 +121,7 @@
margin: 0;
padding: 4px 0;
min-width: auto;
width: 38px;
width: var(--identity-popup-expander-width);
border: 0 none;
-moz-appearance: none;
background-image: url("chrome://browser/skin/controlcenter/arrow-subview.svg"),
@ -181,6 +182,13 @@
font-size: 150%;
}
.identity-popup-headline.host {
word-wrap: break-word;
/* 1em + 2em + 24px is #identity-popup-security-content padding
* 30em is .panel-mainview:not([panelid="PanelUI-popup"]) width */
max-width: calc(30rem - 3rem - 24px - var(--identity-popup-expander-width))
}
.identity-popup-warning-gray {
-moz-padding-start: 24px;
background: url(chrome://browser/skin/controlcenter/warning-gray.svg) no-repeat 0 50%;

View File

@ -34,6 +34,8 @@ support-files =
support-files =
browser_cmd_cookie.html
[browser_cmd_cookie_host.js]
support-files =
browser_cmd_cookie.html
[browser_cmd_csscoverage_oneshot.js]
support-files =
browser_cmd_csscoverage_page1.html

View File

@ -11,6 +11,7 @@
<script type="text/javascript">
document.cookie = "zap=zep";
document.cookie = "zip=zop";
document.cookie = "zig=zag; domain=.mochi.test";
document.getElementById("result").innerHTML = document.cookie;
</script>

View File

@ -1,9 +1,11 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that the cookie command works for host with a port specified
const TEST_URI = "http://mochi.test:8888/browser/devtools/client/commandline/"+
const TEST_URI = "http://mochi.test:8888/browser/devtools/client/commandline/" +
"test/browser_cmd_cookie.html";
function test() {
@ -12,7 +14,7 @@ function test() {
{
setup: 'cookie list',
exec: {
output: [ /zap=zep/, /zip=zop/ ],
output: [ /zap=zep/, /zip=zop/, /zig=zag/ ],
}
},
{
@ -30,7 +32,7 @@ function test() {
{
setup: "cookie list",
exec: {
output: [ /zap=zep/, /zip=zop/, /zup=banana/, /Edit/ ]
output: [ /zap=zep/, /zip=zop/, /zig=zag/, /zup=banana/, /Edit/ ]
}
}
]);

View File

@ -0,0 +1,18 @@
{
"manifest_version": 2,
"name": "test content script sources",
"description": "test content script sources",
"version": "0.1.0",
"applications": {
"gecko": {
"id": "test-contentscript-sources@mozilla.com"
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["webext-content-script.js"],
"run_at": "document_start"
}
]
}

View File

@ -0,0 +1 @@
console.log("CONTENT SCRIPT LOADED");

View File

@ -8,6 +8,7 @@ support-files =
addon3.xpi
addon4.xpi
addon5.xpi
addon-webext-contentscript.xpi
code_binary_search.coffee
code_binary_search.js
code_binary_search.map
@ -109,6 +110,7 @@ support-files =
doc_script-bookmarklet.html
doc_script-switching-01.html
doc_script-switching-02.html
doc_script_webext_contentscript.html
doc_split-console-paused-reload.html
doc_step-many-statements.html
doc_step-out.html
@ -466,6 +468,7 @@ skip-if = e10s && debug
skip-if = e10s && debug
[browser_dbg_sources-bookmarklet.js]
skip-if = e10s && debug
[browser_dbg_sources-webext-contentscript.js]
[browser_dbg_split-console-paused-reload.js]
skip-if = e10s && debug
[browser_dbg_stack-01.js]

View File

@ -0,0 +1,61 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure eval scripts appear in the source list
*/
const TAB_URL = EXAMPLE_URL + "doc_script_webext_contentscript.html";
function test() {
let gPanel, gDebugger;
let gSources, gAddon;
let cleanup = function* (e) {
if (gAddon) {
// Remove the addon, if any.
yield removeAddon(gAddon);
}
if (gPanel) {
// Close the debugger panel, if any.
yield closeDebuggerAndFinish(gPanel);
} else {
// If no debugger panel was opened, call finish directly.
finish();
}
};
return Task.spawn(function* () {
gAddon = yield addAddon(EXAMPLE_URL + "/addon-webext-contentscript.xpi");
[,, gPanel] = yield initDebugger(TAB_URL);
gDebugger = gPanel.panelWin;
gSources = gDebugger.DebuggerView.Sources;
// Wait for a SOURCE_SHOWN event for at most 4 seconds.
yield Promise.race([
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN),
waitForTime(4000),
]);
is(gSources.values.length, 1, "Should have 1 source");
let item = gSources.getItemForAttachment(attachment => {
return attachment.source.url.includes("moz-extension");
});
ok(item, "Got the expected WebExtensions ContentScript source");
ok(item && item.attachment.source.url.includes(item.attachment.group),
"The source is in the expected source group");
is(item && item.attachment.label, "webext-content-script.js",
"Got the expected filename in the label");
yield cleanup();
}).catch((e) => {
ok(false, `Got an unexpected exception: ${e}`);
// Cleanup in case of failures in the above task.
return Task.spawn(cleanup);
});
}

View File

@ -0,0 +1,13 @@
<!-- 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>
</body>
</html>

View File

@ -165,18 +165,23 @@ function synthesizeKeyFromKeyTag(key) {
}
/**
* Wait for eventName on target.
* @param {Object} target An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Boolean} useCapture Optional, for
* Wait for eventName on target to be delivered a number of times.
*
* @param {Object} target
* An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Number} numTimes
* Number of deliveries to wait for.
* @param {Boolean} useCapture
* Optional, for addEventListener/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function once(target, eventName, useCapture = false) {
function waitForNEvents(target, eventName, numTimes, useCapture = false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
let deferred = promise.defer();
let count = 0;
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
@ -186,8 +191,10 @@ function once(target, eventName, useCapture = false) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
info("Got event: '" + eventName + "' on " + target + ".");
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
if (++count == numTimes) {
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}
}, useCapture);
break;
}
@ -196,6 +203,21 @@ function once(target, eventName, useCapture = false) {
return deferred.promise;
}
/**
* Wait for eventName on target.
*
* @param {Object} target
* An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Boolean} useCapture
* Optional, for addEventListener/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function once(target, eventName, useCapture = false) {
return waitForNEvents(target, eventName, 1, useCapture);
}
/**
* Some tests may need to import one or more of the test helper scripts.
* A test helper script is simply a js file that contains common test code that
@ -222,6 +244,20 @@ function waitForTick() {
return deferred.promise;
}
/**
* This shouldn't be used in the tests, but is useful when writing new tests or
* debugging existing tests in order to introduce delays in the test steps
*
* @param {Number} ms
* The time to wait
* @return A promise that resolves when the time is passed
*/
function wait(ms) {
let def = promise.defer();
content.setTimeout(def.resolve, ms);
return def.promise;
}
/**
* Open the toolbox in a given tab.
* @param {XULNode} tab The tab the toolbox should be opened in.

View File

@ -62,6 +62,8 @@ function setPrefDefaults() {
Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
Services.prefs.setBoolPref("devtools.scratchpad.enabled", true);
// Bug 1225160 - Using source maps with browser debugging can lead to a crash
Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
}
window.addEventListener("load", function() {

View File

@ -12,50 +12,6 @@ registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.defaultColorUnit");
});
/**
* Open the toolbox, with the inspector tool visible, and the computed-view
* sidebar tab selected.
* @return a promise that resolves when the inspector is ready and the computed
* view is visible and ready
*/
function openComputedView() {
return openInspectorSidebarTab("computedview").then(({toolbox, inspector}) => {
return {
toolbox,
inspector,
view: inspector.computedview.view
};
});
}
/**
* Get the NodeFront for a given css selector, via the protocol
*
* @param {String} selector
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return {Promise} Resolves to the NodeFront instance
*/
function getNodeFront(selector, {walker}) {
return walker.querySelector(walker.rootNode, selector);
}
/**
* Listen for a new tab to open and return a promise that resolves when one
* does and completes the load event.
*
* @return a promise that resolves to the tab object
*/
var waitForTab = Task.async(function*() {
info("Waiting for a tab to open");
yield once(gBrowser.tabContainer, "TabOpen");
let tab = gBrowser.selectedTab;
let browser = tab.linkedBrowser;
yield once(browser, "load", true);
info("The tab load completed");
return tab;
});
/**
* Dispatch the copy event on the given element
*/
@ -65,20 +21,6 @@ function fireCopyEvent(element) {
element.dispatchEvent(evt);
}
/**
* Simulate the key input for the given input in the window.
*
* @param {String} input
* The string value to input
* @param {Window} win
* The window containing the panel
*/
function synthesizeKeys(input, win) {
for (let key of input.split("")) {
EventUtils.synthesizeKey(key, {}, win);
}
}
/**
* Get references to the name and value span nodes corresponding to a given
* property name in the computed-view

View File

@ -41,76 +41,6 @@ addTab = function(url) {
});
};
/**
* Open the toolbox, with the inspector tool visible, and the rule-view
* sidebar tab selected.
*
* @return a promise that resolves when the inspector is ready and the rule
* view is visible and ready
*/
function openRuleView() {
return openInspectorSidebarTab("ruleview").then(data => {
return {
toolbox: data.toolbox,
inspector: data.inspector,
testActor: data.testActor,
view: data.inspector.ruleview.view
};
});
}
/**
* Set the inspector's current selection to null so that no node is selected
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return a promise that resolves when the inspector is updated
*/
function clearCurrentNodeSelection(inspector) {
info("Clearing the current selection");
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(null);
return updated;
}
/**
* Wait for eventName on target to be delivered a number of times.
*
* @param {Object} target
* An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Number} numTimes
* Number of deliveries to wait for.
* @param {Boolean} useCapture
* Optional, for addEventListener/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function waitForNEvents(target, eventName, numTimes, useCapture = false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
let deferred = promise.defer();
let count = 0;
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
["on", "off"]
]) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
if (++count == numTimes) {
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}
}, useCapture);
break;
}
}
return deferred.promise;
}
/**
* Wait for a content -> chrome message on the message manager (the window
* messagemanager is used).
@ -284,22 +214,6 @@ function* hideTooltipAndWaitForRuleViewChanged(tooltip, view) {
yield onModified;
}
/**
* Listen for a new tab to open and return a promise that resolves when one
* does and completes the load event.
*
* @return a promise that resolves to the tab object
*/
var waitForTab = Task.async(function* () {
info("Waiting for a tab to open");
yield once(gBrowser.tabContainer, "TabOpen");
let tab = gBrowser.selectedTab;
let browser = tab.linkedBrowser;
yield once(browser, "load", true);
info("The tab load completed");
return tab;
});
/**
* Polls a given generator function waiting for it to return true.
*
@ -348,20 +262,6 @@ var getFontFamilyDataURL = Task.async(function* (font, nodeFront) {
return dataURL;
});
/**
* Simulate the key input for the given input in the window.
*
* @param {String} input
* The string value to input
* @param {Window} win
* The window containing the panel
*/
function synthesizeKeys(input, win) {
for (let key of input.split("")) {
EventUtils.synthesizeKey(key, {}, win);
}
}
/**
* Get the DOMNode for a css rule in the rule-view that corresponds to the given
* selector

View File

@ -12,11 +12,17 @@ support-files =
doc_content_stylesheet_xul.css
doc_frame_script.js
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/framework/test/shared-head.js
!/devtools/client/shared/test/test-actor.js
!/devtools/client/shared/test/test-actor-registry.js
[browser_styleinspector_context-menu-copy-color_01.js]
[browser_styleinspector_context-menu-copy-color_02.js]
[browser_styleinspector_context-menu-copy-urls.js]
[browser_styleinspector_csslogic-content-stylesheets.js]
skip-if = e10s && debug # Bug 1250058 (docshell leak when opening 2 toolboxes)
[browser_styleinspector_output-parser.js]
[browser_styleinspector_refresh_when_active.js]
[browser_styleinspector_tooltip-background-image.js]

View File

@ -11,31 +11,20 @@ const TEST_URI = `
</div>
`;
const TEST_CASES = [
{
viewName: "RuleView",
initializer: openRuleView
},
{
viewName: "ComputedView",
initializer: openComputedView
}
];
add_task(function* () {
// Test is slow on Linux EC2 instances - Bug 1137765
requestLongerTimeout(2);
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
for (let test of TEST_CASES) {
yield testView(test);
}
let {inspector} = yield openInspector();
yield testView("ruleview", inspector);
yield testView("computedview", inspector);
});
function* testView({viewName, initializer}) {
info("Testing " + viewName);
function* testView(viewId, inspector) {
info("Testing " + viewId);
let {inspector, view} = yield initializer();
yield inspector.sidebar.select(viewId);
let view = inspector[viewId].view;
yield selectNode("div", inspector);
testIsColorValueNode(view);

View File

@ -19,6 +19,7 @@ add_task(function* () {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield testCopyToClipboard(inspector, view);
yield testManualEdit(inspector, view);
yield testColorPickerEdit(inspector, view);

View File

@ -39,24 +39,24 @@ add_task(function*() {
function* startTest() {
info("Opening rule view");
let ruleViewData = yield openRuleView();
let {inspector, view} = yield openRuleView();
info("Test valid background image URL in rule view");
yield testCopyUrlToClipboard(ruleViewData, "data-uri", ".valid-background", TEST_DATA_URI);
yield testCopyUrlToClipboard(ruleViewData, "url", ".valid-background", TEST_DATA_URI);
yield testCopyUrlToClipboard({view, inspector}, "data-uri", ".valid-background", TEST_DATA_URI);
yield testCopyUrlToClipboard({view, inspector}, "url", ".valid-background", TEST_DATA_URI);
info("Test invalid background image URL in rue view");
yield testCopyUrlToClipboard(ruleViewData, "data-uri", ".invalid-background", ERROR_MESSAGE);
yield testCopyUrlToClipboard(ruleViewData, "url", ".invalid-background", INVALID_IMAGE_URI);
yield testCopyUrlToClipboard({view, inspector}, "data-uri", ".invalid-background", ERROR_MESSAGE);
yield testCopyUrlToClipboard({view, inspector}, "url", ".invalid-background", INVALID_IMAGE_URI);
info("Opening computed view");
let computedViewData = yield openComputedView();
view = selectComputedView(inspector);
info("Test valid background image URL in computed view");
yield testCopyUrlToClipboard(computedViewData, "data-uri", ".valid-background", TEST_DATA_URI);
yield testCopyUrlToClipboard(computedViewData, "url", ".valid-background", TEST_DATA_URI);
yield testCopyUrlToClipboard({view, inspector}, "data-uri", ".valid-background", TEST_DATA_URI);
yield testCopyUrlToClipboard({view, inspector}, "url", ".valid-background", TEST_DATA_URI);
info("Test invalid background image URL in computed view");
yield testCopyUrlToClipboard(computedViewData, "data-uri", ".invalid-background", ERROR_MESSAGE);
yield testCopyUrlToClipboard(computedViewData, "url", ".invalid-background", INVALID_IMAGE_URI);
yield testCopyUrlToClipboard({view, inspector}, "data-uri", ".invalid-background", ERROR_MESSAGE);
yield testCopyUrlToClipboard({view, inspector}, "url", ".invalid-background", INVALID_IMAGE_URI);
}
function* testCopyUrlToClipboard({view, inspector}, type, selector, expected) {

View File

@ -27,7 +27,7 @@ add_task(function*() {
yield addTab(TEST_URI_HTML);
let target = getNode("#target");
let {inspector} = yield openRuleView();
let {inspector} = yield openInspector();
yield selectNode("#target", inspector);
info("Checking stylesheets");
@ -36,7 +36,7 @@ add_task(function*() {
info("Checking authored stylesheets");
yield addTab(TEST_URI_AUTHOR);
({inspector} = yield openRuleView());
({inspector} = yield openInspector());
target = getNode("#target");
yield selectNode("#target", inspector);
yield checkSheets(target);
@ -46,7 +46,7 @@ add_task(function*() {
allowXUL();
yield addTab(TEST_URI_XUL);
({inspector} = yield openRuleView());
({inspector} = yield openInspector());
target = getNode("#target");
yield selectNode("#target", inspector);

View File

@ -14,6 +14,7 @@ const TEST_URI = `
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode("#one", inspector);
is(getRuleViewPropertyValue(view, "element", "color"), "red",
@ -25,7 +26,7 @@ add_task(function*() {
info("Switching to the computed-view");
let onComputedViewReady = inspector.once("computed-view-refreshed");
yield openComputedView();
selectComputedView(inspector);
yield onComputedViewReady;
ok(getComputedViewPropertyValue(cView, "color"), "#F00",

View File

@ -43,7 +43,7 @@ add_task(function*() {
info("Switching over to the computed-view");
let onComputedViewReady = inspector.once("computed-view-refreshed");
({view} = yield openComputedView());
view = selectComputedView(inspector);
yield onComputedViewReady;
info("Testing that the background-image computed style has a tooltip too");

View File

@ -17,7 +17,7 @@ add_task(function*() {
yield testRuleView(view, inspector);
info("Testing computed view tooltip closes on new selection");
({view} = yield openComputedView());
view = selectComputedView(inspector);
yield testComputedView(view, inspector);
});

View File

@ -25,7 +25,7 @@ add_task(function*() {
info("Opening the computed view");
let onComputedViewReady = inspector.once("computed-view-refreshed");
({inspector, view} = yield openComputedView());
view = selectComputedView(inspector);
yield onComputedViewReady;
yield testComputedView(view, inspector.selection.nodeFront);

View File

@ -25,25 +25,27 @@ add_task(function* () {
yield addTab("data:text/html;charset=utf-8,background image tooltip test");
content.document.body.innerHTML = PAGE_CONTENT;
yield testRuleViewUrls();
yield testComputedViewUrls();
let {inspector} = yield openInspector();
yield testRuleViewUrls(inspector);
yield testComputedViewUrls(inspector);
});
function* testRuleViewUrls() {
function* testRuleViewUrls(inspector) {
info("Testing tooltips in the rule view");
let {view, inspector} = yield openRuleView();
let view = selectRuleView(inspector);
yield selectNode("h1", inspector);
let {valueSpan} = getRuleViewProperty(view, "h1", "background");
yield performChecks(view, valueSpan);
}
function* testComputedViewUrls() {
function* testComputedViewUrls(inspector) {
info("Testing tooltips in the computed view");
let {view, inspector} = yield openComputedView();
yield inspector.once("computed-view-refreshed");
let onComputedViewReady = inspector.once("computed-view-refreshed");
let view = selectComputedView(inspector);
yield onComputedViewReady;
let {valueSpan} = getComputedViewProperty(view, "background-image");
yield performChecks(view, valueSpan);

View File

@ -18,6 +18,7 @@ const TEST_URI = `
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode("#testElement", inspector);
yield testRuleView(view, inspector.selection.nodeFront);
});

View File

@ -20,6 +20,7 @@ const TYPE = "CssTransformHighlighter";
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
let overlay = view.highlighters;
ok(!overlay.highlighters[TYPE], "No highlighter exists in the rule-view");
@ -32,7 +33,7 @@ add_task(function*() {
"The same instance of highlighter is returned everytime in the rule-view");
let onComputedViewReady = inspector.once("computed-view-refreshed");
let {view: cView} = yield openComputedView();
let cView = selectComputedView(inspector);
yield onComputedViewReady;
overlay = cView.highlighters;

View File

@ -38,7 +38,7 @@ add_task(function*() {
yield onHighlighterShown;
let onComputedViewReady = inspector.once("computed-view-refreshed");
let {view: cView} = yield openComputedView();
let cView = selectComputedView(inspector);
yield onComputedViewReady;
hs = cView.highlighters;

View File

@ -1,23 +1,19 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from ../../test/head.js */
"use strict";
var Cu = Components.utils;
var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
var {gDevTools} = require("devtools/client/framework/devtools");
var {TargetFactory} = require("devtools/client/framework/target");
var {CssRuleView, _ElementStyle} = require("devtools/client/inspector/rules/rules");
var {CssLogic, CssSelector} = require("devtools/shared/inspector/css-logic");
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
var promise = require("promise");
var {editableField, getInplaceEditorForSpan: inplaceEditor} =
require("devtools/client/shared/inplace-editor");
var {console} = Cu.import("resource://gre/modules/Console.jsm", {});
// Import the inspector's head.js first (which itself imports shared-head.js).
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
this);
// All tests are asynchronous
waitForExplicitFinish();
var {CssRuleView} = require("devtools/client/inspector/rules/rules");
var {CssLogic, CssSelector} = require("devtools/shared/inspector/css-logic");
var {getInplaceEditorForSpan: inplaceEditor} =
require("devtools/client/shared/inplace-editor");
const TEST_URL_ROOT =
"http://example.com/browser/devtools/client/inspector/shared/test/";
@ -26,28 +22,9 @@ const TEST_URL_ROOT_SSL =
const ROOT_TEST_DIR = getRootDirectory(gTestPath);
const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
// Auto clean-up when a test ends
registerCleanupFunction(function*() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
yield gDevTools.closeToolbox(target);
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
// Uncomment this pref to dump all devtools emitted events to the console.
// Services.prefs.setBoolPref("devtools.dump.emit", true);
// Set the testing flag on gDevTools and reset it when the test ends
DevToolsUtils.testing = true;
registerCleanupFunction(() => DevToolsUtils.testing = false);
// Clean-up all prefs that might have been changed during a test run
// (safer here because if the test fails, then the pref is never reverted)
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
Services.prefs.clearUserPref("devtools.dump.emit");
Services.prefs.clearUserPref("devtools.defaultColorUnit");
});
@ -62,8 +39,9 @@ registerCleanupFunction(() => {
*
* add_task(function*() {
* yield addTab(TEST_URI);
* let {toolbox, inspector, view} = yield openComputedView();
*
* let {toolbox, inspector} = yield openInspector();
* inspector.sidebar.select(viewId);
* let view = inspector[viewId].view;
* yield selectNode("#test", inspector);
* yield someAsyncTestFunction(view);
* });
@ -92,263 +70,21 @@ registerCleanupFunction(() => {
*/
/**
* Add a new test tab in the browser and load the given url.
*
* @param {String} url
* The url to be loaded in the new tab
* @return a promise that resolves to the tab object when the url is loaded
* The rule-view tests rely on a frame-script to be injected in the content test
* page. So override the shared-head's addTab to load the frame script after the
* tab was added.
* FIXME: Refactor the rule-view tests to use the testActor instead of a frame
* script, so they can run on remote targets too.
*/
function addTab(url) {
info("Adding a new tab with URL: '" + url + "'");
let def = promise.defer();
window.focus();
let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
let browser = tab.linkedBrowser;
info("Loading the helper frame script " + FRAME_SCRIPT_URL);
browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
browser.addEventListener("load", function onload() {
browser.removeEventListener("load", onload, true);
info("URL '" + url + "' loading complete");
def.resolve(tab);
}, true);
return def.promise;
}
/**
* Simple DOM node accesor function that takes either a node or a string css
* selector as argument and returns the corresponding node
*
* @param {String|DOMNode} nodeOrSelector
* @return {DOMNode|CPOW} Note that in e10s mode a CPOW object is returned which
* doesn't implement *all* of the DOMNode's properties
*/
function getNode(nodeOrSelector) {
info("Getting the node for '" + nodeOrSelector + "'");
return typeof nodeOrSelector === "string" ?
content.document.querySelector(nodeOrSelector) :
nodeOrSelector;
}
/**
* Get the NodeFront for a given css selector, via the protocol
*
* @param {String} selector
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return {Promise} Resolves to the NodeFront instance
*/
function getNodeFront(selector, {walker}) {
return walker.querySelector(walker.rootNode, selector);
}
/*
* Set the inspector's current selection to a node or to the first match of the
* given css selector.
*
* @param {String|NodeFront} data
* The node to select
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @param {String} reason
* Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @return {Promise} Resolves when the inspector is updated with the new node
*/
var selectNode = Task.async(function*(data, inspector, reason="test") {
info("Selecting the node for '" + data + "'");
let nodeFront = data;
if (!data._form) {
nodeFront = yield getNodeFront(data, inspector);
}
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(nodeFront, reason);
yield updated;
});
/**
* Set the inspector's current selection to null so that no node is selected
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return a promise that resolves when the inspector is updated
*/
function clearCurrentNodeSelection(inspector) {
info("Clearing the current selection");
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(null);
return updated;
}
/**
* Open the toolbox, with the inspector tool visible.
*
* @return a promise that resolves when the inspector is ready
*/
var openInspector = Task.async(function*() {
info("Opening the inspector");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let inspector, toolbox;
// Checking if the toolbox and the inspector are already loaded
// The inspector-updated event should only be waited for if the inspector
// isn't loaded yet
toolbox = gDevTools.getToolbox(target);
if (toolbox) {
inspector = toolbox.getPanel("inspector");
if (inspector) {
info("Toolbox and inspector already open");
return {
toolbox: toolbox,
inspector: inspector
};
}
}
info("Opening the toolbox");
toolbox = yield gDevTools.showToolbox(target, "inspector");
yield waitForToolboxFrameFocus(toolbox);
inspector = toolbox.getPanel("inspector");
info("Waiting for the inspector to update");
if (inspector._updateProgress) {
yield inspector.once("inspector-updated");
}
return {
toolbox: toolbox,
inspector: inspector
};
});
/**
* Wait for the toolbox frame to receive focus after it loads
*
* @param {Toolbox} toolbox
* @return a promise that resolves when focus has been received
*/
function waitForToolboxFrameFocus(toolbox) {
info("Making sure that the toolbox's frame is focused");
let def = promise.defer();
let win = toolbox.frame.contentWindow;
waitForFocus(def.resolve, win);
return def.promise;
}
/**
* Open the toolbox, with the inspector tool visible, and the sidebar that
* corresponds to the given id selected
*
* @return a promise that resolves when the inspector is ready and the sidebar
* view is visible and ready
*/
var openInspectorSideBar = Task.async(function*(id) {
let {toolbox, inspector} = yield openInspector();
info("Selecting the " + id + " sidebar");
inspector.sidebar.select(id);
return {
toolbox: toolbox,
inspector: inspector,
view: inspector[id].view
};
});
/**
* Open the toolbox, with the inspector tool visible, and the computed-view
* sidebar tab selected.
*
* @return a promise that resolves when the inspector is ready and the computed
* view is visible and ready
*/
function openComputedView() {
return openInspectorSideBar("computedview");
}
/**
* Open the toolbox, with the inspector tool visible, and the rule-view
* sidebar tab selected.
*
* @return a promise that resolves when the inspector is ready and the rule
* view is visible and ready
*/
function openRuleView() {
return openInspectorSideBar("ruleview");
}
/**
* Wait for eventName on target to be delivered a number of times.
*
* @param {Object} target
* An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Number} numTimes
* Number of deliveries to wait for.
* @param {Boolean} useCapture
* Optional, for addEventListener/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function waitForNEvents(target, eventName, numTimes, useCapture = false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
let deferred = promise.defer();
let count = 0;
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
["on", "off"]
]) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
if (++count == numTimes) {
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}
}, useCapture);
break;
}
}
return deferred.promise;
}
/**
* Wait for eventName on target.
*
* @param {Object} target
* An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Boolean} useCapture
* Optional, for addEventListener/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function once(target, eventName, useCapture=false) {
return waitForNEvents(target, eventName, 1, useCapture);
}
/**
* This shouldn't be used in the tests, but is useful when writing new tests or
* debugging existing tests in order to introduce delays in the test steps
*
* @param {Number} ms
* The time to wait
* @return A promise that resolves when the time is passed
*/
function wait(ms) {
let def = promise.defer();
content.setTimeout(def.resolve, ms);
return def.promise;
}
var _addTab = addTab;
addTab = function(url) {
return _addTab(url).then(tab => {
info("Loading the helper frame script " + FRAME_SCRIPT_URL);
let browser = tab.linkedBrowser;
browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
return tab;
});
};
/**
* Wait for a content -> chrome message on the message manager (the window
@ -492,39 +228,6 @@ function assertHoverTooltipOn(tooltip, element) {
});
}
/**
* Listen for a new tab to open and return a promise that resolves when one
* does and completes the load event.
*
* @return a promise that resolves to the tab object
*/
var waitForTab = Task.async(function*() {
info("Waiting for a tab to open");
yield once(gBrowser.tabContainer, "TabOpen");
let tab = gBrowser.selectedTab;
let browser = tab.linkedBrowser;
yield once(browser, "load", true);
info("The tab load completed");
return tab;
});
/**
* @see SimpleTest.waitForClipboard
*
* @param {Function} setup
* Function to execute before checking for the
* clipboard content
* @param {String|Boolean} expected
* An expected string or validator function
* @return a promise that resolves when the expected string has been found or
* the validator function has returned true, rejects otherwise.
*/
function waitForClipboard(setup, expected) {
let def = promise.defer();
SimpleTest.waitForClipboard(expected, setup, def.resolve, def.reject);
return def.promise;
}
/**
* Polls a given function waiting for it to return true.
*
@ -554,36 +257,6 @@ function waitForSuccess(validatorFn, name="untitled") {
return def.promise;
}
/**
* Create a new style tag containing the given style text and append it to the
* document's head node
*
* @param {Document} doc
* @param {String} style
* @return {DOMNode} The newly created style node
*/
function addStyle(doc, style) {
info("Adding a new style tag to the document with style content: " +
style.substring(0, 50));
let node = doc.createElement("style");
node.setAttribute("type", "text/css");
node.textContent = style;
doc.getElementsByTagName("head")[0].appendChild(node);
return node;
}
/**
* Checks whether the inspector's sidebar corresponding to the given id already
* exists
*
* @param {InspectorPanel}
* @param {String}
* @return {Boolean}
*/
function hasSideBarTab(inspector, id) {
return !!inspector.sidebar.getWindowForTab(id);
}
/**
* Get the dataURL for the font family tooltip.
*
@ -602,20 +275,6 @@ var getFontFamilyDataURL = Task.async(function*(font, nodeFront) {
return dataURL;
});
/**
* Simulate the key input for the given input in the window.
*
* @param {String} input
* The string value to input
* @param {Window} win
* The window containing the panel
*/
function synthesizeKeys(input, win) {
for (let key of input.split("")) {
EventUtils.synthesizeKey(key, {}, win);
}
}
/* *********************************************
* RULE-VIEW
* *********************************************
@ -921,77 +580,3 @@ function getComputedViewPropertyValue(view, name, propertyName) {
return getComputedViewProperty(view, name, propertyName)
.valueSpan.textContent;
}
/* *********************************************
* STYLE-EDITOR
* *********************************************
* Style-editor related utility functions.
*/
/**
* Wait for the toolbox to emit the styleeditor-selected event and when done
* wait for the stylesheet identified by href to be loaded in the stylesheet
* editor
*
* @param {Toolbox} toolbox
* @param {String} href
* Optional, if not provided, wait for the first editor to be ready
* @return a promise that resolves to the editor when the stylesheet editor is
* ready
*/
function waitForStyleEditor(toolbox, href) {
let def = promise.defer();
info("Waiting for the toolbox to switch to the styleeditor");
toolbox.once("styleeditor-selected").then(() => {
let panel = toolbox.getCurrentPanel();
ok(panel && panel.UI, "Styleeditor panel switched to front");
// A helper that resolves the promise once it receives an editor that
// matches the expected href. Returns false if the editor was not correct.
let gotEditor = (event, editor) => {
let currentHref = editor.styleSheet.href;
if (!href || (href && currentHref.endsWith(href))) {
info("Stylesheet editor selected");
panel.UI.off("editor-selected", gotEditor);
editor.getSourceEditor().then(sourceEditor => {
info("Stylesheet editor fully loaded");
def.resolve(sourceEditor);
});
return true;
}
info("The editor was incorrect. Waiting for editor-selected event.");
return false;
};
// The expected editor may already be selected. Check the if the currently
// selected editor is the expected one and if not wait for an
// editor-selected event.
if (!gotEditor("styleeditor-selected", panel.UI.selectedEditor)) {
// The expected editor is not selected (yet). Wait for it.
panel.UI.on("editor-selected", gotEditor);
}
});
return def.promise;
}
/**
* Reload the current page and wait for the inspector to be initialized after
* the navigation
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return a promise that resolves after page reload and inspector
* initialization
*/
function reloadPage(inspector) {
let onNewRoot = inspector.once("new-root");
content.location.reload();
return onNewRoot.then(() => {
inspector.markup._waitForChildren();
});
}

View File

@ -137,6 +137,20 @@ var selectNode = Task.async(function*(selector, inspector, reason="test") {
yield updated;
});
/**
* Set the inspector's current selection to null so that no node is selected
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return a promise that resolves when the inspector is updated
*/
function clearCurrentNodeSelection(inspector) {
info("Clearing the current selection");
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(null);
return updated;
}
/**
* Open the inspector in a tab with given URL.
* @param {string} url The URL to open.
@ -210,12 +224,13 @@ var clickOnInspectMenuItem = Task.async(function*(testActor, selector) {
/**
* Open the toolbox, with the inspector tool visible, and the one of the sidebar
* tabs selected.
* @param {String} id The ID of the sidebar tab to be opened
* @param {String} hostType Optional hostType, as defined in Toolbox.HostType
*
* @param {String} id
* The ID of the sidebar tab to be opened
* @return a promise that resolves when the inspector is ready and the tab is
* visible and ready
*/
var openInspectorSidebarTab = Task.async(function*(id, hostType) {
var openInspectorSidebarTab = Task.async(function* (id) {
let {toolbox, inspector, testActor} = yield openInspector();
info("Selecting the " + id + " sidebar");
@ -228,6 +243,66 @@ var openInspectorSidebarTab = Task.async(function*(id, hostType) {
};
});
/**
* Open the toolbox, with the inspector tool visible, and the rule-view
* sidebar tab selected.
*
* @return a promise that resolves when the inspector is ready and the rule view
* is visible and ready
*/
function openRuleView() {
return openInspectorSidebarTab("ruleview").then(data => {
return {
toolbox: data.toolbox,
inspector: data.inspector,
testActor: data.testActor,
view: data.inspector.ruleview.view
};
});
}
/**
* Open the toolbox, with the inspector tool visible, and the computed-view
* sidebar tab selected.
*
* @return a promise that resolves when the inspector is ready and the computed
* view is visible and ready
*/
function openComputedView() {
return openInspectorSidebarTab("computedview").then(data => {
return {
toolbox: data.toolbox,
inspector: data.inspector,
testActor: data.testActor,
view: data.inspector.computedview.view
};
});
}
/**
* Select the rule view sidebar tab on an already opened inspector panel.
*
* @param {InspectorPanel} inspector
* The opened inspector panel
* @return {CssRuleView} the rule view
*/
function selectRuleView(inspector) {
inspector.sidebar.select("ruleview");
return inspector.ruleview.view;
}
/**
* Select the computed view sidebar tab on an already opened inspector panel.
*
* @param {InspectorPanel} inspector
* The opened inspector panel
* @return {CssComputedView} the computed view
*/
function selectComputedView(inspector) {
inspector.sidebar.select("computedview");
return inspector.computedview.view;
}
/**
* Get the NodeFront for a node that matches a given css selector, via the
* protocol.
@ -672,3 +747,33 @@ function containsFocus(doc, container) {
}
return false;
}
/**
* Listen for a new tab to open and return a promise that resolves when one
* does and completes the load event.
*
* @return a promise that resolves to the tab object
*/
var waitForTab = Task.async(function*() {
info("Waiting for a tab to open");
yield once(gBrowser.tabContainer, "TabOpen");
let tab = gBrowser.selectedTab;
let browser = tab.linkedBrowser;
yield once(browser, "load", true);
info("The tab load completed");
return tab;
});
/**
* Simulate the key input for the given input in the window.
*
* @param {String} input
* The string value to input
* @param {Window} win
* The window containing the panel
*/
function synthesizeKeys(input, win) {
for (let key of input.split("")) {
EventUtils.synthesizeKey(key, {}, win);
}
}

View File

@ -142,7 +142,7 @@
.theme-dark .tabs .tabs-menu-item:hover,
.theme-light .tabs .tabs-menu-item:hover {
background-color: rgba(170,170,170,.2);
background-color: var(--toolbar-tab-hover);
}
.theme-dark .tabs .tabs-menu-item.is-active,
@ -161,7 +161,7 @@
.theme-dark .tabs .tabs-menu-item:active:hover,
.theme-light .tabs .tabs-menu-item:active:hover {
background-color: rgba(170,170,170,.4);
background-color: var(--toolbar-tab-hover-active);
}
.theme-dark .tabs .tabs-menu-item.is-active,
@ -190,7 +190,7 @@
}
.theme-dark .tabs .tabs-menu-item:active:hover {
background-color: hsla(206,37%,4%,.4);
background-color: hsla(206, 37%, 4%, .4); /* --toolbar-tab-hover-active */
}
.theme-dark .tabs .tabs-menu-item.is-active {

View File

@ -79,7 +79,7 @@
margin: 1px 2px 1px 2px;
border: none;
border-radius: 0;
background-color: rgba(170, 170, 170, .2); /* Splitter */
background-color: rgba(170, 170, 170, .2); /* --toolbar-tab-hover */
transition: background 0.05s ease-in-out;
}
@ -90,5 +90,5 @@
.theme-dark .toolbar .btn:not([disabled]):hover:active,
.theme-light .toolbar .btn:not([disabled]):hover:active {
background: rgba(170, 170, 170, .4); /* Splitters */
background: rgba(170, 170, 170, .4); /* --toolbar-tab-hover-active */
}

View File

@ -11,17 +11,24 @@
# A good criteria is the language in which you'd find the best
# documentation on web development on the web.
# LOCALIZATION NOTE (responsive.title): the title displayed in the global
# toolbar
responsive.title=Responsive Design Mode
# LOCALIZATION NOTE (responsive.editDeviceList): option displayed in the device
# selector
responsive.editDeviceList=Edit list…
# LOCALIZATION NOTE (responsive.exit): tooltip text of the exit button.
responsive.exit=Close Responsive Design Mode
# LOCALIZATION NOTE (responsive.done): button text in the device list modal
responsive.done=Done
# LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
# device selector
responsive.noDeviceSelected=no device selected
# LOCALIZATION NOTE (responsive.title): the title displayed in the global
# toolbar
responsive.title=Responsive Design Mode
# LOCALIZATION NOTE (responsive.screenshot): tooltip of the screenshot button.
responsive.screenshot=Take a screenshot of the viewport

View File

@ -7,6 +7,8 @@
const {
ADD_DEVICE,
ADD_DEVICE_TYPE,
UPDATE_DEVICE_DISPLAYED,
UPDATE_DEVICE_MODAL_OPEN,
} = require("./index");
module.exports = {
@ -26,4 +28,20 @@ module.exports = {
};
},
updateDeviceDisplayed(device, deviceType, displayed) {
return {
type: UPDATE_DEVICE_DISPLAYED,
device,
deviceType,
displayed,
};
},
updateDeviceModalOpen(isOpen) {
return {
type: UPDATE_DEVICE_MODAL_OPEN,
isOpen,
};
},
};

View File

@ -38,6 +38,12 @@ createEnum([
// Indicates when the screenshot action ends.
"TAKE_SCREENSHOT_END",
// Update the device display state in the device selector.
"UPDATE_DEVICE_DISPLAYED",
// Update the device modal open state.
"UPDATE_DEVICE_MODAL_OPEN",
], module.exports);
/**

View File

@ -6,21 +6,23 @@
"use strict";
const HTML_NS = "http://www.w3.org/1999/xhtml";
const {
TAKE_SCREENSHOT_START,
TAKE_SCREENSHOT_END,
} = require("./index");
const { getRect } = require("devtools/shared/layout/utils");
const { getFormatStr } = require("../utils/l10n");
const { getToplevelWindow } = require("sdk/window/utils");
const { Task: { spawn, async } } = require("resource://gre/modules/Task.jsm");
const { Task: { spawn } } = require("resource://gre/modules/Task.jsm");
const e10s = require("../utils/e10s");
const BASE_URL = "resource://devtools/client/responsive.html";
const audioCamera = new window.Audio(`${BASE_URL}/audio/camera-click.mp3`);
const animationFrame = () => new Promise(resolve => {
window.requestAnimationFrame(resolve);
});
function getFileName() {
let date = new Date();
let month = ("0" + (date.getMonth() + 1)).substr(-2);
@ -33,17 +35,9 @@ function getFileName() {
}
function createScreenshotFor(node) {
let { top, left, width, height } = getRect(window, node, window);
let mm = node.frameLoader.messageManager;
const canvas = document.createElementNS(HTML_NS, "canvas");
const ctx = canvas.getContext("2d");
const ratio = window.devicePixelRatio;
canvas.width = width * ratio;
canvas.height = height * ratio;
ctx.scale(ratio, ratio);
ctx.drawWindow(window, left, top, width, height, "#fff");
return canvas.toDataURL("image/png", "");
return e10s.request(mm, "RequestScreenshot");
}
function saveToFile(data, filename) {
@ -73,17 +67,16 @@ module.exports = {
// Waiting the next repaint, to ensure the react components
// can be properly render after the action dispatched above
window.requestAnimationFrame(async(function* () {
let iframe = document.querySelector("iframe");
let data = createScreenshotFor(iframe);
yield animationFrame();
simulateCameraEffects(iframe);
let iframe = document.querySelector("iframe");
let data = yield createScreenshotFor(iframe);
yield saveToFile(data, getFileName());
simulateCameraEffects(iframe);
dispatch({ type: TAKE_SCREENSHOT_END });
}));
yield saveToFile(data, getFileName());
dispatch({ type: TAKE_SCREENSHOT_END });
};
}
};

View File

@ -10,15 +10,21 @@ const { createClass, createFactory, PropTypes, DOM: dom } =
require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const {
updateDeviceDisplayed,
updateDeviceModalOpen,
} = require("./actions/devices");
const {
changeDevice,
resizeViewport,
rotateViewport
} = require("./actions/viewports");
const { takeScreenshot } = require("./actions/screenshot");
const Types = require("./types");
const Viewports = createFactory(require("./components/viewports"));
const DeviceModal = createFactory(require("./components/device-modal"));
const GlobalToolbar = createFactory(require("./components/global-toolbar"));
const Viewports = createFactory(require("./components/viewports"));
const { updateDeviceList } = require("./devices");
const Types = require("./types");
let App = createClass({
propTypes: {
@ -46,6 +52,10 @@ let App = createClass({
}, "*");
},
onDeviceListUpdate(devices) {
updateDeviceList(devices);
},
onExit() {
window.postMessage({ type: "exit" }, "*");
},
@ -62,6 +72,14 @@ let App = createClass({
this.props.dispatch(takeScreenshot());
},
onUpdateDeviceDisplayed(device, deviceType, displayed) {
this.props.dispatch(updateDeviceDisplayed(device, deviceType, displayed));
},
onUpdateDeviceModalOpen(isOpen) {
this.props.dispatch(updateDeviceModalOpen(isOpen));
},
render() {
let {
devices,
@ -74,10 +92,13 @@ let App = createClass({
onBrowserMounted,
onChangeViewportDevice,
onContentResize,
onDeviceListUpdate,
onExit,
onResizeViewport,
onRotateViewport,
onScreenshot,
onUpdateDeviceDisplayed,
onUpdateDeviceModalOpen,
} = this;
return dom.div(
@ -99,6 +120,13 @@ let App = createClass({
onContentResize,
onRotateViewport,
onResizeViewport,
onUpdateDeviceModalOpen,
}),
DeviceModal({
devices,
onDeviceListUpdate,
onUpdateDeviceDisplayed,
onUpdateDeviceModalOpen,
})
);
},

View File

@ -13,7 +13,7 @@ const { DOM: dom, createClass, addons, PropTypes } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const { waitForMessage } = require("../utils/e10s");
const e10s = require("../utils/e10s");
module.exports = createClass({
/**
@ -45,9 +45,9 @@ module.exports = createClass({
// quite the same timing as when we _set_ a new size around the browser,
// since it still needs to do async work before the content is actually
// resized to match.
mm.addMessageListener("ResponsiveMode:OnContentResize", onContentResize);
e10s.on(mm, "OnContentResize", onContentResize);
let ready = waitForMessage(mm, "ResponsiveMode:ChildScriptReady");
let ready = e10s.once(mm, "ChildScriptReady");
mm.loadFrameScript("resource://devtools/client/responsivedesign/" +
"responsivedesign-child.js", true);
yield ready;
@ -55,13 +55,12 @@ module.exports = createClass({
let browserWindow = getToplevelWindow(window);
let requiresFloatingScrollbars =
!browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
let started = waitForMessage(mm, "ResponsiveMode:Start:Done");
mm.sendAsyncMessage("ResponsiveMode:Start", {
yield e10s.request(mm, "Start", {
requiresFloatingScrollbars,
// Tests expect events on resize to yield on various size changes
notifyOnResize: DevToolsUtils.testing,
});
yield started;
// manager.js waits for this signal before allowing browser tests to start
this.props.onBrowserMounted();
@ -71,8 +70,8 @@ module.exports = createClass({
let { onContentResize } = this;
let browser = this.refs.browserContainer.querySelector("iframe.browser");
let mm = browser.frameLoader.messageManager;
mm.removeMessageListener("ResponsiveMode:OnContentResize", onContentResize);
mm.sendAsyncMessage("ResponsiveMode:Stop");
e10s.off(mm, "OnContentResize", onContentResize);
e10s.emit(mm, "Stop");
},
onContentResize(msg) {

View File

@ -0,0 +1,139 @@
/* 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 { DOM: dom, createClass, PropTypes, addons } =
require("devtools/client/shared/vendor/react");
const { getStr } = require("../utils/l10n");
const Types = require("../types");
module.exports = createClass({
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
onDeviceListUpdate: PropTypes.func.isRequired,
onUpdateDeviceDisplayed: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
displayName: "DeviceModal",
mixins: [ addons.PureRenderMixin ],
getInitialState() {
return {};
},
componentWillReceiveProps(nextProps) {
let {
devices,
} = nextProps;
for (let type of devices.types) {
for (let device of devices[type]) {
this.setState({
[device.name]: device.displayed,
});
}
}
},
onDeviceCheckboxClick({ target }) {
this.setState({
[target.value]: !this.state[target.value]
});
},
onDeviceModalSubmit() {
let {
devices,
onDeviceListUpdate,
onUpdateDeviceDisplayed,
onUpdateDeviceModalOpen,
} = this.props;
let displayedDeviceList = [];
for (let type of devices.types) {
for (let device of devices[type]) {
if (this.state[device.name] != device.displayed) {
onUpdateDeviceDisplayed(device, type, this.state[device.name]);
}
if (this.state[device.name]) {
displayedDeviceList.push(device.name);
}
}
}
onDeviceListUpdate(displayedDeviceList);
onUpdateDeviceModalOpen(false);
},
render() {
let {
devices,
onUpdateDeviceModalOpen,
} = this.props;
let modalClass = "device-modal container";
if (!devices.isModalOpen) {
modalClass += " hidden";
}
return dom.div(
{
className: modalClass,
},
dom.button({
id: "device-close-button",
className: "toolbar-button devtools-button",
onClick: () => onUpdateDeviceModalOpen(false),
}),
dom.div(
{
className: "device-modal-content",
},
devices.types.map(type => {
return dom.div(
{
className: "device-type",
key: type,
},
dom.header(
{
className: "device-header",
},
type
),
devices[type].map(device => {
return dom.label(
{
className: "device-label",
key: device.name,
},
dom.input({
className: "device-input-checkbox",
type: "checkbox",
value: device.name,
checked: this.state[device.name],
onChange: this.onDeviceCheckboxClick,
}),
device.name
);
})
);
})
),
dom.button(
{
id: "device-submit-button",
onClick: this.onDeviceModalSubmit,
},
getStr("responsive.done")
)
);
},
});

View File

@ -9,6 +9,7 @@ const { DOM: dom, createClass, PropTypes, addons } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
module.exports = createClass({
propTypes: {
@ -16,6 +17,7 @@ module.exports = createClass({
selectedDevice: PropTypes.string.isRequired,
onChangeViewportDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
displayName: "DeviceSelector",
@ -27,8 +29,14 @@ module.exports = createClass({
devices,
onChangeViewportDevice,
onResizeViewport,
onUpdateDeviceModalOpen,
} = this.props;
if (target.value === OPEN_DEVICE_MODAL_VALUE) {
onUpdateDeviceModalOpen(true);
return;
}
for (let type of devices.types) {
for (let device of devices[type]) {
if (device.name === target.value) {
@ -50,7 +58,9 @@ module.exports = createClass({
let options = [];
for (let type of devices.types) {
for (let device of devices[type]) {
options.push(device);
if (device.displayed) {
options.push(device);
}
}
}
@ -75,7 +85,10 @@ module.exports = createClass({
key: device.name,
value: device.name,
}, device.name);
})
}),
dom.option({
value: OPEN_DEVICE_MODAL_VALUE,
}, getStr("responsive.editDeviceList"))
);
},

View File

@ -30,7 +30,7 @@ module.exports = createClass({
return dom.header(
{
id: "global-toolbar",
className: "toolbar",
className: "container",
},
dom.span(
{

View File

@ -6,6 +6,7 @@
DevToolsModules(
'browser.js',
'device-modal.js',
'device-selector.js',
'global-toolbar.js',
'resizable-viewport.js',

View File

@ -28,6 +28,7 @@ module.exports = createClass({
onContentResize: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
displayName: "ResizableViewport",
@ -123,6 +124,7 @@ module.exports = createClass({
onContentResize,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
} = this.props;
let resizeHandleClass = "viewport-resize-handle";
@ -145,6 +147,7 @@ module.exports = createClass({
onChangeViewportDevice,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
}),
dom.div(
{

View File

@ -17,6 +17,7 @@ module.exports = createClass({
onChangeViewportDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
displayName: "ViewportToolbar",
@ -30,17 +31,19 @@ module.exports = createClass({
onChangeViewportDevice,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
} = this.props;
return dom.div(
{
className: "toolbar viewport-toolbar",
className: "viewport-toolbar container",
},
DeviceSelector({
devices,
selectedDevice,
onChangeViewportDevice,
onResizeViewport,
onUpdateDeviceModalOpen,
}),
dom.button({
className: "viewport-rotate-button toolbar-button devtools-button",

View File

@ -22,6 +22,7 @@ module.exports = createClass({
onContentResize: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
displayName: "Viewport",
@ -59,8 +60,9 @@ module.exports = createClass({
location,
screenshot,
viewport,
onContentResize,
onBrowserMounted,
onContentResize,
onUpdateDeviceModalOpen,
} = this.props;
let {
@ -83,6 +85,7 @@ module.exports = createClass({
onContentResize,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
}),
ViewportDimension({
viewport,

View File

@ -21,6 +21,7 @@ module.exports = createClass({
onContentResize: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
displayName: "Viewports",
@ -36,6 +37,7 @@ module.exports = createClass({
onContentResize,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
} = this.props;
return dom.div(
@ -54,6 +56,7 @@ module.exports = createClass({
onContentResize,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
});
})
);

View File

@ -0,0 +1,81 @@
/* 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 Services = require("Services");
const { Task } = require("resource://gre/modules/Task.jsm");
const { GetDevices } = require("devtools/client/shared/devices");
const { addDevice, addDeviceType } = require("./actions/devices");
const DISPLAYED_DEVICES_PREF = "devtools.responsive.html.displayedDeviceList";
/**
* Get the device catalog and load the devices onto the store.
*
* @param {Function} dispatch
* Action dispatch function
*/
let initDevices = Task.async(function* (dispatch) {
let deviceList = loadDeviceList();
let devices = yield GetDevices();
for (let type of devices.TYPES) {
dispatch(addDeviceType(type));
for (let device of devices[type]) {
if (device.os == "fxos") {
continue;
}
let newDevice = Object.assign({}, device, {
displayed: deviceList.includes(device.name) ?
true :
!!device.featured,
});
if (newDevice.displayed) {
deviceList.push(newDevice.name);
}
dispatch(addDevice(newDevice, type));
}
}
updateDeviceList(deviceList);
});
/**
* Returns an array containing the user preference of displayed devices.
*
* @return {Array} containing the device names that are to be displayed in the
* device catalog.
*/
function loadDeviceList() {
let deviceList = [];
if (Services.prefs.prefHasUserValue(DISPLAYED_DEVICES_PREF)) {
try {
deviceList = JSON.parse(Services.prefs.getCharPref(
DISPLAYED_DEVICES_PREF));
} catch (e) {
console.error(e);
}
}
return deviceList;
}
/**
* Update the displayed device list preference with the given device list.
*
* @param {Array} devices
* Array of device names that are displayed in the device catalog.
*/
function updateDeviceList(devices) {
Services.prefs.setCharPref(DISPLAYED_DEVICES_PREF, JSON.stringify(devices));
}
exports.initDevices = initDevices;
exports.loadDeviceList = loadDeviceList;
exports.updateDeviceList = updateDeviceList;

View File

@ -6,7 +6,9 @@
*/
.theme-light {
--box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
--rdm-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
--submit-button-active-background-color: rgba(0,0,0,0.12);
--submit-button-active-color: var(--theme-body-color);
--viewport-active-color: var(--theme-body-color);
--viewport-selection-arrow: url("./images/select-arrow.svg#light");
--viewport-selection-arrow-selected:
@ -14,7 +16,9 @@
}
.theme-dark {
--box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
--rdm-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
--submit-button-active-background-color: var(--toolbar-tab-hover-active);
--submit-button-active-color: var(--theme-selection-color);
--viewport-active-color: var(--theme-selection-color);
--viewport-selection-arrow: url("./images/select-arrow.svg#dark");
--viewport-selection-arrow-selected:
@ -51,10 +55,10 @@ html, body {
}
/**
* Common style for toolbars and toolbar buttons
* Common style for containers and toolbar buttons
*/
.toolbar {
.container {
background-color: var(--theme-toolbar-background);
border: 1px solid var(--theme-splitter-color);
}
@ -80,7 +84,7 @@ html, body {
#global-toolbar {
color: var(--theme-body-color-alt);
border-radius: 2px;
box-shadow: var(--box-shadow);
box-shadow: var(--rdm-box-shadow);
margin: 30px 0;
padding: 4px 5px;
display: inline-flex;
@ -142,7 +146,7 @@ html, body {
.resizable-viewport {
border: 1px solid var(--theme-splitter-color);
box-shadow: var(--box-shadow);
box-shadow: var(--rdm-box-shadow);
position: relative;
}
@ -308,3 +312,88 @@ html, body {
.viewport-dimension-separator {
-moz-user-select: none;
}
/**
* Device Modal
*/
.device-modal {
border-radius: 2px;
box-shadow: var(--rdm-box-shadow);
position: absolute;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 642px;
height: 612px;
}
.device-modal.hidden {
display: none;
}
.device-modal-content {
display: flex;
flex-direction: column;
flex-wrap: wrap;
overflow: auto;
height: 550px;
width: 600px;
margin: 20px;
}
#device-close-button,
#device-close-button::before {
position: absolute;
top: 5px;
right: 2px;
width: 12px;
height: 12px;
}
#device-close-button::before {
background-image: url("./images/close.svg");
margin: -6px 0 0 -6px;
}
.device-type {
display: flex;
flex-direction: column;
padding: 10px;
}
.device-header {
font-weight: bold;
text-transform: capitalize;
padding: 0 0 3px 23px;
}
.device-label {
padding-bottom: 3px;
}
.device-input-checkbox {
margin-right: 5px;
}
#device-submit-button {
background-color: var(--theme-tab-toolbar-background);
border-width: 1px 0 0 0;
border-top-width: 1px;
border-top-style: solid;
border-top-color: var(--theme-splitter-color);
color: var(--theme-body-color);
width: 100%;
height: 20px;
}
#device-submit-button:hover {
background-color: var(--toolbar-tab-hover);
}
#device-submit-button:hover:active {
background-color: var(--submit-button-active-background-color);
color: var(--submit-button-active-color);
}

View File

@ -13,7 +13,7 @@ const { require } = BrowserLoader({
baseURI: "resource://devtools/client/responsive.html/",
window: this
});
const { GetDevices } = require("devtools/client/shared/devices");
const { Task } = require("resource://gre/modules/Task.jsm");
const Telemetry = require("devtools/client/shared/telemetry");
const { loadSheet } = require("sdk/stylesheet/utils");
@ -22,9 +22,9 @@ const { createFactory, createElement } =
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
const { initDevices } = require("./devices");
const App = createFactory(require("./app"));
const Store = require("./store");
const { addDevice, addDeviceType } = require("./actions/devices");
const { changeLocation } = require("./actions/location");
const { addViewport, resizeViewport } = require("./actions/viewports");
@ -34,7 +34,7 @@ let bootstrap = {
store: null,
init() {
init: Task.async(function* () {
// Load a special UA stylesheet to reset certain styles such as dropdown
// lists.
loadSheet(window,
@ -42,11 +42,11 @@ let bootstrap = {
"agent");
this.telemetry.toolOpened("responsive");
let store = this.store = Store();
yield initDevices(this.dispatch.bind(this));
let provider = createElement(Provider, { store }, App());
ReactDOM.render(provider, document.querySelector("#root"));
this.initDevices();
window.postMessage({ type: "init" }, "*");
},
}),
destroy() {
this.store = null;
@ -69,19 +69,6 @@ let bootstrap = {
this.store.dispatch(action);
},
initDevices() {
GetDevices().then(devices => {
for (let type of devices.TYPES) {
this.dispatch(addDeviceType(type));
for (let device of devices[type]) {
if (device.os != "fxos") {
this.dispatch(addDevice(device, type));
}
}
}
});
},
};
window.addEventListener("load", function onLoad() {

View File

@ -16,6 +16,7 @@ DIRS += [
DevToolsModules(
'app.js',
'constants.js',
'devices.js',
'index.css',
'manager.js',
'reducers.js',

View File

@ -7,10 +7,13 @@
const {
ADD_DEVICE,
ADD_DEVICE_TYPE,
UPDATE_DEVICE_DISPLAYED,
UPDATE_DEVICE_MODAL_OPEN,
} = require("../actions/index");
const INITIAL_DEVICES = {
types: [],
isModalOpen: false,
};
let reducers = {
@ -28,6 +31,26 @@ let reducers = {
});
},
[UPDATE_DEVICE_DISPLAYED](devices, { device, deviceType, displayed }) {
let newDevices = devices[deviceType].map(d => {
if (d == device) {
d.displayed = displayed;
}
return d;
});
return Object.assign({}, devices, {
[deviceType]: newDevices,
});
},
[UPDATE_DEVICE_MODAL_OPEN](devices, { isOpen }) {
return Object.assign({}, devices, {
isModalOpen: isOpen,
});
},
};
module.exports = function(devices = INITIAL_DEVICES, action) {

View File

@ -9,6 +9,8 @@ support-files =
!/devtools/client/framework/test/shared-head.js
!/devtools/client/framework/test/shared-redux-head.js
[browser_device_modal_exit.js]
[browser_device_modal_submit.js]
[browser_device_width.js]
[browser_exit_button.js]
[browser_mouse_resize.js]

View File

@ -0,0 +1,38 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test submitting display device changes on the device modal
const TEST_URL = "data:text/html;charset=utf-8,";
addRDMTask(TEST_URL, function* ({ ui }) {
let { store, document } = ui.toolWindow;
let modal = document.querySelector(".device-modal");
let closeButton = document.querySelector("#device-close-button");
// Wait until the viewport has been added
yield waitUntilState(store, state => state.viewports.length == 1);
openDeviceModal(ui);
let deviceListBefore = loadDeviceList();
info("Check the first unchecked device and exit the modal.");
let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => !cb.checked)[0];
let value = uncheckedCb.value;
uncheckedCb.click();
closeButton.click();
ok(modal.classList.contains("hidden"),
"The device modal is hidden on exit.");
info("Check that the device list remains unchanged after exitting.");
let deviceListAfter = loadDeviceList();
is(deviceListBefore.length, deviceListAfter.length,
"Got expected number of displayed devices.");
ok(!deviceListAfter.includes(value),
value + " was not added to displayed device list.");
});

View File

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test submitting display device changes on the device modal
const TEST_URL = "data:text/html;charset=utf-8,";
addRDMTask(TEST_URL, function* ({ ui }) {
let { store, document } = ui.toolWindow;
let modal = document.querySelector(".device-modal");
let select = document.querySelector(".viewport-device-selector");
let submitButton = document.querySelector("#device-submit-button");
// Wait until the viewport has been added
yield waitUntilState(store, state => state.viewports.length == 1);
openDeviceModal(ui);
info("Checking displayed device checkboxes are checked in the device modal.");
let checkedCbs = [...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => cb.checked);
let deviceList = loadDeviceList();
is(deviceList.length, checkedCbs.length,
"Got expected number of displayed devices.");
for (let cb of checkedCbs) {
ok(deviceList.includes(cb.value), cb.value + " is correctly checked.");
}
info("Check the first unchecked device and submit new device list.");
let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => !cb.checked)[0];
let value = uncheckedCb.value;
uncheckedCb.click();
submitButton.click();
ok(modal.classList.contains("hidden"),
"The device modal is hidden on submit.");
info("Checking new device is added to the displayed device list.");
deviceList = loadDeviceList();
ok(deviceList.includes(value), value + " added to displayed device list.");
info("Checking new device is added to the device selector.");
let options = [...select.options];
is(options.length - 2, deviceList.length,
"Got expected number of devices in device selector.");
ok(options.filter(o => o.value === value)[0],
value + " added to the device selector.");
info("Reopen device modal and check new device is correctly checked");
openDeviceModal(ui);
ok([...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => cb.checked && cb.value === value)[0],
value + " is checked in the device modal.");
});

View File

@ -1,5 +1,5 @@
{
"TYPES": [ "phones" ],
"TYPES": [ "phones", "tablets", "laptops", "televisions", "consoles", "watches" ],
"phones": [
{
"name": "Firefox OS Flame",
@ -20,6 +20,632 @@
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Alcatel One Touch Fire C",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4019X; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Alcatel One Touch Fire E",
"width": 320,
"height": 480,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch6015X; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Apple iPhone 4",
"width": 320,
"height": 480,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPhone 5",
"width": 320,
"height": 568,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPhone 5s",
"width": 320,
"height": 568,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "Apple iPhone 6",
"width": 375,
"height": 667,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPhone 6 Plus",
"width": 414,
"height": 736,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "Apple iPhone 6s",
"width": 375,
"height": 667,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "Apple iPhone 6s Plus",
"width": 414,
"height": 736,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "BlackBerry Z30",
"width": 360,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
"touch": true,
"firefoxOS": false,
"os": "blackberryos"
},
{
"name": "Geeksphone Keon",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Geeksphone Peak, Revolution",
"width": 360,
"height": 640,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Google Nexus S",
"width": 320,
"height": 533,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Google Nexus 4",
"width": 384,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 4 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
"touch": true,
"firefoxOS": true,
"os": "android",
"featured": true
},
{
"name": "Google Nexus 5",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 5 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
"touch": true,
"firefoxOS": true,
"os": "android",
"featured": true
},
{
"name": "Google Nexus 6",
"width": 412,
"height": 732,
"pixelRatio": 3.5,
"userAgent": "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36",
"touch": true,
"firefoxOS": true,
"os": "android",
"featured": true
},
{
"name": "Intex Cloud Fx",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "KDDI Fx0",
"width": 360,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Mobile; LGL25; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "LG Fireweb",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; LG-D300; rv:18.1) Gecko/18.1 Firefox/18.1",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "LG Optimus L70",
"width": 384,
"height": 640,
"pixelRatio": 1.25,
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Nokia Lumia 520",
"width": 320,
"height": 533,
"pixelRatio": 1.4,
"userAgent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Nokia N9",
"width": 360,
"height": 640,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "OnePlus One",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Android 5.1.1; Mobile; rv:43.0) Gecko/43.0 Firefox/43.0",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy S3",
"width": 360,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy S4",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy S5",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Samsung Galaxy S6",
"width": 360,
"height": 640,
"pixelRatio": 4,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Sony Xperia Z3",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Spice Fire One Mi-FX1",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Symphony GoFox F15",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:30.0) Gecko/30.0 Firefox/30.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "ZTE Open",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; ZTEOPEN; rv:18.1) Gecko/18.0 Firefox/18.1",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "ZTE Open II",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; OPEN2; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "ZTE Open C",
"width": 320,
"height": 450,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; OPENC; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Zen Fire 105",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
}
],
"tablets": [
{
"name": "Amazon Kindle Fire HDX 8.9",
"width": 1280,
"height": 800,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
"touch": true,
"firefoxOS": false,
"os": "fireos",
"featured": true
},
{
"name": "Apple iPad",
"width": 1024,
"height": 768,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPad Air 2",
"width": 1024,
"height": 768,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "Apple iPad Mini",
"width": 1024,
"height": 768,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPad Mini 2",
"width": 1024,
"height": 768,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "BlackBerry PlayBook",
"width": 1024,
"height": 600,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
"touch": true,
"firefoxOS": false,
"os": "blackberryos"
},
{
"name": "Foxconn InFocus",
"width": 1280,
"height": 800,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Google Nexus 7",
"width": 960,
"height": 600,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Google Nexus 10",
"width": 1280,
"height": 800,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy Note 2",
"width": 360,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy Note 3",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Tesla Model S",
"width": 1200,
"height": 1920,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (X11; Linux) AppleWebKit/534.34 (KHTML, like Gecko) QtCarBrowser Safari/534.34",
"touch": true,
"firefoxOS": false,
"os": "linux"
},
{
"name": "VIA Vixen",
"width": 1024,
"height": 600,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
}
],
"laptops": [
{
"name": "Laptop (1366 x 768)",
"width": 1366,
"height": 768,
"pixelRatio": 1,
"userAgent": "",
"touch": false,
"firefoxOS": false,
"os": "windows",
"featured": true
},
{
"name": "Laptop (1920 x 1080)",
"width": 1280,
"height": 720,
"pixelRatio": 1.5,
"userAgent": "",
"touch": false,
"firefoxOS": false,
"os": "windows",
"featured": true
},
{
"name": "Laptop (1920 x 1080) with touch",
"width": 1280,
"height": 720,
"pixelRatio": 1.5,
"userAgent": "",
"touch": true,
"firefoxOS": false,
"os": "windows"
}
],
"televisions": [
{
"name": "720p HD Television",
"width": 1280,
"height": 720,
"pixelRatio": 1,
"userAgent": "",
"touch": false,
"firefoxOS": true,
"os": "custom"
},
{
"name": "1080p Full HD Television",
"width": 1920,
"height": 1080,
"pixelRatio": 1,
"userAgent": "",
"touch": false,
"firefoxOS": true,
"os": "custom"
},
{
"name": "4K Ultra HD Television",
"width": 3840,
"height": 2160,
"pixelRatio": 1,
"userAgent": "",
"touch": false,
"firefoxOS": true,
"os": "custom"
}
],
"consoles": [
{
"name": "Nintendo 3DS",
"width": 320,
"height": 240,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7585.EU",
"touch": true,
"firefoxOS": false,
"os": "nintendo"
},
{
"name": "Nintendo Wii U Gamepad",
"width": 854,
"height": 480,
"pixelRatio": 0.87,
"userAgent": "Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.15 NintendoBrowser/4.1.1.9601.EU",
"touch": true,
"firefoxOS": false,
"os": "nintendo"
},
{
"name": "Sony PlayStation Vita",
"width": 960,
"height": 544,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Playstation Vita 1.61) AppleWebKit/531.22.8 (KHTML, like Gecko) Silk/3.2",
"touch": true,
"firefoxOS": false,
"os": "playstation"
}
],
"watches": [
{
"name": "LG G Watch",
"width": 280,
"height": 280,
"pixelRatio": 1,
"userAgent": "",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "LG G Watch R",
"width": 320,
"height": 320,
"pixelRatio": 1,
"userAgent": "",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Motorola Moto 360",
"width": 320,
"height": 290,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Linux; Android 5.0.1; Moto 360 Build/LWX48T) AppleWebkit/537.36 (KHTML, like Gecko) Chrome/19.77.34.5 Mobile Safari/537.36",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Samsung Gear Live",
"width": 320,
"height": 320,
"pixelRatio": 1,
"userAgent": "",
"touch": true,
"firefoxOS": true,
"os": "android"
}
]
}

View File

@ -26,6 +26,7 @@ SimpleTest.requestCompleteLog();
SimpleTest.waitForExplicitFinish();
DevToolsUtils.testing = true;
Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
Services.prefs.setCharPref("devtools.devices.url",
TEST_URI_ROOT + "devices.json");
Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);
@ -34,8 +35,12 @@ registerCleanupFunction(() => {
DevToolsUtils.testing = false;
Services.prefs.clearUserPref("devtools.devices.url");
Services.prefs.clearUserPref("devtools.responsive.html.enabled");
Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
});
const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
const { loadDeviceList } = require("devtools/client/responsive.html/devices");
const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
/**
* Open responsive design mode for the given tab.
@ -125,3 +130,25 @@ var setViewportSize = Task.async(function* (ui, manager, width, height) {
yield resized;
}
});
function openDeviceModal(ui) {
let { document } = ui.toolWindow;
let select = document.querySelector(".viewport-device-selector");
let modal = document.querySelector(".device-modal");
let editDeviceOption = [...select.options].filter(o => {
return o.value === OPEN_DEVICE_MODAL_VALUE;
})[0];
info("Checking initial device modal state");
ok(modal.classList.contains("hidden"),
"The device modal is hidden by default.");
info("Opening device modal through device selector.");
EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"},
ui.toolWindow);
EventUtils.synthesizeMouseAtCenter(editDeviceOption, {type: "mouseup"},
ui.toolWindow);
ok(!modal.classList.contains("hidden"),
"The device modal is displayed.");
}

View File

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test updating the device `displayed` property
const {
addDevice,
addDeviceType,
updateDeviceDisplayed,
} = require("devtools/client/responsive.html/actions/devices");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
let device = {
"name": "Firefox OS Flame",
"width": 320,
"height": 570,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
};
dispatch(addDeviceType("phones"));
dispatch(addDevice(device, "phones"));
dispatch(updateDeviceDisplayed(device, "phones", true));
equal(getState().devices.phones.length, 1,
"Correct number of phones");
ok(getState().devices.phones[0].displayed,
"Device phone list contains enabled Firefox OS Flame");
});

View File

@ -11,3 +11,4 @@ firefox-appdir = browser
[test_change_viewport_device.js]
[test_resize_viewport.js]
[test_rotate_viewport.js]
[test_update_device_displayed.js]

View File

@ -32,9 +32,12 @@ const device = {
// Whether or not it is a touch device
touch: PropTypes.bool,
// The operating system of the device
// The operating system of the device
os: PropTypes.String,
// Whether or not the device is displayed in the device selector
displayed: PropTypes.bool,
};
/**
@ -63,6 +66,9 @@ exports.devices = {
// An array of watch devices
watches: PropTypes.arrayOf(PropTypes.shape(device)),
// Whether or not the device modal is open
isModalOpen: PropTypes.bool,
};
/**

View File

@ -4,20 +4,100 @@
"use strict";
const promise = require("promise");
const { defer } = require("promise");
module.exports = {
// The prefix used for RDM messages in content.
// see: devtools/client/responsivedesign/responsivedesign-child.js
const MESSAGE_PREFIX = "ResponsiveMode:";
const REQUEST_DONE_SUFFIX = ":Done";
waitForMessage(mm, message) {
let deferred = promise.defer();
/**
* Registers a message `listener` that is called every time messages of
* specified `message` is emitted on the given message manager.
* @param {nsIMessageListenerManager} mm
* The Message Manager
* @param {String} message
* The message. It will be prefixed with the constant `MESSAGE_PREFIX`
* @param {Function} listener
* The listener function that processes the message.
*/
function on(mm, message, listener) {
mm.addMessageListener(MESSAGE_PREFIX + message, listener);
}
exports.on = on;
let onMessage = event => {
mm.removeMessageListener(message, onMessage);
deferred.resolve();
};
mm.addMessageListener(message, onMessage);
/**
* Removes a message `listener` for the specified `message` on the given
* message manager.
* @param {nsIMessageListenerManager} mm
* The Message Manager
* @param {String} message
* The message. It will be prefixed with the constant `MESSAGE_PREFIX`
* @param {Function} listener
* The listener function that processes the message.
*/
function off(mm, message, listener) {
mm.removeMessageListener(MESSAGE_PREFIX + message, listener);
}
exports.off = off;
return deferred.promise;
},
/**
* Resolves a promise the next time the specified `message` is sent over the
* given message manager.
* @param {nsIMessageListenerManager} mm
* The Message Manager
* @param {String} message
* The message. It will be prefixed with the constant `MESSAGE_PREFIX`
* @returns {Promise}
* A promise that is resolved when the given message is emitted.
*/
function once(mm, message) {
let { resolve, promise } = defer();
};
on(mm, message, function onMessage({data}) {
off(mm, message, onMessage);
resolve(data);
});
return promise;
}
exports.once = once;
/**
* Asynchronously emit a `message` to the listeners of the given message
* manager.
*
* @param {nsIMessageListenerManager} mm
* The Message Manager
* @param {String} message
* The message. It will be prefixed with the constant `MESSAGE_PREFIX`.
* @param {Object} data
* A JSON object containing data to be delivered to the listeners.
*/
function emit(mm, message, data) {
mm.sendAsyncMessage(MESSAGE_PREFIX + message, data);
}
exports.emit = emit;
/**
* Asynchronously send a "request" over the given message manager, and returns
* a promise that is resolved when the request is complete.
*
* @param {nsIMessageListenerManager} mm
* The Message Manager
* @param {String} message
* The message. It will be prefixed with the constant `MESSAGE_PREFIX`, and
* also suffixed with `REQUEST_DONE_SUFFIX` for the reply.
* @param {Object} data
* A JSON object containing data to be delivered to the listeners.
* @returns {Promise}
* A promise that is resolved when the request is done.
*/
function request(mm, message, data) {
let done = once(mm, message + REQUEST_DONE_SUFFIX);
emit(mm, message, data);
return done;
}
exports.request = request;

View File

@ -150,12 +150,14 @@ var global = this;
function screenshot() {
let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
let width = content.innerWidth;
let height = content.innerHeight;
let ratio = content.devicePixelRatio;
let width = content.innerWidth * ratio;
let height = content.innerHeight * ratio;
canvas.mozOpaque = true;
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext("2d");
ctx.scale(ratio, ratio);
ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
sendAsyncMessage("ResponsiveMode:RequestScreenshot:Done", canvas.toDataURL());
}

View File

@ -3,13 +3,38 @@
* 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/. */
.theme-dark,
.theme-light {
--number-color: var(--theme-highlight-green);
--string-color: var(--theme-highlight-orange);
--null-color: var(--theme-comment);
--object-color: var(--theme-body-color);
--caption-color: var(--theme-highlight-blue);
--location-color: var(--theme-content-color1);
--source-link-color: var(--theme-highlight-blue);
--node-color: var(--theme-highlight-bluegrey);
--reference-color: var(--theme-highlight-purple);
}
.theme-firebug {
--number-color: #000088;
--string-color: #FF0000;
--null-color: #787878;
--object-color: DarkGreen;
--caption-color: #444444;
--location-color: #555555;
--source-link-color: blue;
--node-color: rgb(0, 0, 136);
--reference-color: rgb(102, 102, 255);
}
/******************************************************************************/
.objectLink:hover {
cursor: pointer;
text-decoration: underline;
}
/******************************************************************************/
.inline {
display: inline;
white-space: normal;
@ -17,7 +42,7 @@
.objectBox-object {
font-weight: bold;
color: DarkGreen;
color: var(--object-color);
white-space: pre-wrap;
}
@ -33,22 +58,22 @@
.objectLink-element,
.objectLink-textNode,
.objectBox-array > .length {
color: #000088;
color: var(--number-color);
}
.objectBox-string {
color: #FF0000;
color: var(--string-color);
}
.objectLink-function,
.objectBox-stackTrace,
.objectLink-profile {
color: DarkGreen;
color: var(--object-color);
}
.objectLink-Location {
font-style: italic;
color: #555555;
color: var(--location-color);
}
.objectBox-null,
@ -56,14 +81,7 @@
.objectBox-hint,
.logRowHint {
font-style: italic;
color: #787878;
}
.objectBox-scope {
color: #707070;
}
.objectBox-optimizedAway {
color: #909090;
color: var(--null-color);
}
.objectLink-sourceLink {
@ -72,12 +90,7 @@
top: 2px;
padding-left: 8px;
font-weight: bold;
color: #0000FF;
}
.objectLink-sourceLink > .systemLink {
float: right;
color: #FF0000;
color: var(--source-link-color);
}
/******************************************************************************/
@ -88,7 +101,7 @@
.objectLink-object,
.objectLink-Date {
font-weight: bold;
color: DarkGreen;
color: var(--object-color);
white-space: pre-wrap;
}
@ -101,7 +114,7 @@
.objectLink-NamedNodeMap .arrayRightBracket,
.objectLink-Attr .attrEqual,
.objectLink-Attr .attrTitle {
color: rgb(0, 0, 136)
color: var(--node-color);
}
.objectLink-object .nodeName {
@ -133,63 +146,36 @@
.objectLink-Reference {
font-weight: bold;
color: rgb(102, 102, 255);
color: var(--reference-color);
}
.objectBox-array > .objectTitle {
font-weight: bold;
color: DarkGreen;
color: var(--object-color);
}
/******************************************************************************/
.caption {
font-weight: bold;
color: #444444;
color: var(--caption-color);
}
/******************************************************************************/
/* Light Theme & Dark Theme */
.theme-dark .domLabel,
.theme-light .domLabel {
color: var(--theme-highlight-blue);
}
.theme-dark .objectBox-array .length,
.theme-light .objectBox-array .length,
.theme-dark .objectBox-number,
.theme-light .objectBox-number {
color: var(--theme-highlight-green);
}
.theme-dark .objectBox-string,
.theme-light .objectBox-string {
color: var(--theme-highlight-orange);
}
/* Themes */
.theme-dark .objectBox-null,
.theme-dark .objectBox-undefined,
.theme-light .objectBox-null,
.theme-light .objectBox-undefined {
font-style: normal;
color: var(--theme-comment);
}
.theme-dark .objectBox-array,
.theme-light .objectBox-array {
color: var(--theme-body-color);
}
.theme-dark .objectBox-object,
.theme-light .objectBox-object {
font-weight: normal;
color: var(--theme-highlight-blue);
white-space: pre-wrap;
}
.theme-dark .caption,
.theme-light .caption {
font-weight: normal;
color: var(--theme-highlight-blue);
}

View File

@ -46,10 +46,6 @@
text-decoration: underline;
}
.treeTable .treeRow:hover {
background-color: var(--theme-body-background);
}
/* Filtering */
.treeTable .treeRow.hidden {
display: none;
@ -82,17 +78,6 @@
background-size: 9px 9px !important;
}
/* Default toggle icon. The immediate children operator must be
used here since there might be nested tree components inside
a tree and we don't want to alter those. */
.treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
background-image: url(chrome://devtools/skin/images/firebug/twisty-closed-firebug.svg);
}
.treeTable .treeRow.hasChildren.opened > .treeLabelCell > .treeIcon {
background-image: url(chrome://devtools/skin/images/firebug/twisty-open-firebug.svg);
}
/******************************************************************************/
/* Header */
@ -149,40 +134,47 @@
/* Light Theme: toggle icon */
.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
background-image: url("chrome://devtools/skin/images/controls.png");
background-image: url(chrome://devtools/skin/images/controls.png);
background-size: 56px 28px;
background-position: 0 -14px;
}
.theme-light .treeTable .treeRow.hasChildren.opened > .treeLabelCell > .treeIcon {
background-image: url("chrome://devtools/skin/images/controls.png");
background-image: url(chrome://devtools/skin/images/controls.png);
background-size: 56px 28px;
background-position: -14px -14px;
}
/* Dark Theme: toggle icon */
.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
background-image: url("chrome://devtools/skin/images/controls.png");
background-image: url(chrome://devtools/skin/images/controls.png);
background-size: 56px 28px;
background-position: -28px -14px;
}
.theme-dark .treeTable .treeRow.hasChildren.opened > .treeLabelCell > .treeIcon {
background-image: url("chrome://devtools/skin/images/controls.png");
background-image: url(chrome://devtools/skin/images/controls.png);
background-size: 56px 28px;
background-position: -42px -14px;
}
.theme-dark .treeTable .treeRow:hover {
background-color: var(--theme-selection-background-semitransparent);
/* Dark and Light Themes: Support for retina displays */
@media (min-resolution: 1.1dppx) {
.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon,
.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
background-image: url("chrome://devtools/skin/images/controls@2x.png");
}
}
/* Dark and Light Themes: colors */
.theme-light .treeTable .treeRow:hover,
.theme-dark .treeTable .treeRow:hover {
background-color: var(--theme-selection-background) !important;
}
.theme-firebug .treeTable .treeRow:hover {
background-color: var(--theme-body-background);
}
.theme-light .treeTable .treeLabel,
.theme-dark .treeTable .treeLabel {
color: var(--theme-highlight-pink);
@ -192,10 +184,6 @@
color: var(--theme-highlight-pink);
}
/* Dark and Light Themes: Support for retina displays */
@media (min-resolution: 1.1dppx) {
.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon,
.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
background-image: url("chrome://devtools/skin/images/controls@2x.png");
}
.theme-firebug .treeTable .treeLabel {
color: var(--theme-body-color);
}

View File

@ -5,7 +5,7 @@ code, and optionally help with indentation.
# Upgrade
Currently used version is 5.13.0. To upgrade, download a new version of
Currently used version is 5.13.2. To upgrade, download a new version of
CodeMirror from the project's page [1] and replace all JavaScript and
CSS files inside the codemirror directory [2].

0
devtools/client/sourceeditor/codemirror/addon/comment/comment.js vendored Executable file → Normal file
View File

View File

0
devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/edit/closetag.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js vendored Executable file → Normal file
View File

0
devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js vendored Executable file → Normal file
View File

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