зеркало из https://github.com/mozilla/gecko-dev.git
merge fx-team to mozilla-central a=merge
This commit is contained in:
Коммит
311c76e3c8
|
@ -729,3 +729,16 @@ addEventListener("pageshow", function(event) {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
addMessageListener("ContextMenu:SaveVideoFrameAsImage", (message) => {
|
||||
let video = message.objects.target;
|
||||
let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
let ctxDraw = canvas.getContext("2d");
|
||||
ctxDraw.drawImage(video, 0, 0);
|
||||
sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", {
|
||||
dataURL: canvas.toDataURL("image/jpeg", ""),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1037,6 +1037,7 @@ nsContextMenu.prototype = {
|
|||
},
|
||||
|
||||
saveVideoFrameAsImage: function () {
|
||||
let mm = this.browser.messageManager;
|
||||
let name = "";
|
||||
if (this.mediaURL) {
|
||||
try {
|
||||
|
@ -1048,13 +1049,18 @@ nsContextMenu.prototype = {
|
|||
}
|
||||
if (!name)
|
||||
name = "snapshot.jpg";
|
||||
var video = this.target;
|
||||
var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
var ctxDraw = canvas.getContext("2d");
|
||||
ctxDraw.drawImage(video, 0, 0);
|
||||
saveImageURL(canvas.toDataURL("image/jpeg", ""), name, "SaveImageTitle", true, false, document.documentURIObject, this.target.ownerDocument);
|
||||
|
||||
mm.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage", {}, {
|
||||
target: this.target,
|
||||
});
|
||||
|
||||
let onMessage = (message) => {
|
||||
mm.removeMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
|
||||
let dataURL = message.data.dataURL;
|
||||
saveImageURL(dataURL, name, "SaveImageTitle", true, false,
|
||||
document.documentURIObject, this.target.ownerDocument);
|
||||
};
|
||||
mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
|
||||
},
|
||||
|
||||
fullScreenVideo: function () {
|
||||
|
|
|
@ -16,9 +16,6 @@ support-files =
|
|||
browser_star_hsts.sjs
|
||||
browser_tab_dragdrop2_frame1.xul
|
||||
browser_web_channel.html
|
||||
bug564387.html
|
||||
bug564387_video1.ogv
|
||||
bug564387_video1.ogv^headers^
|
||||
bug592338.html
|
||||
bug792517-2.html
|
||||
bug792517.html
|
||||
|
@ -82,6 +79,9 @@ support-files =
|
|||
test_wyciwyg_copying.html
|
||||
title_test.svg
|
||||
video.ogg
|
||||
web_video.html
|
||||
web_video1.ogv
|
||||
web_video1.ogv^headers^
|
||||
zoom_test.html
|
||||
test_no_mcb_on_http_site_img.html
|
||||
test_no_mcb_on_http_site_img.css
|
||||
|
@ -411,6 +411,7 @@ skip-if = buildapp == 'mulet' || e10s # Bug ?????? - test directly manipulates c
|
|||
skip-if = buildapp == 'mulet' || e10s # e10s: Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly
|
||||
[browser_save_video.js]
|
||||
skip-if = buildapp == 'mulet' || e10s # Bug ?????? - test directly manipulates content (event.target)
|
||||
[browser_save_video_frame.js]
|
||||
[browser_scope.js]
|
||||
[browser_searchSuggestionUI.js]
|
||||
skip-if = e10s
|
||||
|
|
|
@ -24,6 +24,7 @@ add_task(function*() {
|
|||
let actionURL = makeActionURI("switchtab", {url: "about:about"}).spec;
|
||||
yield check_a11y_label("% about", "about:about " + actionURL + " Tab");
|
||||
|
||||
gURLBar.popup.hidePopup();
|
||||
yield promisePopupHidden(gURLBar.popup);
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ function test() {
|
|||
waitForExplicitFinish();
|
||||
var fileName;
|
||||
|
||||
gBrowser.loadURI("http://mochi.test:8888/browser/browser/base/content/test/general/bug564387.html");
|
||||
gBrowser.loadURI("http://mochi.test:8888/browser/browser/base/content/test/general/web_video.html");
|
||||
|
||||
gBrowser.addEventListener("pageshow", function pageShown(event) {
|
||||
if (event.target.location == "about:blank")
|
||||
|
@ -66,7 +66,7 @@ function test() {
|
|||
function onTransferComplete(downloadSuccess) {
|
||||
ok(downloadSuccess, "Video file should have been downloaded successfully");
|
||||
|
||||
is(fileName, "Bug564387-expectedName.ogv",
|
||||
is(fileName, "web-video1-expectedName.ogv",
|
||||
"Video file name is correctly retrieved from Content-Disposition http header");
|
||||
|
||||
finish();
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const VIDEO_URL = "http://mochi.test:8888/browser/browser/base/content/test/general/web_video.html";
|
||||
|
||||
/**
|
||||
* mockTransfer.js provides a utility that lets us mock out
|
||||
* the "Save File" dialog.
|
||||
*/
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
|
||||
this);
|
||||
|
||||
/**
|
||||
* Creates and returns an nsIFile for a new temporary save
|
||||
* directory.
|
||||
*
|
||||
* @return nsIFile
|
||||
*/
|
||||
function createTemporarySaveDirectory() {
|
||||
let 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, 0755);
|
||||
return saveDir;
|
||||
}
|
||||
/**
|
||||
* MockTransfer exposes a "mockTransferCallback" global which
|
||||
* allows us to define a callback to be called once the mock file
|
||||
* selector has selected where to save the file.
|
||||
*/
|
||||
function waitForTransferComplete() {
|
||||
return new Promise((resolve) => {
|
||||
mockTransferCallback = () => {
|
||||
ok(true, "Transfer completed");
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given some browser, loads a framescript that right-clicks
|
||||
* on the video1 element to spawn a contextmenu.
|
||||
*/
|
||||
function rightClickVideo(browser) {
|
||||
let frame_script = () => {
|
||||
const Ci = Components.interfaces;
|
||||
let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
|
||||
let document = content.document;
|
||||
let video = document.getElementById("video1");
|
||||
let rect = video.getBoundingClientRect();
|
||||
|
||||
/* Synthesize a click in the center of the video. */
|
||||
let left = rect.left + (rect.width / 2);
|
||||
let top = rect.top + (rect.height / 2);
|
||||
|
||||
utils.sendMouseEvent("contextmenu", left, top,
|
||||
2, /* aButton */
|
||||
1, /* aClickCount */
|
||||
0 /* aModifiers */);
|
||||
};
|
||||
let mm = browser.messageManager;
|
||||
mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a page with a <video> element, right-clicks it and chooses
|
||||
* to save a frame screenshot to the disk. Completes once we've
|
||||
* verified that the frame has been saved to disk.
|
||||
*/
|
||||
add_task(function*() {
|
||||
let MockFilePicker = SpecialPowers.MockFilePicker;
|
||||
MockFilePicker.init(window);
|
||||
|
||||
// Create the folder the video will be saved into.
|
||||
let destDir = createTemporarySaveDirectory();
|
||||
let destFile = destDir.clone();
|
||||
|
||||
MockFilePicker.displayDirectory = destDir;
|
||||
MockFilePicker.showCallback = function(fp) {
|
||||
destFile.append(fp.defaultString);
|
||||
MockFilePicker.returnFiles = [destFile];
|
||||
MockFilePicker.filterIndex = 1; // kSaveAsType_URL
|
||||
};
|
||||
|
||||
mockTransferRegisterer.register();
|
||||
|
||||
// Make sure that we clean these things up when we're done.
|
||||
registerCleanupFunction(function () {
|
||||
mockTransferRegisterer.unregister();
|
||||
MockFilePicker.cleanup();
|
||||
destDir.remove(true);
|
||||
});
|
||||
|
||||
let tab = gBrowser.addTab();
|
||||
gBrowser.selectedTab = tab;
|
||||
let browser = tab.linkedBrowser;
|
||||
info("Loading video tab");
|
||||
yield promiseTabLoadEvent(tab, VIDEO_URL);
|
||||
info("Video tab loaded.");
|
||||
|
||||
let video = browser.contentDocument.getElementById("video1");
|
||||
let context = document.getElementById("contentAreaContextMenu");
|
||||
let popupPromise = promisePopupShown(context);
|
||||
|
||||
info("Synthesizing right-click on video element");
|
||||
rightClickVideo(browser);
|
||||
info("Waiting for popup to fire popupshown.");
|
||||
yield popupPromise;
|
||||
info("Popup fired popupshown");
|
||||
|
||||
let saveSnapshotCommand = document.getElementById("context-video-saveimage");
|
||||
let promiseTransfer = waitForTransferComplete()
|
||||
info("Firing save snapshot command");
|
||||
saveSnapshotCommand.doCommand();
|
||||
context.hidePopup();
|
||||
info("Waiting for transfer completion");
|
||||
yield promiseTransfer;
|
||||
info("Transfer complete");
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
<html>
|
||||
<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=564387 -->
|
||||
<head>
|
||||
<title> Bug 564387 test</title>
|
||||
</head>
|
||||
<body>
|
||||
Testing for Mozilla Bug: 564387
|
||||
<br>
|
||||
<video src="bug564387_video1.ogv" id="video1"> </video>
|
||||
</body>
|
||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||
Content-Disposition: filename="Bug564387-expectedName.ogv"
|
||||
Content-Type: video/ogg
|
||||
|
|
@ -744,7 +744,7 @@ function is_element_hidden(element, msg) {
|
|||
function promisePopupEvent(popup, eventSuffix) {
|
||||
let endState = {shown: "open", hidden: "closed"}[eventSuffix];
|
||||
|
||||
if (popup.state = endState)
|
||||
if (popup.state == endState)
|
||||
return Promise.resolve();
|
||||
|
||||
let eventType = "popup" + eventSuffix;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Document with Web Video</title>
|
||||
</head>
|
||||
<body>
|
||||
This document has some web video in it.
|
||||
<br>
|
||||
<video src="web_video1.ogv" id="video1"> </video>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||
Content-Disposition: filename="web-video1-expectedName.ogv"
|
||||
Content-Type: video/ogg
|
||||
|
|
@ -126,9 +126,9 @@ let gSyncPane = {
|
|||
});
|
||||
setEventListener("syncViewQuota", "command", gSyncPane.openQuotaDialog);
|
||||
setEventListener("syncChangePassword", "command",
|
||||
gSyncUtils.changePassword);
|
||||
() => gSyncUtils.changePassword());
|
||||
setEventListener("syncResetPassphrase", "command",
|
||||
gSyncUtils.resetPassphrase);
|
||||
() => gSyncUtils.resetPassphrase());
|
||||
setEventListener("syncReset", "command", gSyncPane.resetSync);
|
||||
setEventListener("syncAddDeviceLabel", "click", function () {
|
||||
gSyncPane.openAddDevice();
|
||||
|
|
|
@ -65,6 +65,8 @@ support-files =
|
|||
[browser_broadcast.js]
|
||||
[browser_capabilities.js]
|
||||
[browser_cleaner.js]
|
||||
[browser_crashedTabs.js]
|
||||
skip-if = !e10s || os == "linux" # Waiting on OMTC enabled by default on Linux (Bug 994541)
|
||||
[browser_dying_cache.js]
|
||||
[browser_dynamic_frames.js]
|
||||
[browser_form_restore_events.js]
|
||||
|
|
|
@ -0,0 +1,328 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const PAGE_1 = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
|
||||
const PAGE_2 = "data:text/html,<html><body>Another%20regular,%20everyday,%20normal%20page.";
|
||||
|
||||
/**
|
||||
* Returns a Promise that resolves once a remote <xul:browser> has experienced
|
||||
* a crash. Also does the job of cleaning up the minidump of the crash.
|
||||
*
|
||||
* @param browser
|
||||
* The <xul:browser> that will crash
|
||||
* @return Promise
|
||||
*/
|
||||
function crashBrowser(browser) {
|
||||
// This frame script is injected into the remote browser, and used to
|
||||
// intentionally crash the tab. We crash by using js-ctypes and dereferencing
|
||||
// a bad pointer. The crash should happen immediately upon loading this
|
||||
// frame script.
|
||||
let frame_script = () => {
|
||||
const Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/ctypes.jsm");
|
||||
|
||||
let dies = function() {
|
||||
let zero = new ctypes.intptr_t(8);
|
||||
let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
|
||||
badptr.contents
|
||||
};
|
||||
|
||||
dump("Et tu, Brute?");
|
||||
dies();
|
||||
}
|
||||
|
||||
let crashCleanupPromise = new Promise((resolve, reject) => {
|
||||
let observer = (subject, topic, data) => {
|
||||
is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
|
||||
ok(subject instanceof Ci.nsIPropertyBag2,
|
||||
'Subject implements nsIPropertyBag2.');
|
||||
// we might see this called as the process terminates due to previous tests.
|
||||
// We are only looking for "abnormal" exits...
|
||||
if (!subject.hasKey("abnormal")) {
|
||||
info("This is a normal termination and isn't the one we are looking for...");
|
||||
return;
|
||||
}
|
||||
|
||||
let dumpID;
|
||||
if ('nsICrashReporter' in Ci) {
|
||||
dumpID = subject.getPropertyAsAString('dumpID');
|
||||
ok(dumpID, "dumpID is present and not an empty string");
|
||||
}
|
||||
|
||||
if (dumpID) {
|
||||
let minidumpDirectory = getMinidumpDirectory();
|
||||
removeFile(minidumpDirectory, dumpID + '.dmp');
|
||||
removeFile(minidumpDirectory, dumpID + '.extra');
|
||||
}
|
||||
|
||||
Services.obs.removeObserver(observer, 'ipc:content-shutdown');
|
||||
resolve();
|
||||
};
|
||||
|
||||
Services.obs.addObserver(observer, 'ipc:content-shutdown');
|
||||
});
|
||||
|
||||
let aboutTabCrashedLoadPromise = new Promise((resolve, reject) => {
|
||||
browser.addEventListener("AboutTabCrashedLoad", function onCrash() {
|
||||
browser.removeEventListener("AboutTabCrashedLoad", onCrash, false);
|
||||
resolve();
|
||||
}, false, true);
|
||||
});
|
||||
|
||||
// This frame script will crash the remote browser as soon as it is
|
||||
// evaluated.
|
||||
let mm = browser.messageManager;
|
||||
mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false);
|
||||
return Promise.all([crashCleanupPromise, aboutTabCrashedLoadPromise]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where crash dumps are stored.
|
||||
*
|
||||
* @return nsIFile
|
||||
*/
|
||||
function getMinidumpDirectory() {
|
||||
let dir = Services.dirsvc.get('ProfD', Ci.nsIFile);
|
||||
dir.append("minidumps");
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a file from a directory. This is a no-op if the file does not
|
||||
* exist.
|
||||
*
|
||||
* @param directory
|
||||
* The nsIFile representing the directory to remove from.
|
||||
* @param filename
|
||||
* A string for the file to remove from the directory.
|
||||
*/
|
||||
function removeFile(directory, filename) {
|
||||
let file = directory.clone();
|
||||
file.append(filename);
|
||||
if (file.exists()) {
|
||||
file.remove(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the documentURI of the root document of a remote browser
|
||||
* to see if it equals URI. Returns a Promise that resolves if
|
||||
* there is a match, and rejects with an error message if they
|
||||
* do not match.
|
||||
*
|
||||
* @param browser
|
||||
* The remote <xul:browser> to check the root document URI in.
|
||||
* @param URI
|
||||
* A string to match the root document URI against.
|
||||
* @return Promise
|
||||
*/
|
||||
function promiseContentDocumentURIEquals(browser, URI) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let frame_script = () => {
|
||||
sendAsyncMessage("test:documenturi", {
|
||||
uri: content.document.documentURI,
|
||||
});
|
||||
};
|
||||
|
||||
let mm = browser.messageManager;
|
||||
mm.addMessageListener("test:documenturi", function onMessage(message) {
|
||||
mm.removeMessageListener("test:documenturi", onMessage);
|
||||
let contentURI = message.data.uri;
|
||||
if (contentURI == URI) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(`Content has URI ${contentURI} which does not match ${URI}`);
|
||||
}
|
||||
});
|
||||
|
||||
mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the window.history.length of the root window of a remote
|
||||
* browser to see if it equals length. Returns a Promise that resolves
|
||||
* if there is a match, and rejects with an error message if they
|
||||
* do not match.
|
||||
*
|
||||
* @param browser
|
||||
* The remote <xul:browser> to check the root window.history.length
|
||||
* @param length
|
||||
* The expected history length
|
||||
* @return Promise
|
||||
*/
|
||||
function promiseHistoryLength(browser, length) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let frame_script = () => {
|
||||
sendAsyncMessage("test:historylength", {
|
||||
length: content.history.length,
|
||||
});
|
||||
};
|
||||
|
||||
let mm = browser.messageManager;
|
||||
mm.addMessageListener("test:historylength", function onMessage(message) {
|
||||
mm.removeMessageListener("test:historylength", onMessage);
|
||||
let contentLength = message.data.length;
|
||||
if (contentLength == length) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(`Content has window.history.length ${contentLength} which does ` +
|
||||
`not equal expected ${length}`);
|
||||
}
|
||||
});
|
||||
|
||||
mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that if a tab crashes, that information about the tab crashed
|
||||
* page does not get added to the tab history.
|
||||
*/
|
||||
add_task(function test_crash_page_not_in_history() {
|
||||
let newTab = gBrowser.addTab();
|
||||
gBrowser.selectedTab = newTab;
|
||||
let browser = newTab.linkedBrowser;
|
||||
ok(browser.isRemoteBrowser, "Should be a remote browser");
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
browser.loadURI(PAGE_1);
|
||||
yield promiseBrowserLoaded(browser);
|
||||
TabState.flush(browser);
|
||||
|
||||
// Crash the tab
|
||||
yield crashBrowser(browser);
|
||||
// Flush out any notifications from the crashed browser.
|
||||
TabState.flush(browser);
|
||||
|
||||
// Check the tab state and make sure the tab crashed page isn't
|
||||
// mentioned.
|
||||
let {entries} = JSON.parse(ss.getTabState(newTab));
|
||||
is(entries.length, 1, "Should have a single history entry");
|
||||
is(entries[0].url, PAGE_1,
|
||||
"Single entry should be the page we visited before crashing");
|
||||
|
||||
gBrowser.removeTab(newTab);
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that if a tab crashes, that when we browse away from that page
|
||||
* to a non-blacklisted site (so the browser becomes remote again), that
|
||||
* we record history for that new visit.
|
||||
*/
|
||||
add_task(function test_revived_history_from_remote() {
|
||||
let newTab = gBrowser.addTab();
|
||||
gBrowser.selectedTab = newTab;
|
||||
let browser = newTab.linkedBrowser;
|
||||
ok(browser.isRemoteBrowser, "Should be a remote browser");
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
browser.loadURI(PAGE_1);
|
||||
yield promiseBrowserLoaded(browser);
|
||||
TabState.flush(browser);
|
||||
|
||||
// Crash the tab
|
||||
yield crashBrowser(browser);
|
||||
// Flush out any notifications from the crashed browser.
|
||||
TabState.flush(browser);
|
||||
|
||||
// Browse to a new site that will cause the browser to
|
||||
// become remote again.
|
||||
browser.loadURI(PAGE_2);
|
||||
yield promiseBrowserLoaded(browser);
|
||||
ok(browser.isRemoteBrowser, "Should be a remote browser");
|
||||
TabState.flush(browser);
|
||||
|
||||
// Check the tab state and make sure the tab crashed page isn't
|
||||
// mentioned.
|
||||
let {entries} = JSON.parse(ss.getTabState(newTab));
|
||||
is(entries.length, 2, "Should have two history entries");
|
||||
is(entries[0].url, PAGE_1,
|
||||
"First entry should be the page we visited before crashing");
|
||||
is(entries[1].url, PAGE_2,
|
||||
"Second entry should be the page we visited after crashing");
|
||||
|
||||
gBrowser.removeTab(newTab);
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that if a tab crashes, that when we browse away from that page
|
||||
* to a blacklisted site (so the browser stays non-remote), that
|
||||
* we record history for that new visit.
|
||||
*/
|
||||
add_task(function test_revived_history_from_non_remote() {
|
||||
let newTab = gBrowser.addTab();
|
||||
gBrowser.selectedTab = newTab;
|
||||
let browser = newTab.linkedBrowser;
|
||||
ok(browser.isRemoteBrowser, "Should be a remote browser");
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
browser.loadURI(PAGE_1);
|
||||
yield promiseBrowserLoaded(browser);
|
||||
TabState.flush(browser);
|
||||
|
||||
// Crash the tab
|
||||
yield crashBrowser(browser);
|
||||
// Flush out any notifications from the crashed browser.
|
||||
TabState.flush(browser);
|
||||
|
||||
// Browse to a new site that will not cause the browser to
|
||||
// become remote again.
|
||||
browser.loadURI("about:mozilla");
|
||||
yield promiseBrowserLoaded(browser);
|
||||
ok(!browser.isRemoteBrowser, "Should not be a remote browser");
|
||||
TabState.flush(browser);
|
||||
|
||||
// Check the tab state and make sure the tab crashed page isn't
|
||||
// mentioned.
|
||||
let {entries} = JSON.parse(ss.getTabState(newTab));
|
||||
is(entries.length, 2, "Should have two history entries");
|
||||
is(entries[0].url, PAGE_1,
|
||||
"First entry should be the page we visited before crashing");
|
||||
is(entries[1].url, "about:mozilla",
|
||||
"Second entry should be the page we visited after crashing");
|
||||
|
||||
gBrowser.removeTab(newTab);
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that we can revive a crashed tab back to the page that
|
||||
* it was on when it crashed.
|
||||
*/
|
||||
add_task(function test_revive_tab_from_session_store() {
|
||||
let newTab = gBrowser.addTab();
|
||||
gBrowser.selectedTab = newTab;
|
||||
let browser = newTab.linkedBrowser;
|
||||
ok(browser.isRemoteBrowser, "Should be a remote browser");
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
browser.loadURI(PAGE_1);
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
browser.loadURI(PAGE_2);
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
TabState.flush(browser);
|
||||
|
||||
// Crash the tab
|
||||
yield crashBrowser(browser);
|
||||
// Flush out any notifications from the crashed browser.
|
||||
TabState.flush(browser);
|
||||
|
||||
// Use SessionStore to revive the tab
|
||||
SessionStore.reviveCrashedTab(newTab);
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
// We can't just check browser.currentURI.spec, because from
|
||||
// the outside, a crashed tab has the same URI as the page
|
||||
// it crashed on (much like an about:neterror page). Instead,
|
||||
// we have to use the documentURI on the content.
|
||||
yield promiseContentDocumentURIEquals(browser, PAGE_2);
|
||||
|
||||
// We should also have two entries in the browser history.
|
||||
yield promiseHistoryLength(browser, 2);
|
||||
|
||||
gBrowser.removeTab(newTab);
|
||||
});
|
|
@ -320,6 +320,29 @@ function whenBrowserUnloaded(aBrowser, aContainer, aCallback = next) {
|
|||
executeSoon(aCallback);
|
||||
}, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a page in a browser, and returns a Promise that
|
||||
* resolves once a "load" event has been fired within that
|
||||
* browser.
|
||||
*
|
||||
* @param browser
|
||||
* The browser to load the page in.
|
||||
* @param uri
|
||||
* The URI to load.
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
function loadPage(browser, uri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
browser.addEventListener("load", function onLoad(event) {
|
||||
browser.removeEventListener("load", onLoad, true);
|
||||
resolve();
|
||||
}, true);
|
||||
browser.loadURI(uri);
|
||||
});
|
||||
}
|
||||
|
||||
function promiseBrowserUnloaded(aBrowser, aContainer) {
|
||||
return new Promise(resolve => {
|
||||
whenBrowserUnloaded(aBrowser, aContainer, resolve);
|
||||
|
|
|
@ -8925,6 +8925,9 @@ AC_SUBST(MOZ_FOLD_LIBS)
|
|||
AC_SUBST(MOZ_ENABLE_SZIP)
|
||||
AC_SUBST(MOZ_SZIP_FLAGS)
|
||||
|
||||
dnl Host JavaScript runtime, if any, to use during cross compiles.
|
||||
AC_SUBST(JS_BINARY)
|
||||
|
||||
if test "$MOZ_DEBUG"; then
|
||||
MOZ_EM_DEBUG=1
|
||||
fi
|
||||
|
|
|
@ -16,6 +16,7 @@ import android.text.Editable;
|
|||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -24,6 +25,7 @@ import android.widget.LinearLayout;
|
|||
import android.widget.TextView;
|
||||
|
||||
public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnClickListener, GeckoEventListener {
|
||||
private static final String LOGTAG = "FindInPageBar";
|
||||
private static final String REQUEST_ID = "FindInPageBar";
|
||||
|
||||
private final Context mContext;
|
||||
|
@ -181,13 +183,23 @@ public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnC
|
|||
/**
|
||||
* Request find operation, and update matchCount results (current count and total).
|
||||
*/
|
||||
private void sendRequestToFinderHelper(String request, String searchString) {
|
||||
private void sendRequestToFinderHelper(final String request, final String searchString) {
|
||||
GeckoAppShell.sendRequestToGecko(new GeckoRequest(request, searchString) {
|
||||
@Override
|
||||
public void onResponse(NativeJSObject nativeJSObject) {
|
||||
final int total = nativeJSObject.optInt("total", 0);
|
||||
final int current = nativeJSObject.optInt("current", 0);
|
||||
updateResult(total, current);
|
||||
}
|
||||
|
||||
public void onError() {
|
||||
// Gecko didn't respond due to state change, javascript error, etc.
|
||||
updateResult(0, 0);
|
||||
Log.d(LOGTAG, "No response from Gecko on request to match string: [" +
|
||||
searchString + "]");
|
||||
}
|
||||
|
||||
private void updateResult(int total, int current) {
|
||||
final Boolean statusVisibility = (total > 0);
|
||||
final String statusText = current + "/" + total;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.mozilla.gecko.fxa.activities;
|
|||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.fxa.FirefoxAccounts;
|
||||
import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
|
||||
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
|
||||
import org.mozilla.gecko.fxa.login.Engaged;
|
||||
import org.mozilla.gecko.fxa.login.State;
|
||||
|
@ -18,6 +19,7 @@ import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
|
|||
import android.accounts.Account;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
|
@ -33,6 +35,7 @@ public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity i
|
|||
// Set in onCreate.
|
||||
protected TextView verificationLinkTextView;
|
||||
protected View resendLink;
|
||||
protected View changeEmail;
|
||||
|
||||
// Set in onResume.
|
||||
protected AndroidFxAccount fxAccount;
|
||||
|
@ -56,6 +59,8 @@ public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity i
|
|||
verificationLinkTextView = (TextView) ensureFindViewById(null, R.id.verification_link_text, "verification link text");
|
||||
resendLink = ensureFindViewById(null, R.id.resend_confirmation_email_link, "resend confirmation email link");
|
||||
resendLink.setOnClickListener(this);
|
||||
changeEmail = ensureFindViewById(null, R.id.change_confirmation_email_link, "change confirmation email address");
|
||||
changeEmail.setOnClickListener(this);
|
||||
|
||||
View backToBrowsingButton = ensureFindViewById(null, R.id.button, "back to browsing button");
|
||||
backToBrowsingButton.setOnClickListener(new OnClickListener() {
|
||||
|
@ -159,6 +164,12 @@ public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity i
|
|||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
FxAccountCodeResender.resendCode(this, fxAccount);
|
||||
if (v.equals(resendLink)) {
|
||||
FxAccountCodeResender.resendCode(this, fxAccount);
|
||||
} else if (v.equals(changeEmail)) {
|
||||
final Account account = fxAccount.getAndroidAccount();
|
||||
Intent intent = new Intent(this, FxAccountGetStartedActivity.class);
|
||||
FxAccountStatusActivity.maybeDeleteAndroidAccount(this, account, intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.mozilla.gecko.background.common.log.Logger;
|
|||
import org.mozilla.gecko.fxa.FirefoxAccounts;
|
||||
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
import org.mozilla.gecko.LocaleAware.LocaleAwareActivity;
|
||||
import org.mozilla.gecko.LocaleAware.LocaleAwareFragmentActivity;
|
||||
|
||||
import android.accounts.Account;
|
||||
|
@ -113,7 +114,7 @@ public class FxAccountStatusActivity extends LocaleAwareFragmentActivity {
|
|||
* Helper function to maybe remove the given Android account.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
public void maybeDeleteAndroidAccount(final Account account) {
|
||||
public static void maybeDeleteAndroidAccount(final Activity activity, final Account account, final Intent intent) {
|
||||
if (account == null) {
|
||||
Logger.warn(LOG_TAG, "Trying to delete null account; ignoring request.");
|
||||
return;
|
||||
|
@ -123,11 +124,13 @@ public class FxAccountStatusActivity extends LocaleAwareFragmentActivity {
|
|||
@Override
|
||||
public void run(AccountManagerFuture<Boolean> future) {
|
||||
Logger.info(LOG_TAG, "Account " + Utils.obfuscateEmail(account.name) + " removed.");
|
||||
final Activity activity = FxAccountStatusActivity.this;
|
||||
final String text = activity.getResources().getString(R.string.fxaccount_remove_account_toast, account.name);
|
||||
Toast.makeText(activity, text, Toast.LENGTH_LONG).show();
|
||||
|
||||
finish();
|
||||
if (intent != null) {
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
activity.finish();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -138,20 +141,20 @@ public class FxAccountStatusActivity extends LocaleAwareFragmentActivity {
|
|||
final int icon;
|
||||
if (AppConstants.Versions.feature11Plus) {
|
||||
final TypedValue typedValue = new TypedValue();
|
||||
getTheme().resolveAttribute(android.R.attr.alertDialogIcon, typedValue, true);
|
||||
activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, typedValue, true);
|
||||
icon = typedValue.resourceId;
|
||||
} else {
|
||||
icon = android.R.drawable.ic_dialog_alert;
|
||||
}
|
||||
|
||||
final AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
final AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.fxaccount_remove_account_dialog_title)
|
||||
.setIcon(icon)
|
||||
.setMessage(R.string.fxaccount_remove_account_dialog_message)
|
||||
.setPositiveButton(android.R.string.ok, new Dialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
AccountManager.get(FxAccountStatusActivity.this).removeAccount(account, callback, null);
|
||||
AccountManager.get(activity).removeAccount(account, callback, null);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() {
|
||||
|
@ -174,7 +177,7 @@ public class FxAccountStatusActivity extends LocaleAwareFragmentActivity {
|
|||
}
|
||||
|
||||
if (itemId == R.id.remove_account) {
|
||||
maybeDeleteAndroidAccount(FirefoxAccounts.getFirefoxAccount(this));
|
||||
maybeDeleteAndroidAccount(this, FirefoxAccounts.getFirefoxAccount(this), null);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -229,7 +229,6 @@ public class HistoryPanel extends HomeFragment {
|
|||
|
||||
final SpannableStringBuilder hintBuilder = formatHintText(hintText);
|
||||
if (hintBuilder != null) {
|
||||
emptyHint.setText(hintBuilder);
|
||||
emptyHint.setText(hintBuilder);
|
||||
emptyHint.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
emptyHint.setVisibility(View.VISIBLE);
|
||||
|
|
|
@ -400,9 +400,9 @@ size. -->
|
|||
<!ENTITY home_last_tabs_empty "Your recent tabs show up here.">
|
||||
<!ENTITY home_open_all "Open all">
|
||||
<!ENTITY home_most_recent_empty "Websites you visited most recently show up here.">
|
||||
<!-- Localization note (home_most_recent_emptyhint): "Psst" is a sound that might be used to attract someone's attention unobtrusively, and intended to hint at "Private browsing" to the user.
|
||||
<!-- Localization note (home_most_recent_emptyhint2): "Psst" is a sound that might be used to attract someone's attention unobtrusively, and intended to hint at Private Browsing to the user.
|
||||
The placeholders &formatS1; and &formatS2; are used to mark the location of text underlining. -->
|
||||
<!ENTITY home_most_recent_emptyhint "Psst: &formatS1;Private browsing&formatS2; mode won\'t save your history.">
|
||||
<!ENTITY home_most_recent_emptyhint2 "Psst: using a &formatS1;New Private Tab&formatS2; won\'t save your history.">
|
||||
<!ENTITY home_reading_list_empty "Articles you save for later show up here.">
|
||||
<!-- Localization note (home_reading_list_hint): The "TIP" string is synonymous to "hint", "clue", etc. This string is displayed
|
||||
as an advisory message on how to add content to the reading list when the reading list empty.
|
||||
|
|
|
@ -167,6 +167,7 @@
|
|||
<!-- Localization note: &formatS; is the Firefox Account's email address. -->
|
||||
<!ENTITY fxaccount_confirm_account_verification_link 'A verification link has been sent to &formatS;'>
|
||||
<!ENTITY fxaccount_confirm_account_resend_email 'Resend email'>
|
||||
<!ENTITY fxaccount_confirm_account_change_email 'Forget this email address?'>
|
||||
<!ENTITY fxaccount_confirm_account_verification_link_sent2 'Verification email sent'>
|
||||
<!ENTITY fxaccount_confirm_account_verification_link_not_sent2 'Couldn\'t send verification email'>
|
||||
|
||||
|
|
|
@ -45,6 +45,11 @@
|
|||
style="@style/FxAccountLinkItem"
|
||||
android:text="@string/fxaccount_confirm_account_resend_email" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_confirmation_email_link"
|
||||
style="@style/FxAccountLinkItem"
|
||||
android:text="@string/fxaccount_confirm_account_change_email" />
|
||||
|
||||
<LinearLayout style="@style/FxAccountSpacer" />
|
||||
|
||||
<ImageView
|
||||
|
|
|
@ -355,7 +355,7 @@
|
|||
<string name="home_last_tabs_empty">&home_last_tabs_empty;</string>
|
||||
<string name="home_open_all">&home_open_all;</string>
|
||||
<string name="home_most_recent_empty">&home_most_recent_empty;</string>
|
||||
<string name="home_most_recent_emptyhint">&home_most_recent_emptyhint;</string>
|
||||
<string name="home_most_recent_emptyhint">&home_most_recent_emptyhint2;</string>
|
||||
<string name="home_reading_list_empty">&home_reading_list_empty;</string>
|
||||
<string name="home_reading_list_hint">&home_reading_list_hint2;</string>
|
||||
<string name="home_reading_list_hint_accessible">&home_reading_list_hint_accessible;</string>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
#! /bin/sh
|
||||
# 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/.
|
||||
|
||||
# Wrapper for running SpiderMonkey js shell in automation with correct
|
||||
# LD_LIBRARY_PATH.
|
||||
|
||||
# We don't have a reference to topsrcdir at this point, but we are invoked as
|
||||
# "$topsrcdir/mobile/android/config/js_wrapper.sh" so we can extract topsrcdir
|
||||
# from $0.
|
||||
topsrcdir=`cd \`dirname $0\`/../../..; pwd`
|
||||
|
||||
JS_BINARY="$topsrcdir/jsshell/js"
|
||||
|
||||
LD_LIBRARY_PATH="$topsrcdir/jsshell${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}"
|
||||
export LD_LIBRARY_PATH
|
||||
|
||||
# Pass through all arguments and exit with status from js shell.
|
||||
exec "$JS_BINARY" "$@"
|
|
@ -53,3 +53,5 @@ ac_add_options --enable-stdcxx-compat
|
|||
mk_add_options "export ANT_HOME=$topsrcdir/apache-ant"
|
||||
|
||||
mk_add_options "export PATH=$topsrcdir/apache-ant/bin:$PATH"
|
||||
|
||||
JS_BINARY="$topsrcdir/mobile/android/config/js_wrapper.sh"
|
||||
|
|
|
@ -28,5 +28,12 @@
|
|||
"digest": "e28b7a12fbbef02ad742958df8dd356ea2adb8ef79e95cd8eb8dbc953eb4cc11888969dac7d636187fd3ace9c63d9a6bc3d7795021c1d811a843e413fe5e52c9",
|
||||
"algorithm": "sha512",
|
||||
"filename": "apache-ant-bin.tar.bz2"
|
||||
},
|
||||
{
|
||||
"size": 4906080,
|
||||
"digest": "d735544e039da89382c53b2302b7408d4610247b4f8b5cdc5a4d5a8ec5470947b19e8ea7f7a37e78222e661347e394e0030d81f41534138b527b14e9c4e55634",
|
||||
"algorithm": "sha512",
|
||||
"filename": "jsshell.tar.xz",
|
||||
"unpack": "True"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,5 +28,12 @@
|
|||
"digest": "e28b7a12fbbef02ad742958df8dd356ea2adb8ef79e95cd8eb8dbc953eb4cc11888969dac7d636187fd3ace9c63d9a6bc3d7795021c1d811a843e413fe5e52c9",
|
||||
"algorithm": "sha512",
|
||||
"filename": "apache-ant-bin.tar.bz2"
|
||||
},
|
||||
{
|
||||
"size": 4906080,
|
||||
"digest": "d735544e039da89382c53b2302b7408d4610247b4f8b5cdc5a4d5a8ec5470947b19e8ea7f7a37e78222e661347e394e0030d81f41534138b527b14e9c4e55634",
|
||||
"algorithm": "sha512",
|
||||
"filename": "jsshell.tar.xz",
|
||||
"unpack": "True"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,5 +28,12 @@
|
|||
"digest": "e28b7a12fbbef02ad742958df8dd356ea2adb8ef79e95cd8eb8dbc953eb4cc11888969dac7d636187fd3ace9c63d9a6bc3d7795021c1d811a843e413fe5e52c9",
|
||||
"algorithm": "sha512",
|
||||
"filename": "apache-ant-bin.tar.bz2"
|
||||
},
|
||||
{
|
||||
"size": 4906080,
|
||||
"digest": "d735544e039da89382c53b2302b7408d4610247b4f8b5cdc5a4d5a8ec5470947b19e8ea7f7a37e78222e661347e394e0030d81f41534138b527b14e9c4e55634",
|
||||
"algorithm": "sha512",
|
||||
"filename": "jsshell.tar.xz",
|
||||
"unpack": "True"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -35,6 +35,13 @@ MOZ_PACKAGER_MINIFY=1
|
|||
|
||||
include $(topsrcdir)/toolkit/mozapps/installer/packager.mk
|
||||
|
||||
# Note that JS_BINARY can be defined in packager.mk, so this test must come
|
||||
# after including that file. MOZ_PACKAGER_MINIFY_JS is used in packager.mk, but
|
||||
# since recipe evaluation is deferred, we can set it here after the inclusion.
|
||||
ifneq (,$(JS_BINARY))
|
||||
MOZ_PACKAGER_MINIFY_JS=1
|
||||
endif
|
||||
|
||||
ifeq (bundle, $(MOZ_FS_LAYOUT))
|
||||
BINPATH = $(_BINPATH)
|
||||
DEFINES += -DAPPNAME=$(_APPNAME)
|
||||
|
|
|
@ -154,6 +154,7 @@
|
|||
<string name="fxaccount_confirm_account_header">&fxaccount_confirm_account_header;</string>
|
||||
<string name="fxaccount_confirm_account_verification_link">&fxaccount_confirm_account_verification_link;</string>
|
||||
<string name="fxaccount_confirm_account_resend_email">&fxaccount_confirm_account_resend_email;</string>
|
||||
<string name="fxaccount_confirm_account_change_email">&fxaccount_confirm_account_change_email;</string>
|
||||
<string name="fxaccount_confirm_account_verification_link_sent">&fxaccount_confirm_account_verification_link_sent2;</string>
|
||||
<string name="fxaccount_confirm_account_verification_link_not_sent">&fxaccount_confirm_account_verification_link_not_sent2;</string>
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ else:
|
|||
|
||||
|
||||
__all__ = ['jsmin', 'JavascriptMinify']
|
||||
__version__ = '2.0.3'
|
||||
__version__ = '2.0.11'
|
||||
|
||||
|
||||
def jsmin(js):
|
||||
|
@ -72,7 +72,20 @@ class JavascriptMinify(object):
|
|||
def minify(self, instream=None, outstream=None):
|
||||
if instream and outstream:
|
||||
self.ins, self.outs = instream, outstream
|
||||
write = self.outs.write
|
||||
|
||||
self.is_return = False
|
||||
self.return_buf = ''
|
||||
|
||||
def write(char):
|
||||
# all of this is to support literal regular expressions.
|
||||
# sigh
|
||||
if char in 'return':
|
||||
self.return_buf += char
|
||||
self.is_return = self.return_buf == 'return'
|
||||
self.outs.write(char)
|
||||
if self.is_return:
|
||||
self.return_buf = ''
|
||||
|
||||
read = self.ins.read
|
||||
|
||||
space_strings = "abcdefghijklmnopqrstuvwxyz"\
|
||||
|
@ -82,6 +95,7 @@ class JavascriptMinify(object):
|
|||
newlineend_strings = enders + space_strings
|
||||
do_newline = False
|
||||
do_space = False
|
||||
escape_slash_count = 0
|
||||
doing_single_comment = False
|
||||
previous_before_comment = ''
|
||||
doing_multi_comment = False
|
||||
|
@ -90,13 +104,18 @@ class JavascriptMinify(object):
|
|||
quote_buf = []
|
||||
|
||||
previous = read(1)
|
||||
if previous == '\\':
|
||||
escape_slash_count += 1
|
||||
next1 = read(1)
|
||||
if previous == '/':
|
||||
if next1 == '/':
|
||||
doing_single_comment = True
|
||||
elif next1 == '*':
|
||||
doing_multi_comment = True
|
||||
previous = next1
|
||||
next1 = read(1)
|
||||
else:
|
||||
in_re = True # literal regex at start of script
|
||||
write(previous)
|
||||
elif not previous:
|
||||
return
|
||||
|
@ -111,16 +130,20 @@ class JavascriptMinify(object):
|
|||
return
|
||||
|
||||
while 1:
|
||||
next2 = read(1)
|
||||
next2 = read(1)
|
||||
if not next2:
|
||||
last = next1.strip()
|
||||
if not (doing_single_comment or doing_multi_comment)\
|
||||
and last not in ('', '/'):
|
||||
if in_quote:
|
||||
write(''.join(quote_buf))
|
||||
write(last)
|
||||
break
|
||||
if doing_multi_comment:
|
||||
if next1 == '*' and next2 == '/':
|
||||
doing_multi_comment = False
|
||||
if previous_before_comment and previous_before_comment in space_strings:
|
||||
do_space = True
|
||||
next2 = read(1)
|
||||
elif doing_single_comment:
|
||||
if next1 in '\r\n':
|
||||
|
@ -164,9 +187,17 @@ class JavascriptMinify(object):
|
|||
or previous_non_space > '~') \
|
||||
and (next2 in space_strings or next2 > '~'):
|
||||
do_space = True
|
||||
elif previous_non_space in '-+' and next2 == previous_non_space:
|
||||
# protect against + ++ or - -- sequences
|
||||
do_space = True
|
||||
elif self.is_return and next2 == '/':
|
||||
# returning a regex...
|
||||
write(' ')
|
||||
elif next1 == '/':
|
||||
if do_space:
|
||||
write(' ')
|
||||
if in_re:
|
||||
if previous != '\\':
|
||||
if previous != '\\' or (not escape_slash_count % 2) or next2 in 'gimy':
|
||||
in_re = False
|
||||
write('/')
|
||||
elif next2 == '/':
|
||||
|
@ -174,8 +205,12 @@ class JavascriptMinify(object):
|
|||
previous_before_comment = previous_non_space
|
||||
elif next2 == '*':
|
||||
doing_multi_comment = True
|
||||
previous_before_comment = previous_non_space
|
||||
previous = next1
|
||||
next1 = next2
|
||||
next2 = read(1)
|
||||
else:
|
||||
in_re = previous_non_space in '(,=:[?!&|'
|
||||
in_re = previous_non_space in '(,=:[?!&|;' or self.is_return # literal regular expression
|
||||
write('/')
|
||||
else:
|
||||
if do_space:
|
||||
|
@ -184,12 +219,19 @@ class JavascriptMinify(object):
|
|||
if do_newline:
|
||||
write('\n')
|
||||
do_newline = False
|
||||
|
||||
write(next1)
|
||||
if not in_re and next1 in "'\"":
|
||||
in_quote = next1
|
||||
quote_buf = []
|
||||
|
||||
previous = next1
|
||||
next1 = next2
|
||||
|
||||
if previous >= '!':
|
||||
previous_non_space = previous
|
||||
|
||||
if previous == '\\':
|
||||
escape_slash_count += 1
|
||||
else:
|
||||
escape_slash_count = 0
|
||||
|
|
|
@ -94,10 +94,37 @@ another thing;"""
|
|||
"""
|
||||
expected = r"""function foo(){alert('crud');}"""
|
||||
self.assertMinified(js, expected)
|
||||
|
||||
|
||||
def testBlockCommentStartingWithSlash(self):
|
||||
self.assertMinified('A; /*/ comment */ B', 'A;B')
|
||||
|
||||
def testBlockCommentEndingWithSlash(self):
|
||||
self.assertMinified('A; /* comment /*/ B', 'A;B')
|
||||
|
||||
def testLeadingBlockCommentStartingWithSlash(self):
|
||||
self.assertMinified('/*/ comment */ A', 'A')
|
||||
|
||||
def testLeadingBlockCommentEndingWithSlash(self):
|
||||
self.assertMinified('/* comment /*/ A', 'A')
|
||||
|
||||
def testEmptyBlockComment(self):
|
||||
self.assertMinified('/**/ A', 'A')
|
||||
|
||||
def testBlockCommentMultipleOpen(self):
|
||||
self.assertMinified('/* A /* B */ C', 'C')
|
||||
|
||||
def testJustAComment(self):
|
||||
self.assertMinified(' // a comment', '')
|
||||
|
||||
|
||||
def test_issue_10(self):
|
||||
js = '''
|
||||
files = [{name: value.replace(/^.*\\\\/, '')}];
|
||||
// comment
|
||||
A
|
||||
'''
|
||||
expected = '''files=[{name:value.replace(/^.*\\\\/,'')}]; A'''
|
||||
self.assertMinified(js, expected)
|
||||
|
||||
def testRe(self):
|
||||
js = r'''
|
||||
var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
|
||||
|
@ -153,6 +180,20 @@ Element.cleanWhitespace(element);"""
|
|||
expected = r"""inspect:function(useDoubleQuotes){var escapedString=this.gsub(/[\x00-\x1f\\]/,function(match){var character=String.specialChar[match[0]];return character?character:'\\u00'+match[0].charCodeAt().toPaddedString(2,16);});if(useDoubleQuotes)return'"'+escapedString.replace(/"/g,'\\"')+'"';return"'"+escapedString.replace(/'/g,'\\\'')+"'";},toJSON:function(){return this.inspect(true);},unfilterJSON:function(filter){return this.sub(filter||Prototype.JSONFilter,'#{1}');},"""
|
||||
self.assertMinified(js, expected)
|
||||
|
||||
def testLiteralRe(self):
|
||||
js = r"""
|
||||
myString.replace(/\\/g, '/');
|
||||
console.log("hi");
|
||||
"""
|
||||
expected = r"""myString.replace(/\\/g,'/');console.log("hi");"""
|
||||
self.assertMinified(js, expected)
|
||||
|
||||
js = r''' return /^data:image\//i.test(url) ||
|
||||
/^(https?|ftp|file|about|chrome|resource):/.test(url);
|
||||
'''
|
||||
expected = r'''return /^data:image\//i.test(url)||/^(https?|ftp|file|about|chrome|resource):/.test(url);'''
|
||||
self.assertMinified(js, expected)
|
||||
|
||||
def testNoBracesWithComment(self):
|
||||
js = r"""
|
||||
onSuccess: function(transport) {
|
||||
|
@ -247,6 +288,68 @@ var foo = "hey";
|
|||
self.assertMinified('x.replace(/\//, "_")// slash to underscore',
|
||||
'x.replace(/\//,"_")')
|
||||
|
||||
def testSlashesNearComments(self):
|
||||
original = '''
|
||||
{ a: n / 2, }
|
||||
// comment
|
||||
'''
|
||||
expected = '''{a:n/2,}'''
|
||||
self.assertMinified(original, expected)
|
||||
|
||||
def testReturn(self):
|
||||
original = '''
|
||||
return foo;//comment
|
||||
return bar;'''
|
||||
expected = 'return foo; return bar;'
|
||||
self.assertMinified(original, expected)
|
||||
|
||||
def test_space_plus(self):
|
||||
original = '"s" + ++e + "s"'
|
||||
expected = '"s"+ ++e+"s"'
|
||||
self.assertMinified(original, expected)
|
||||
|
||||
def test_no_final_newline(self):
|
||||
original = '"s"'
|
||||
expected = '"s"'
|
||||
self.assertMinified(original, expected)
|
||||
|
||||
def test_space_with_regex_repeats(self):
|
||||
original = '/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");'
|
||||
self.assertMinified(original, original) # there should be nothing jsmin can do here
|
||||
|
||||
def test_space_with_regex_repeats_not_at_start(self):
|
||||
original = 'aaa;/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");'
|
||||
self.assertMinified(original, original) # there should be nothing jsmin can do here
|
||||
|
||||
def test_space_in_regex(self):
|
||||
original = '/a (a)/.test("a")'
|
||||
self.assertMinified(original, original)
|
||||
|
||||
def test_angular_1(self):
|
||||
original = '''var /** holds major version number for IE or NaN for real browsers */
|
||||
msie,
|
||||
jqLite, // delay binding since jQuery could be loaded after us.'''
|
||||
minified = jsmin.jsmin(original)
|
||||
self.assertTrue('var msie' in minified)
|
||||
|
||||
def test_angular_2(self):
|
||||
original = 'var/* comment */msie;'
|
||||
expected = 'var msie;'
|
||||
self.assertMinified(original, expected)
|
||||
|
||||
def test_angular_3(self):
|
||||
original = 'var /* comment */msie;'
|
||||
expected = 'var msie;'
|
||||
self.assertMinified(original, expected)
|
||||
|
||||
def test_angular_4(self):
|
||||
original = 'var /* comment */ msie;'
|
||||
expected = 'var msie;'
|
||||
self.assertMinified(original, expected)
|
||||
|
||||
def test_angular_4(self):
|
||||
original = 'a/b'
|
||||
self.assertMinified(original, original)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
Загрузка…
Ссылка в новой задаче