зеркало из https://github.com/mozilla/gecko-dev.git
Merge m-c to b2ginbound a=merge
This commit is contained in:
Коммит
1974bec813
|
@ -17,8 +17,6 @@ class AccessibleWrap;
|
|||
} // namespace a11y
|
||||
} // namespace mozilla
|
||||
|
||||
struct MaiUtilClass;
|
||||
|
||||
extern "C" {
|
||||
void actionInterfaceInitCB(AtkActionIface* aIface);
|
||||
void componentInterfaceInitCB(AtkComponentIface* aIface);
|
||||
|
|
|
@ -23,7 +23,6 @@ struct nsRect;
|
|||
class nsIFrame;
|
||||
class nsIAtom;
|
||||
class nsIPersistentProperties;
|
||||
class nsView;
|
||||
|
||||
namespace mozilla {
|
||||
namespace a11y {
|
||||
|
@ -39,7 +38,6 @@ class HTMLLIAccessible;
|
|||
class HyperTextAccessible;
|
||||
class ImageAccessible;
|
||||
class KeyBinding;
|
||||
class MathMLAccessible;
|
||||
class ProxyAccessible;
|
||||
class Relation;
|
||||
class RootAccessible;
|
||||
|
|
|
@ -23,8 +23,6 @@
|
|||
|
||||
class nsAccessiblePivot;
|
||||
|
||||
class nsIScrollableView;
|
||||
|
||||
const uint32_t kDefaultCacheLength = 128;
|
||||
|
||||
namespace mozilla {
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
#include "BaseAccessibles.h"
|
||||
|
||||
class nsGenericHTMLElement;
|
||||
|
||||
namespace mozilla {
|
||||
namespace a11y {
|
||||
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
#include "HTMLFormControlAccessible.h"
|
||||
|
||||
class nsIMutableArray;
|
||||
|
||||
namespace mozilla {
|
||||
namespace a11y {
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
#include "TableAccessible.h"
|
||||
#include "TableCellAccessible.h"
|
||||
|
||||
class nsITableLayout;
|
||||
class nsITableCellLayout;
|
||||
|
||||
namespace mozilla {
|
||||
|
|
|
@ -20,11 +20,7 @@ class Accessible;
|
|||
} // namespace a11y
|
||||
} // namespace mozilla
|
||||
|
||||
class nsINode;
|
||||
class nsIContent;
|
||||
class nsIFrame;
|
||||
class nsIPresShell;
|
||||
class nsPluginFrame;
|
||||
|
||||
// 0e7e6879-854b-4260-bc6e-525b5fb5cf34
|
||||
#define NS_IACCESSIBILITYSERVICE_IID \
|
||||
|
|
|
@ -191,6 +191,12 @@ this.EventManager.prototype = {
|
|||
let acc = aEvent.accessible;
|
||||
if (acc === this.contentControl.vc.position) {
|
||||
this.present(Presentation.nameChanged(acc));
|
||||
} else {
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
|
||||
['text', 'all']);
|
||||
if (liveRegion) {
|
||||
this.present(Presentation.nameChanged(acc, isPolite));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -293,6 +299,12 @@ this.EventManager.prototype = {
|
|||
if (position === target ||
|
||||
Utils.getEmbeddedControl(position) === target) {
|
||||
this.present(Presentation.valueChanged(target));
|
||||
} else {
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
|
||||
['text', 'all']);
|
||||
if (liveRegion) {
|
||||
this.present(Presentation.valueChanged(target, isPolite));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -523,18 +523,19 @@ B2GPresenter.prototype.pivotChanged =
|
|||
};
|
||||
|
||||
B2GPresenter.prototype.nameChanged =
|
||||
function B2GPresenter_nameChanged(aAccessible) {
|
||||
function B2GPresenter_nameChanged(aAccessible, aIsPolite = true) {
|
||||
return {
|
||||
type: this.type,
|
||||
details: {
|
||||
eventType: 'name-change',
|
||||
data: aAccessible.name
|
||||
data: aAccessible.name,
|
||||
options: {enqueue: aIsPolite}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
B2GPresenter.prototype.valueChanged =
|
||||
function B2GPresenter_valueChanged(aAccessible) {
|
||||
function B2GPresenter_valueChanged(aAccessible, aIsPolite = true) {
|
||||
|
||||
// the editable value changes are handled in the text changed presenter
|
||||
if (Utils.getState(aAccessible).contains(States.EDITABLE)) {
|
||||
|
@ -545,7 +546,8 @@ B2GPresenter.prototype.valueChanged =
|
|||
type: this.type,
|
||||
details: {
|
||||
eventType: 'value-change',
|
||||
data: aAccessible.value
|
||||
data: aAccessible.value,
|
||||
options: {enqueue: aIsPolite}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
namespace mozilla {
|
||||
namespace a11y {
|
||||
|
||||
struct objc_class;
|
||||
|
||||
class RootAccessibleWrap : public RootAccessible
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -44,6 +44,17 @@
|
|||
document.getElementById('fruit').setAttribute('aria-label', 'banana');
|
||||
}
|
||||
|
||||
function renameSlider() {
|
||||
document.getElementById('slider').setAttribute(
|
||||
'aria-label', 'mover');
|
||||
}
|
||||
|
||||
function changeSliderValue() {
|
||||
document.getElementById('slider').setAttribute('aria-valuenow', '5');
|
||||
document.getElementById('slider').setAttribute(
|
||||
'aria-valuetext', 'medium');
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
#windows {
|
||||
|
@ -89,5 +100,9 @@
|
|||
</div>
|
||||
<button id="home">Home</button>
|
||||
<button id="fruit" aria-label="apple"></button>
|
||||
<div id="live" aria-live="polite" aria-label="live">
|
||||
<div id="slider" role="slider" aria-label="slider" aria-valuemin="0"
|
||||
aria-valuemax="10" aria-valuenow="0"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -629,7 +629,7 @@ ExpectedNameChange.prototype = Object.create(ExpectedPresent.prototype);
|
|||
function ExpectedValueChange(aValue, aOptions) {
|
||||
ExpectedPresent.call(this, {
|
||||
eventType: 'value-change',
|
||||
data: [aValue]
|
||||
data: aValue
|
||||
}, null, aOptions);
|
||||
}
|
||||
|
||||
|
|
|
@ -58,8 +58,12 @@
|
|||
new ExpectedCursorChange(['Home', {'string': 'pushbutton'}])],
|
||||
[ContentMessages.simpleMoveNext,
|
||||
new ExpectedCursorChange(['apple', {'string': 'pushbutton'}])],
|
||||
[ContentMessages.simpleMoveNext,
|
||||
new ExpectedCursorChange(['slider', '0', {'string': 'slider'}])],
|
||||
|
||||
// Simple traversal backward
|
||||
[ContentMessages.simpleMovePrevious,
|
||||
new ExpectedCursorChange(['apple', {'string': 'pushbutton'}])],
|
||||
[ContentMessages.simpleMovePrevious,
|
||||
new ExpectedCursorChange(['Home', {'string': 'pushbutton'}])],
|
||||
[ContentMessages.simpleMovePrevious,
|
||||
|
@ -92,7 +96,7 @@
|
|||
// Move from an inner frame to the last element in the parent doc
|
||||
[ContentMessages.simpleMoveLast,
|
||||
new ExpectedCursorChange(
|
||||
['apple', {'string': 'pushbutton'}], { b2g_todo: true })],
|
||||
['slider', '0', {'string': 'slider'}], { b2g_todo: true })],
|
||||
|
||||
[ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
|
||||
|
||||
|
@ -147,6 +151,12 @@
|
|||
new ExpectedCursorChange(['apple', {'string': 'pushbutton'}])],
|
||||
[doc.defaultView.renameFruit, new ExpectedNameChange('banana')],
|
||||
|
||||
// Name and value changes inside a live-region (no cursor present)
|
||||
[doc.defaultView.renameSlider,
|
||||
new ExpectedNameChange('mover')],
|
||||
[doc.defaultView.changeSliderValue,
|
||||
new ExpectedValueChange('medium')],
|
||||
|
||||
// Blur button and reset cursor
|
||||
[ContentMessages.focusSelector('button#fruit', true), null],
|
||||
[ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
|
||||
|
@ -222,14 +232,14 @@
|
|||
|
||||
// Open dialog in outer doc, while cursor is also in outer doc
|
||||
[ContentMessages.simpleMoveLast,
|
||||
new ExpectedCursorChange(['banana', {'string': 'pushbutton'}])],
|
||||
new ExpectedCursorChange(['mover'])],
|
||||
[doc.defaultView.showAlert,
|
||||
new ExpectedCursorChange(['This is an alert!',
|
||||
{'string': 'headingLevel', 'args': [1]},
|
||||
{'string': 'dialog'}])],
|
||||
|
||||
[doc.defaultView.hideAlert,
|
||||
new ExpectedCursorChange(['banana', {'string': 'pushbutton'}])],
|
||||
new ExpectedCursorChange(['mover'])],
|
||||
|
||||
[ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
#include "nsCOMPtr.h"
|
||||
#include "nsRefPtrHashtable.h"
|
||||
|
||||
class nsIArray;
|
||||
class nsIContent;
|
||||
|
||||
namespace mozilla {
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
#include "XULMenuAccessible.h"
|
||||
#include "XULSelectControlAccessible.h"
|
||||
|
||||
class nsIWeakReference;
|
||||
|
||||
namespace mozilla {
|
||||
namespace a11y {
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
|
|||
'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
|
||||
'browser.safebrowsing.reportURL': 'http://localhost/safebrowsing-dummy/report',
|
||||
'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport',
|
||||
'browser.selfsupport.url': 'http://localhost/repair-dummy',
|
||||
'browser.selfsupport.url': 'https://localhost/selfsupport-dummy',
|
||||
'browser.trackingprotection.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
|
||||
'browser.trackingprotection.updateURL': 'http://localhost/safebrowsing-dummy/update',
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"browser.safebrowsing.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
|
||||
"browser.safebrowsing.reportURL": "http://localhost/safebrowsing-dummy/report",
|
||||
"browser.safebrowsing.malware.reportURL": "http://localhost/safebrowsing-dummy/malwarereport",
|
||||
"browser.selfsupport.url": "http://localhost/repair-dummy",
|
||||
"browser.selfsupport.url": "https://localhost/selfsupport-dummy",
|
||||
"browser.trackingprotection.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
|
||||
"browser.trackingprotection.updateURL": "http://localhost/safebrowsing-dummy/update",
|
||||
"browser.newtabpage.directory.source": "data:application/json,{'jetpack':1}",
|
||||
|
|
|
@ -323,7 +323,7 @@ pref("media.fragmented-mp4.gonk.enabled", true);
|
|||
pref("media.video-queue.default-size", 3);
|
||||
|
||||
// optimize images' memory usage
|
||||
pref("image.decode-only-on-draw.enabled", true);
|
||||
pref("image.decode-only-on-draw.enabled", false);
|
||||
pref("image.mem.allow_locking_in_content_processes", true);
|
||||
// Limit the surface cache to 1/8 of main memory or 128MB, whichever is smaller.
|
||||
// Almost everything that was factored into 'max_decoded_image_kb' is now stored
|
||||
|
@ -1124,8 +1124,5 @@ pref("dom.mozSettings.allowForceReadOnly", false);
|
|||
// RequestSync API is enabled by default on B2G.
|
||||
pref("dom.requestSync.enabled", true);
|
||||
|
||||
// Use vsync aligned rendering
|
||||
pref("gfx.vsync.hw-vsync.enabled", true);
|
||||
pref("gfx.vsync.compositor", true);
|
||||
// Resample touch events on b2g
|
||||
pref("gfx.touch.resample", true);
|
||||
pref("gfx.vsync.refreshdriver", true);
|
||||
|
|
|
@ -30,14 +30,19 @@ html xul|scrollbar {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Scrollbar code will reset the margin to the correct side depending on
|
||||
layout.scrollbar.side pref */
|
||||
xul|scrollbar[orient="vertical"] {
|
||||
margin-left: -8px;
|
||||
-moz-margin-start: -8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
}
|
||||
|
||||
/* workaround for bug 1119057: as -moz-margin-start may not work as expected,
|
||||
* force a right margin value in RTL mode. */
|
||||
[dir="rtl"] xul|scrollbar[root="true"][orient="vertical"] {
|
||||
-moz-margin-start: unset;
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
xul|scrollbar[orient="vertical"] xul|thumb {
|
||||
max-width: 6px !important;
|
||||
min-width: 6px !important;
|
||||
|
|
|
@ -209,6 +209,7 @@
|
|||
@RESPATH@/components/dom_json.xpt
|
||||
@RESPATH@/components/dom_messages.xpt
|
||||
@RESPATH@/components/dom_power.xpt
|
||||
@RESPATH@/components/dom_push.xpt
|
||||
@RESPATH@/components/dom_quota.xpt
|
||||
@RESPATH@/components/dom_range.xpt
|
||||
@RESPATH@/components/dom_security.xpt
|
||||
|
@ -631,7 +632,7 @@
|
|||
@RESPATH@/components/AppsService.manifest
|
||||
@RESPATH@/components/Push.js
|
||||
@RESPATH@/components/Push.manifest
|
||||
@RESPATH@/components/PushServiceLauncher.js
|
||||
@RESPATH@/components/PushNotificationService.js
|
||||
|
||||
@RESPATH@/components/InterAppComm.manifest
|
||||
@RESPATH@/components/InterAppCommService.js
|
||||
|
|
|
@ -30,6 +30,7 @@ externalProtocolUnknown=<Unknown>
|
|||
externalProtocolChkMsg=Remember my choice for all links of this type.
|
||||
externalProtocolLaunchBtn=Launch application
|
||||
malwareBlocked=The site at %S has been reported as an attack site and has been blocked based on your security preferences.
|
||||
unwantedBlocked=The site at %S has been reported as serving unwanted software and has been blocked based on your security preferences.
|
||||
phishingBlocked=The website at %S has been reported as a web forgery designed to trick users into sharing personal or financial information.
|
||||
cspBlocked=This page has a content security policy that prevents it from being loaded in this way.
|
||||
corruptedContentError=The page you are trying to view cannot be shown because an error in the data transmission was detected.
|
||||
|
|
|
@ -374,6 +374,7 @@
|
|||
<h1 id="et_nssFailure2">&nssFailure2.title;</h1>
|
||||
<h1 id="et_nssBadCert">&nssBadCert.title;</h1>
|
||||
<h1 id="et_malwareBlocked">&malwareBlocked.title;</h1>
|
||||
<h1 id="et_unwantedBlocked">&unwantedBlocked.title;</h1>
|
||||
<h1 id="et_cspBlocked">&cspBlocked.title;</h1>
|
||||
<h1 id="et_remoteXUL">&remoteXUL.title;</h1>
|
||||
<h1 id="et_corruptedContentError">&corruptedContentError.title;</h1>
|
||||
|
@ -401,6 +402,7 @@
|
|||
<div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
|
||||
<div id="ed_nssBadCert">&nssBadCert.longDesc2;</div>
|
||||
<div id="ed_malwareBlocked">&malwareBlocked.longDesc;</div>
|
||||
<div id="ed_unwantedBlocked">&unwantedBlocked.longDesc;</div>
|
||||
<div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
|
||||
<div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
|
||||
<div id="ed_corruptedContentError">&corruptedContentError.longDesc;</div>
|
||||
|
|
|
@ -79,6 +79,9 @@
|
|||
case "phishingBlocked" :
|
||||
initPage_phishing();
|
||||
break;
|
||||
case "unwantedBlocked" :
|
||||
initPage_unwanted();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +90,7 @@
|
|||
*/
|
||||
function initPage_malware()
|
||||
{
|
||||
// Remove phishing strings
|
||||
// Remove phishing and unwanted strings
|
||||
var el = document.getElementById("errorTitleText_phishing");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
|
@ -97,18 +100,57 @@
|
|||
el = document.getElementById("errorLongDescText_phishing");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorTitleText_unwanted");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorShortDescText_unwanted");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorLongDescText_unwanted");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
// Set sitename
|
||||
document.getElementById("malware_sitename").textContent = getHostString();
|
||||
document.title = document.getElementById("errorTitleText_malware")
|
||||
.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize custom strings and functionality for blocked malware case
|
||||
*/
|
||||
function initPage_unwanted()
|
||||
{
|
||||
// Remove phishing and malware strings
|
||||
var el = document.getElementById("errorTitleText_phishing");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorShortDescText_phishing");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorLongDescText_phishing");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorTitleText_malware");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorShortDescText_malware");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorLongDescText_malware");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
// Set sitename
|
||||
document.getElementById("unwanted_sitename").textContent = getHostString();
|
||||
document.title = document.getElementById("errorTitleText_unwanted")
|
||||
.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize custom strings and functionality for blocked phishing case
|
||||
*/
|
||||
function initPage_phishing()
|
||||
{
|
||||
// Remove malware strings
|
||||
// Remove malware and unwanted strings
|
||||
var el = document.getElementById("errorTitleText_malware");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
|
@ -118,6 +160,15 @@
|
|||
el = document.getElementById("errorLongDescText_malware");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorTitleText_unwanted");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorShortDescText_unwanted");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
el = document.getElementById("errorLongDescText_unwanted");
|
||||
el.parentNode.removeChild(el);
|
||||
|
||||
// Set sitename
|
||||
document.getElementById("phishing_sitename").textContent = getHostString();
|
||||
document.title = document.getElementById("errorTitleText_phishing")
|
||||
|
@ -161,6 +212,7 @@
|
|||
<div id="errorTitle">
|
||||
<h1 id="errorTitleText_phishing">&safeb.blocked.phishingPage.title;</h1>
|
||||
<h1 id="errorTitleText_malware">&safeb.blocked.malwarePage.title;</h1>
|
||||
<h1 id="errorTitleText_unwanted">&safeb.blocked.unwantedPage.title;</h1>
|
||||
</div>
|
||||
|
||||
<div id="errorLongContent">
|
||||
|
@ -169,12 +221,14 @@
|
|||
<div id="errorShortDesc">
|
||||
<p id="errorShortDescText_phishing">&safeb.blocked.phishingPage.shortDesc;</p>
|
||||
<p id="errorShortDescText_malware">&safeb.blocked.malwarePage.shortDesc;</p>
|
||||
<p id="errorShortDescText_unwanted">&safeb.blocked.unwantedPage.shortDesc;</p>
|
||||
</div>
|
||||
|
||||
<!-- Long Description -->
|
||||
<div id="errorLongDesc">
|
||||
<p id="errorLongDescText_phishing">&safeb.blocked.phishingPage.longDesc;</p>
|
||||
<p id="errorLongDescText_malware">&safeb.blocked.malwarePage.longDesc;</p>
|
||||
<p id="errorLongDescText_unwanted">&safeb.blocked.unwantedPage.longDesc;</p>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
|
|
|
@ -2659,7 +2659,7 @@ let BrowserOnClick = {
|
|||
msg.data.sslStatusAsString);
|
||||
break;
|
||||
case "Browser:SiteBlockedError":
|
||||
this.onAboutBlocked(msg.data.elementId, msg.data.isMalware,
|
||||
this.onAboutBlocked(msg.data.elementId, msg.data.reason,
|
||||
msg.data.isTopFrame, msg.data.location);
|
||||
break;
|
||||
case "Browser:EnableOnlineMode":
|
||||
|
@ -2843,10 +2843,15 @@ let BrowserOnClick = {
|
|||
}
|
||||
},
|
||||
|
||||
onAboutBlocked: function (elementId, isMalware, isTopFrame, location) {
|
||||
// Depending on what page we are displaying here (malware/phishing)
|
||||
onAboutBlocked: function (elementId, reason, isTopFrame, location) {
|
||||
// Depending on what page we are displaying here (malware/phishing/unwanted)
|
||||
// use the right strings and links for each.
|
||||
let bucketName = isMalware ? "WARNING_MALWARE_PAGE_":"WARNING_PHISHING_PAGE_";
|
||||
let bucketName = "WARNING_PHISHING_PAGE_";
|
||||
if (reason === 'malware') {
|
||||
bucketName = "WARNING_MALWARE_PAGE_";
|
||||
} else if (reason === 'unwanted') {
|
||||
bucketName = "WARNING_UNWANTED_PAGE_";
|
||||
}
|
||||
let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
|
||||
let nsISecTel = Ci.nsISecurityUITelemetry;
|
||||
bucketName += isTopFrame ? "TOP_" : "FRAME_";
|
||||
|
@ -2857,33 +2862,19 @@ let BrowserOnClick = {
|
|||
break;
|
||||
|
||||
case "reportButton":
|
||||
// This is the "Why is this site blocked" button. For malware,
|
||||
// we can fetch a site-specific report, for phishing, we redirect
|
||||
// to the generic page describing phishing protection.
|
||||
// This is the "Why is this site blocked" button. We redirect
|
||||
// to the generic page describing phishing/malware protection.
|
||||
|
||||
// We log even if malware/phishing info URL couldn't be found:
|
||||
// We log even if malware/phishing/unwanted info URL couldn't be found:
|
||||
// the measurement is for how many users clicked the WHY BLOCKED button
|
||||
secHistogram.add(nsISecTel[bucketName + "WHY_BLOCKED"]);
|
||||
|
||||
if (isMalware) {
|
||||
// Get the stop badware "why is this blocked" report url,
|
||||
// append the current url, and go there.
|
||||
try {
|
||||
let reportURL = formatURL("browser.safebrowsing.malware.reportURL", true);
|
||||
reportURL += location;
|
||||
gBrowser.loadURI(reportURL);
|
||||
} catch (e) {
|
||||
Components.utils.reportError("Couldn't get malware report URL: " + e);
|
||||
}
|
||||
}
|
||||
else { // It's a phishing site, not malware
|
||||
openHelpLink("phishing-malware", false, "current");
|
||||
}
|
||||
openHelpLink("phishing-malware", false, "current");
|
||||
break;
|
||||
|
||||
case "ignoreWarningButton":
|
||||
secHistogram.add(nsISecTel[bucketName + "IGNORE_WARNING"]);
|
||||
this.ignoreWarningButton(isMalware);
|
||||
this.ignoreWarningButton(reason);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
@ -2910,7 +2901,7 @@ let BrowserOnClick = {
|
|||
}
|
||||
},
|
||||
|
||||
ignoreWarningButton: function (isMalware) {
|
||||
ignoreWarningButton: function (reason) {
|
||||
// Allow users to override and continue through to the site,
|
||||
// but add a notify bar as a reminder, so that they don't lose
|
||||
// track after, e.g., tab switching.
|
||||
|
@ -2929,7 +2920,7 @@ let BrowserOnClick = {
|
|||
}];
|
||||
|
||||
let title;
|
||||
if (isMalware) {
|
||||
if (reason === 'malware') {
|
||||
title = gNavigatorBundle.getString("safebrowsing.reportedAttackSite");
|
||||
buttons[1] = {
|
||||
label: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.label"),
|
||||
|
@ -2938,7 +2929,7 @@ let BrowserOnClick = {
|
|||
openUILinkIn(gSafeBrowsing.getReportURL('MalwareError'), 'tab');
|
||||
}
|
||||
};
|
||||
} else {
|
||||
} else if (reason === 'phishing') {
|
||||
title = gNavigatorBundle.getString("safebrowsing.reportedWebForgery");
|
||||
buttons[1] = {
|
||||
label: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.label"),
|
||||
|
@ -2947,6 +2938,10 @@ let BrowserOnClick = {
|
|||
openUILinkIn(gSafeBrowsing.getReportURL('Error'), 'tab');
|
||||
}
|
||||
};
|
||||
} else if (reason === 'unwanted') {
|
||||
title = gNavigatorBundle.getString("safebrowsing.reportedUnwantedSite");
|
||||
// There is no button for reporting errors since Google doesn't currently
|
||||
// provide a URL endpoint for these reports.
|
||||
}
|
||||
|
||||
let notificationBox = gBrowser.getNotificationBox();
|
||||
|
@ -3994,6 +3989,11 @@ var XULBrowserWindow = {
|
|||
},
|
||||
|
||||
showTooltip: function (x, y, tooltip) {
|
||||
if (Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService).
|
||||
getCurrentSession()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The x,y coordinates are relative to the <browser> element using
|
||||
// the chrome zoom level.
|
||||
let elt = document.getElementById("remoteBrowserTooltip");
|
||||
|
|
|
@ -123,6 +123,13 @@ let handleContentContextMenu = function (event) {
|
|||
InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
|
||||
}
|
||||
|
||||
// Set the event target first as the copy image command needs it to
|
||||
// determine what was context-clicked on. Then, update the state of the
|
||||
// commands on the context menu.
|
||||
docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
|
||||
.setCommandNode(event.target);
|
||||
event.target.ownerDocument.defaultView.updateCommands("contentcontextmenu");
|
||||
|
||||
let customMenuItems = PageMenuChild.build(event.target);
|
||||
let principal = doc.nodePrincipal;
|
||||
sendSyncMessage("contextmenu",
|
||||
|
@ -377,9 +384,15 @@ let ClickEventHandler = {
|
|||
},
|
||||
|
||||
onAboutBlocked: function (targetElement, ownerDoc) {
|
||||
var reason = 'phishing';
|
||||
if (/e=malwareBlocked/.test(ownerDoc.documentURI)) {
|
||||
reason = 'malware';
|
||||
} else if (/e=unwantedBlocked/.test(ownerDoc.documentURI)) {
|
||||
reason = 'unwanted';
|
||||
}
|
||||
sendAsyncMessage("Browser:SiteBlockedError", {
|
||||
location: ownerDoc.location.href,
|
||||
isMalware: /e=malwareBlocked/.test(ownerDoc.documentURI),
|
||||
reason: reason,
|
||||
elementId: targetElement.getAttribute("id"),
|
||||
isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView)
|
||||
});
|
||||
|
|
|
@ -628,7 +628,9 @@ nsContextMenu.prototype = {
|
|||
this.inSyntheticDoc = ownerDoc.mozSyntheticDocument;
|
||||
// First, do checks for nodes that never have children.
|
||||
if (this.target.nodeType == Node.ELEMENT_NODE) {
|
||||
// See if the user clicked on an image.
|
||||
// See if the user clicked on an image. This check mirrors
|
||||
// nsDocumentViewer::GetInImage. Make sure to update both if this is
|
||||
// changed.
|
||||
if (this.target instanceof Ci.nsIImageLoadingContent &&
|
||||
this.target.currentURI) {
|
||||
this.onImage = true;
|
||||
|
|
|
@ -486,6 +486,11 @@ Sanitizer.prototype = {
|
|||
.getService(Ci.nsISiteSecurityService);
|
||||
sss.clearAll();
|
||||
|
||||
// Clear all push notification subscriptions
|
||||
var push = Cc["@mozilla.org/push/NotificationService;1"]
|
||||
.getService(Ci.nsIPushNotificationService);
|
||||
push.clearAll();
|
||||
|
||||
TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS");
|
||||
},
|
||||
|
||||
|
|
|
@ -144,7 +144,6 @@ skip-if = e10s # bug 967873 means permitUnload doesn't work in e10s mode
|
|||
[browser_bookmark_titles.js]
|
||||
skip-if = buildapp == 'mulet' || toolkit == "windows" # Disabled on Windows due to frequent failures (bugs 825739, 841341)
|
||||
[browser_bug304198.js]
|
||||
skip-if = e10s
|
||||
[browser_bug321000.js]
|
||||
skip-if = true # browser_bug321000.js is disabled because newline handling is shaky (bug 592528)
|
||||
[browser_bug329212.js]
|
||||
|
@ -215,7 +214,6 @@ skip-if = e10s # Bug 1093373 - relies on browser.sessionHistory
|
|||
[browser_bug562649.js]
|
||||
[browser_bug563588.js]
|
||||
[browser_bug565575.js]
|
||||
skip-if = e10s
|
||||
[browser_bug565667.js]
|
||||
skip-if = toolkit != "cocoa"
|
||||
[browser_bug567306.js]
|
||||
|
@ -250,7 +248,6 @@ skip-if = e10s && debug
|
|||
[browser_bug647886.js]
|
||||
skip-if = buildapp == 'mulet' || e10s # Bug 1093373 - Relies on browser.sessionHistory
|
||||
[browser_bug655584.js]
|
||||
skip-if = e10s
|
||||
[browser_bug664672.js]
|
||||
[browser_bug676619.js]
|
||||
skip-if = buildapp == 'mulet' || os == "mac" # mac: Intermittent failures, bug 925225
|
||||
|
@ -337,7 +334,6 @@ skip-if = os == "linux" # Linux: Intermittent failures, bug 917535
|
|||
[browser_menuButtonFitts.js]
|
||||
skip-if = os != "win" # The Fitts Law menu button is only supported on Windows (bug 969376)
|
||||
[browser_middleMouse_noJSPaste.js]
|
||||
skip-if = e10s # Bug 921952 - Content:Click event issues
|
||||
[browser_minimize.js]
|
||||
skip-if = e10s # Bug 1100664 - test directly access content docShells (TypeError: gBrowser.docShell is null)
|
||||
[browser_mixedcontent_securityflags.js]
|
||||
|
@ -388,7 +384,7 @@ skip-if = buildapp == 'mulet' || e10s # e10s: Bug 933103 - mochitest's EventUtil
|
|||
[browser_save_link_when_window_navigates.js]
|
||||
skip-if = buildapp == 'mulet' || e10s # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly
|
||||
[browser_save_video.js]
|
||||
skip-if = buildapp == 'mulet' || e10s # Bug 1100698 - test uses synthesizeMouse and then does a load of other stuff that breaks in e10s
|
||||
skip-if = buildapp == 'mulet'
|
||||
[browser_save_video_frame.js]
|
||||
[browser_scope.js]
|
||||
[browser_searchSuggestionUI.js]
|
||||
|
@ -465,7 +461,6 @@ skip-if = (os == "win" && !debug) || e10s # Bug 1007418
|
|||
[browser_windowopen_reflows.js]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[browser_wyciwyg_urlbarCopying.js]
|
||||
skip-if = e10s # Bug 1100703 - test directly manipulates content (content.document.getElementById)
|
||||
[browser_zbug569342.js]
|
||||
skip-if = e10s # Bug 1094240 - has findbar-related failures
|
||||
[browser_registerProtocolHandler_notification.js]
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
add_task(function* () {
|
||||
let charsToDelete, deletedURLTab, fullURLTab, partialURLTab, testPartialURL, testURL;
|
||||
|
||||
charsToDelete = 5;
|
||||
|
@ -13,112 +11,96 @@ function test() {
|
|||
partialURLTab = gBrowser.addTab();
|
||||
testURL = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
|
||||
|
||||
let loaded1 = BrowserTestUtils.browserLoaded(deletedURLTab.linkedBrowser, testURL);
|
||||
let loaded2 = BrowserTestUtils.browserLoaded(fullURLTab.linkedBrowser, testURL);
|
||||
let loaded3 = BrowserTestUtils.browserLoaded(partialURLTab.linkedBrowser, testURL);
|
||||
deletedURLTab.linkedBrowser.loadURI(testURL);
|
||||
fullURLTab.linkedBrowser.loadURI(testURL);
|
||||
partialURLTab.linkedBrowser.loadURI(testURL);
|
||||
yield Promise.all([loaded1, loaded2, loaded3]);
|
||||
|
||||
testURL = gURLBar.trimValue(testURL);
|
||||
testPartialURL = testURL.substr(0, (testURL.length - charsToDelete));
|
||||
|
||||
function cleanUp() {
|
||||
gBrowser.removeTab(fullURLTab);
|
||||
gBrowser.removeTab(partialURLTab);
|
||||
gBrowser.removeTab(deletedURLTab);
|
||||
}
|
||||
|
||||
function cycleTabs() {
|
||||
gBrowser.selectedTab = fullURLTab;
|
||||
function* cycleTabs() {
|
||||
yield BrowserTestUtils.switchTab(gBrowser, fullURLTab);
|
||||
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after switching back to fullURLTab');
|
||||
|
||||
gBrowser.selectedTab = partialURLTab;
|
||||
yield BrowserTestUtils.switchTab(gBrowser, partialURLTab);
|
||||
is(gURLBar.textValue, testPartialURL, 'gURLBar.textValue should be testPartialURL after switching back to partialURLTab');
|
||||
gBrowser.selectedTab = deletedURLTab;
|
||||
yield BrowserTestUtils.switchTab(gBrowser, deletedURLTab);
|
||||
is(gURLBar.textValue, '', 'gURLBar.textValue should be "" after switching back to deletedURLTab');
|
||||
|
||||
gBrowser.selectedTab = fullURLTab;
|
||||
yield BrowserTestUtils.switchTab(gBrowser, fullURLTab);
|
||||
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after switching back to fullURLTab');
|
||||
}
|
||||
|
||||
// function borrowed from browser_bug386835.js
|
||||
function load(tab, url, cb) {
|
||||
tab.linkedBrowser.addEventListener("load", function (event) {
|
||||
event.currentTarget.removeEventListener("load", arguments.callee, true);
|
||||
cb();
|
||||
}, true);
|
||||
tab.linkedBrowser.loadURI(url);
|
||||
}
|
||||
|
||||
function urlbarBackspace(cb) {
|
||||
gBrowser.selectedBrowser.focus();
|
||||
gURLBar.addEventListener("focus", function () {
|
||||
gURLBar.removeEventListener("focus", arguments.callee, false);
|
||||
function urlbarBackspace() {
|
||||
return new Promise((resolve, reject) => {
|
||||
gBrowser.selectedBrowser.focus();
|
||||
gURLBar.addEventListener("input", function () {
|
||||
gURLBar.removeEventListener("input", arguments.callee, false);
|
||||
cb();
|
||||
resolve();
|
||||
}, false);
|
||||
executeSoon(function () {
|
||||
EventUtils.synthesizeKey("VK_BACK_SPACE", {});
|
||||
});
|
||||
}, false);
|
||||
gURLBar.focus();
|
||||
gURLBar.focus();
|
||||
EventUtils.synthesizeKey("VK_BACK_SPACE", {});
|
||||
});
|
||||
}
|
||||
|
||||
function prepareDeletedURLTab(cb) {
|
||||
gBrowser.selectedTab = deletedURLTab;
|
||||
function* prepareDeletedURLTab() {
|
||||
yield BrowserTestUtils.switchTab(gBrowser, deletedURLTab);
|
||||
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to deletedURLTab');
|
||||
|
||||
// simulate the user removing the whole url from the location bar
|
||||
gPrefService.setBoolPref("browser.urlbar.clickSelectsAll", true);
|
||||
|
||||
urlbarBackspace(function () {
|
||||
is(gURLBar.textValue, "", 'gURLBar.textValue should be "" (just set)');
|
||||
if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll"))
|
||||
gPrefService.clearUserPref("browser.urlbar.clickSelectsAll");
|
||||
cb();
|
||||
});
|
||||
yield urlbarBackspace();
|
||||
is(gURLBar.textValue, "", 'gURLBar.textValue should be "" (just set)');
|
||||
if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll")) {
|
||||
gPrefService.clearUserPref("browser.urlbar.clickSelectsAll");
|
||||
}
|
||||
}
|
||||
|
||||
function prepareFullURLTab(cb) {
|
||||
gBrowser.selectedTab = fullURLTab;
|
||||
function* prepareFullURLTab() {
|
||||
yield BrowserTestUtils.switchTab(gBrowser, fullURLTab);
|
||||
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to fullURLTab');
|
||||
cb();
|
||||
}
|
||||
|
||||
function preparePartialURLTab(cb) {
|
||||
gBrowser.selectedTab = partialURLTab;
|
||||
function* preparePartialURLTab() {
|
||||
yield BrowserTestUtils.switchTab(gBrowser, partialURLTab);
|
||||
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to partialURLTab');
|
||||
|
||||
// simulate the user removing part of the url from the location bar
|
||||
gPrefService.setBoolPref("browser.urlbar.clickSelectsAll", false);
|
||||
|
||||
var deleted = 0;
|
||||
urlbarBackspace(function () {
|
||||
let deleted = 0;
|
||||
while (deleted < charsToDelete) {
|
||||
yield urlbarBackspace(arguments.callee);
|
||||
deleted++;
|
||||
if (deleted < charsToDelete) {
|
||||
urlbarBackspace(arguments.callee);
|
||||
} else {
|
||||
is(gURLBar.textValue, testPartialURL, "gURLBar.textValue should be testPartialURL (just set)");
|
||||
if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll"))
|
||||
gPrefService.clearUserPref("browser.urlbar.clickSelectsAll");
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
is(gURLBar.textValue, testPartialURL, "gURLBar.textValue should be testPartialURL (just set)");
|
||||
if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll")) {
|
||||
gPrefService.clearUserPref("browser.urlbar.clickSelectsAll");
|
||||
}
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
testURL = gURLBar.trimValue(testURL);
|
||||
testPartialURL = testURL.substr(0, (testURL.length - charsToDelete));
|
||||
// prepare the three tabs required by this test
|
||||
|
||||
// prepare the three tabs required by this test
|
||||
prepareFullURLTab(function () {
|
||||
preparePartialURLTab(function () {
|
||||
prepareDeletedURLTab(function () {
|
||||
// now cycle the tabs and make sure everything looks good
|
||||
cycleTabs();
|
||||
cleanUp();
|
||||
finish();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// First tab
|
||||
yield* prepareFullURLTab();
|
||||
yield* preparePartialURLTab();
|
||||
yield* prepareDeletedURLTab();
|
||||
|
||||
// now cycle the tabs and make sure everything looks good
|
||||
yield* cycleTabs();
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
load(deletedURLTab, testURL, function() {
|
||||
load(fullURLTab, testURL, function() {
|
||||
load(partialURLTab, testURL, runTests);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
function test() {
|
||||
add_task(function* () {
|
||||
gBrowser.selectedBrowser.focus();
|
||||
BrowserOpenTab();
|
||||
|
||||
yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => BrowserOpenTab(), false);
|
||||
ok(gURLBar.focused, "location bar is focused for a new tab");
|
||||
|
||||
gBrowser.selectedTab = gBrowser.tabs[0];
|
||||
yield BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
|
||||
ok(!gURLBar.focused, "location bar isn't focused for the previously selected tab");
|
||||
|
||||
gBrowser.selectedTab = gBrowser.tabs[1];
|
||||
yield BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[1]);
|
||||
ok(gURLBar.focused, "location bar is re-focused when selecting the new tab");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,20 +4,20 @@
|
|||
|
||||
// Bug 655584 - awesomebar suggestions don't update after tab is closed
|
||||
|
||||
function test() {
|
||||
add_task(function* () {
|
||||
var tab1 = gBrowser.addTab();
|
||||
var tab2 = gBrowser.addTab();
|
||||
|
||||
// When urlbar in a new tab is focused, and a tab switch occurs,
|
||||
// the urlbar popup should be closed
|
||||
gBrowser.selectedTab = tab2;
|
||||
yield BrowserTestUtils.switchTab(gBrowser, tab2);
|
||||
gURLBar.focus(); // focus the urlbar in the tab we will switch to
|
||||
gBrowser.selectedTab = tab1;
|
||||
yield BrowserTestUtils.switchTab(gBrowser, tab1);
|
||||
gURLBar.openPopup();
|
||||
gBrowser.selectedTab = tab2;
|
||||
yield BrowserTestUtils.switchTab(gBrowser, tab2);
|
||||
ok(!gURLBar.popupOpen, "urlbar focused in tab to switch to, close popup");
|
||||
|
||||
// cleanup
|
||||
gBrowser.removeCurrentTab();
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// This test is used to check copy and paste in editable areas to ensure that non-text
|
||||
// types (html and images) are copied to and pasted from the clipboard properly.
|
||||
|
||||
let testPage = "<div id='main' contenteditable='true'>Test <b>Bold</b> After Text</div>";
|
||||
let testPage = "<body style='margin: 0'><img id='img' tabindex='1' src='http://example.org/browser/browser/base/content/test/general/moz.png'>" +
|
||||
" <div id='main' contenteditable='true'>Test <b>Bold</b> After Text</div>" +
|
||||
"</body>";
|
||||
|
||||
add_task(function*() {
|
||||
let tab = gBrowser.addTab();
|
||||
|
@ -12,7 +14,12 @@ add_task(function*() {
|
|||
yield promiseTabLoadEvent(tab, "data:text/html," + escape(testPage));
|
||||
yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW);
|
||||
|
||||
let results = yield ContentTask.spawn(browser, {}, function* () {
|
||||
const modifier = (content.navigator.platform.indexOf("Mac") >= 0) ?
|
||||
Components.interfaces.nsIDOMWindowUtils.MODIFIER_META :
|
||||
Components.interfaces.nsIDOMWindowUtils.MODIFIER_CONTROL;
|
||||
|
||||
let results = yield ContentTask.spawn(browser, { modifier: modifier },
|
||||
function* (arg) {
|
||||
var doc = content.document;
|
||||
var main = doc.getElementById("main");
|
||||
main.focus();
|
||||
|
@ -20,10 +27,7 @@ add_task(function*() {
|
|||
const utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindowUtils);
|
||||
|
||||
const modifier = (content.navigator.platform.indexOf("Mac") >= 0) ?
|
||||
Components.interfaces.nsIDOMWindowUtils.MODIFIER_META :
|
||||
Components.interfaces.nsIDOMWindowUtils.MODIFIER_CONTROL;
|
||||
|
||||
const modifier = arg.modifier;
|
||||
function sendKey(key)
|
||||
{
|
||||
if (utils.sendKeyEvent("keydown", key, 0, modifier)) {
|
||||
|
@ -46,7 +50,7 @@ add_task(function*() {
|
|||
selection.modify("extend", "right", "word");
|
||||
selection.modify("extend", "right", "word");
|
||||
|
||||
yield new content.Promise((resolve, reject) => {
|
||||
yield new Promise((resolve, reject) => {
|
||||
addEventListener("copy", function copyEvent(event) {
|
||||
removeEventListener("copy", copyEvent, true);
|
||||
// The data is empty as the selection is copied during the event default phase.
|
||||
|
@ -59,7 +63,7 @@ add_task(function*() {
|
|||
|
||||
selection.modify("move", "right", "line");
|
||||
|
||||
yield new content.Promise((resolve, reject) => {
|
||||
yield new Promise((resolve, reject) => {
|
||||
addEventListener("paste", function copyEvent(event) {
|
||||
removeEventListener("paste", copyEvent, true);
|
||||
let clipboardData = event.clipboardData;
|
||||
|
@ -80,7 +84,7 @@ add_task(function*() {
|
|||
selection.modify("extend", "left", "word");
|
||||
selection.modify("extend", "left", "character");
|
||||
|
||||
yield new content.Promise((resolve, reject) => {
|
||||
yield new Promise((resolve, reject) => {
|
||||
addEventListener("cut", function copyEvent(event) {
|
||||
removeEventListener("cut", copyEvent, true);
|
||||
event.clipboardData.setData("text/plain", "Some text");
|
||||
|
@ -94,7 +98,7 @@ add_task(function*() {
|
|||
|
||||
selection.modify("move", "left", "line");
|
||||
|
||||
yield new content.Promise((resolve, reject) => {
|
||||
yield new Promise((resolve, reject) => {
|
||||
addEventListener("paste", function copyEvent(event) {
|
||||
removeEventListener("paste", copyEvent, true);
|
||||
let clipboardData = event.clipboardData;
|
||||
|
@ -110,6 +114,7 @@ add_task(function*() {
|
|||
});
|
||||
|
||||
is(main.innerHTML, "<i>Italic</i> Test <b>Bold</b> After<b></b>", "Copy and paste html 2");
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
|
@ -118,6 +123,61 @@ add_task(function*() {
|
|||
ok(results[t].startsWith("PASSED"), results[t]);
|
||||
}
|
||||
|
||||
// Next, check that the Copy Image command works.
|
||||
|
||||
// The context menu needs to be opened to properly initialize for the copy
|
||||
// image command to run.
|
||||
let contextMenu = document.getElementById("contentAreaContextMenu");
|
||||
let contextMenuShown = promisePopupShown(contextMenu);
|
||||
BrowserTestUtils.synthesizeMouseAtCenter("#img", { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
|
||||
yield contextMenuShown;
|
||||
|
||||
document.getElementById("context-copyimage-contents").doCommand();
|
||||
|
||||
contextMenu.hidePopup();
|
||||
yield promisePopupHidden(contextMenu);
|
||||
|
||||
// Focus the content again
|
||||
yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW);
|
||||
|
||||
let expectedContent = yield ContentTask.spawn(browser, { modifier: modifier },
|
||||
function* (arg) {
|
||||
var doc = content.document;
|
||||
var main = doc.getElementById("main");
|
||||
main.focus();
|
||||
|
||||
yield new Promise((resolve, reject) => {
|
||||
addEventListener("paste", function copyEvent(event) {
|
||||
removeEventListener("paste", copyEvent, true);
|
||||
let clipboardData = event.clipboardData;
|
||||
|
||||
// DataTransfer doesn't support the image types yet, so only text/html
|
||||
// will be present.
|
||||
if (clipboardData.getData("text/html") !=
|
||||
'<img id="img" tabindex="1" src="http://example.org/browser/browser/base/content/test/general/moz.png">') {
|
||||
reject();
|
||||
}
|
||||
resolve();
|
||||
}, true)
|
||||
|
||||
const utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindowUtils);
|
||||
|
||||
const modifier = arg.modifier;
|
||||
if (utils.sendKeyEvent("keydown", "v", 0, modifier)) {
|
||||
utils.sendKeyEvent("keypress", "v", "v".charCodeAt(0), modifier);
|
||||
}
|
||||
utils.sendKeyEvent("keyup", "v", 0, modifier);
|
||||
});
|
||||
|
||||
// Return the new content which should now include an image.
|
||||
return main.innerHTML;
|
||||
});
|
||||
|
||||
is(expectedContent, '<i>Italic</i> <img id="img" tabindex="1" ' +
|
||||
'src="http://example.org/browser/browser/base/content/test/general/moz.png">' +
|
||||
'Test <b>Bold</b> After<b></b>', "Paste after copy image");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
|
|
|
@ -3,51 +3,32 @@
|
|||
|
||||
const middleMousePastePref = "middlemouse.contentLoadURL";
|
||||
const autoScrollPref = "general.autoScroll";
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
Services.prefs.setBoolPref(middleMousePastePref, true);
|
||||
Services.prefs.setBoolPref(autoScrollPref, false);
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab();
|
||||
add_task(function* () {
|
||||
yield pushPrefs([middleMousePastePref, true], [autoScrollPref, false]);
|
||||
|
||||
registerCleanupFunction(function () {
|
||||
Services.prefs.clearUserPref(middleMousePastePref);
|
||||
Services.prefs.clearUserPref(autoScrollPref);
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
|
||||
|
||||
addPageShowListener(function () {
|
||||
let pagePrincipal = gBrowser.contentPrincipal;
|
||||
|
||||
// copy javascript URI to the clipboard
|
||||
let url = "javascript:http://www.example.com/";
|
||||
waitForClipboard(url,
|
||||
function() {
|
||||
Components.classes["@mozilla.org/widget/clipboardhelper;1"]
|
||||
.getService(Components.interfaces.nsIClipboardHelper)
|
||||
.copyString(url, document);
|
||||
},
|
||||
function () {
|
||||
// Middle click on the content area
|
||||
info("Middle clicking");
|
||||
EventUtils.sendMouseEvent({type: "click", button: 1}, gBrowser);
|
||||
},
|
||||
function() {
|
||||
ok(false, "Failed to copy URL to the clipboard");
|
||||
finish();
|
||||
}
|
||||
);
|
||||
|
||||
addPageShowListener(function () {
|
||||
is(gBrowser.currentURI.spec, url.replace(/^javascript:/, ""), "url loaded by middle click doesn't include JS");
|
||||
finish();
|
||||
let url = "javascript:http://www.example.com/";
|
||||
yield new Promise((resolve, reject) => {
|
||||
SimpleTest.waitForClipboard(url, () => {
|
||||
Components.classes["@mozilla.org/widget/clipboardhelper;1"]
|
||||
.getService(Components.interfaces.nsIClipboardHelper)
|
||||
.copyString(url, document);
|
||||
}, resolve, () => {
|
||||
ok(false, "Clipboard copy failed");
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addPageShowListener(func) {
|
||||
gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() {
|
||||
gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false);
|
||||
func();
|
||||
});
|
||||
}
|
||||
let middlePagePromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
|
||||
// Middle click on the content area
|
||||
info("Middle clicking");
|
||||
yield BrowserTestUtils.synthesizeMouse(null, 10, 10, {button: 1}, gBrowser.selectedBrowser);
|
||||
yield middlePagePromise;
|
||||
|
||||
is(gBrowser.currentURI.spec, url.replace(/^javascript:/, ""), "url loaded by middle click doesn't include JS");
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -8,70 +8,68 @@ MockFilePicker.init(window);
|
|||
* TestCase for bug 564387
|
||||
* <https://bugzilla.mozilla.org/show_bug.cgi?id=564387>
|
||||
*/
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
add_task(function* () {
|
||||
var fileName;
|
||||
|
||||
let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
gBrowser.loadURI("http://mochi.test:8888/browser/browser/base/content/test/general/web_video.html");
|
||||
yield loadPromise;
|
||||
|
||||
gBrowser.addEventListener("pageshow", function pageShown(event) {
|
||||
if (event.target.location == "about:blank")
|
||||
return;
|
||||
gBrowser.removeEventListener("pageshow", pageShown);
|
||||
let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
|
||||
|
||||
executeSoon(function () {
|
||||
document.addEventListener("popupshown", contextMenuOpened);
|
||||
yield BrowserTestUtils.synthesizeMouseAtCenter("#video1",
|
||||
{ type: "contextmenu", button: 2 },
|
||||
gBrowser.selectedBrowser);
|
||||
info("context menu click on video1");
|
||||
|
||||
var video1 = gBrowser.contentDocument.getElementById("video1");
|
||||
EventUtils.synthesizeMouseAtCenter(video1,
|
||||
{ type: "contextmenu", button: 2 },
|
||||
gBrowser.contentWindow);
|
||||
info("context menu click on video1");
|
||||
});
|
||||
});
|
||||
yield popupShownPromise;
|
||||
|
||||
function contextMenuOpened(event) {
|
||||
event.currentTarget.removeEventListener("popupshown", contextMenuOpened);
|
||||
info("context menu opened on video1");
|
||||
info("context menu opened on video1");
|
||||
|
||||
// Create the folder the video will be saved into.
|
||||
var destDir = createTemporarySaveDirectory();
|
||||
var destFile = destDir.clone();
|
||||
// Create the folder the video 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
|
||||
};
|
||||
MockFilePicker.displayDirectory = destDir;
|
||||
MockFilePicker.showCallback = function(fp) {
|
||||
fileName = fp.defaultString;
|
||||
destFile.append(fileName);
|
||||
MockFilePicker.returnFiles = [destFile];
|
||||
MockFilePicker.filterIndex = 1; // kSaveAsType_URL
|
||||
};
|
||||
|
||||
let transferCompletePromise = new Promise((resolve) => {
|
||||
function onTransferComplete(downloadSuccess) {
|
||||
ok(downloadSuccess, "Video file should have been downloaded successfully");
|
||||
|
||||
is(fileName, "web-video1-expectedName.ogv",
|
||||
"Video file name is correctly retrieved from Content-Disposition http header");
|
||||
resolve();
|
||||
}
|
||||
|
||||
mockTransferCallback = onTransferComplete;
|
||||
mockTransferRegisterer.register();
|
||||
});
|
||||
|
||||
registerCleanupFunction(function () {
|
||||
mockTransferRegisterer.unregister();
|
||||
MockFilePicker.cleanup();
|
||||
destDir.remove(true);
|
||||
});
|
||||
registerCleanupFunction(function () {
|
||||
mockTransferRegisterer.unregister();
|
||||
MockFilePicker.cleanup();
|
||||
destDir.remove(true);
|
||||
});
|
||||
|
||||
// Select "Save Video As" option from context menu
|
||||
var saveVideoCommand = document.getElementById("context-savevideo");
|
||||
saveVideoCommand.doCommand();
|
||||
info("context-savevideo command executed");
|
||||
// Select "Save Video As" option from context menu
|
||||
var saveVideoCommand = document.getElementById("context-savevideo");
|
||||
saveVideoCommand.doCommand();
|
||||
info("context-savevideo command executed");
|
||||
|
||||
event.target.hidePopup();
|
||||
}
|
||||
let contextMenu = document.getElementById("contentAreaContextMenu");
|
||||
let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
|
||||
contextMenu.hidePopup();
|
||||
yield popupHiddenPromise;
|
||||
|
||||
function onTransferComplete(downloadSuccess) {
|
||||
ok(downloadSuccess, "Video file should have been downloaded successfully");
|
||||
yield transferCompletePromise;
|
||||
});
|
||||
|
||||
is(fileName, "web-video1-expectedName.ogv",
|
||||
"Video file name is correctly retrieved from Content-Disposition http header");
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
|
|
|
@ -1,39 +1,31 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
let url = "http://mochi.test:8888/browser/browser/base/content/test/general/test_wyciwyg_copying.html";
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab(url);
|
||||
tab.linkedBrowser.addEventListener("pageshow", function () {
|
||||
let btn = content.document.getElementById("btn");
|
||||
executeSoon(function () {
|
||||
EventUtils.synthesizeMouseAtCenter(btn, {}, content);
|
||||
let currentURL = gBrowser.currentURI.spec;
|
||||
ok(/^wyciwyg:\/\//i.test(currentURL), currentURL + " is a wyciwyg URI");
|
||||
|
||||
executeSoon(function () {
|
||||
testURLBarCopy(url, endTest);
|
||||
});
|
||||
});
|
||||
}, false);
|
||||
|
||||
function endTest() {
|
||||
while (gBrowser.tabs.length > 1)
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
}
|
||||
|
||||
function testURLBarCopy(targetValue, cb) {
|
||||
function testURLBarCopy(targetValue) {
|
||||
return new Promise((resolve, reject) => {
|
||||
info("Expecting copy of: " + targetValue);
|
||||
waitForClipboard(targetValue, function () {
|
||||
gURLBar.focus();
|
||||
gURLBar.select();
|
||||
|
||||
goDoCommand("cmd_copy");
|
||||
}, cb, cb);
|
||||
}
|
||||
}, resolve, () => {
|
||||
ok(false, "Clipboard copy failed");
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const url = "http://mochi.test:8888/browser/browser/base/content/test/general/test_wyciwyg_copying.html";
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
yield BrowserTestUtils.synthesizeMouseAtCenter("#btn", {}, tab.linkedBrowser);
|
||||
let currentURL = gBrowser.currentURI.spec;
|
||||
ok(/^wyciwyg:\/\//i.test(currentURL), currentURL + " is a wyciwyg URI");
|
||||
|
||||
yield testURLBarCopy(url);
|
||||
|
||||
while (gBrowser.tabs.length > 1)
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
|
|
@ -468,6 +468,17 @@ loop.shared.actions = (function() {
|
|||
LeaveRoom: Action.define("leaveRoom", {
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to record a link click for metrics purposes.
|
||||
*/
|
||||
RecordClick: Action.define("recordClick", {
|
||||
// Note: for ToS and Privacy links, this should be the link, for
|
||||
// other links this should be a generic description so that we don't
|
||||
// record what users are clicking, just the information about the fact
|
||||
// they clicked the link in that spot (e.g. "Shared URL").
|
||||
linkInfo: String
|
||||
}),
|
||||
|
||||
/**
|
||||
* Requires detailed information on sad feedback.
|
||||
*/
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-36116321-15', 'auto');
|
||||
ga('set', 'anonymizeIp', true);
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
|
@ -129,6 +130,7 @@
|
|||
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
|
||||
<script type="text/javascript" src="js/fxOSMarketplace.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneMetricsStore.js"></script>
|
||||
<script type="text/javascript" src="js/webapp.js"></script>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
/* 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/. */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.store = loop.store || {};
|
||||
|
||||
/**
|
||||
* The standalone metrics store is used to log activities to
|
||||
* analytics.
|
||||
*
|
||||
* Where possible we log events via receiving actions. However, some
|
||||
* combinations of actions and events require us to listen directly to
|
||||
* changes in the activeRoomStore so that we gain the benefit of the logic
|
||||
* in that store.
|
||||
*/
|
||||
loop.store.StandaloneMetricsStore = (function() {
|
||||
"use strict";
|
||||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
|
||||
loop.store.metrics = loop.store.metrics || {};
|
||||
|
||||
var METRICS_GA_CATEGORY = loop.store.METRICS_GA_CATEGORY = {
|
||||
general: "/conversation/ interactions",
|
||||
download: "Firefox Downloads"
|
||||
};
|
||||
|
||||
var METRICS_GA_ACTIONS = loop.store.METRICS_GA_ACTIONS = {
|
||||
audioMute: "audio mute",
|
||||
button: "button click",
|
||||
download: "download button click",
|
||||
faceMute: "face mute",
|
||||
link: "link click",
|
||||
pageLoad: "page load messages",
|
||||
success: "success",
|
||||
support: "support link click"
|
||||
}
|
||||
|
||||
var StandaloneMetricsStore = loop.store.createStore({
|
||||
actions: [
|
||||
"gotMediaPermission",
|
||||
"joinRoom",
|
||||
"leaveRoom",
|
||||
"mediaConnected",
|
||||
"recordClick"
|
||||
],
|
||||
|
||||
/**
|
||||
* Initializes the store and starts listening to the activeRoomStore.
|
||||
*
|
||||
* @param {Object} options Options for the store, should include a
|
||||
* reference to the activeRoomStore.
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
|
||||
if (!options.activeRoomStore) {
|
||||
throw new Error("Missing option activeRoomStore");
|
||||
}
|
||||
|
||||
// Don't bother listening if we're not storing metrics.
|
||||
// I'd love for ga to be an option, but that messes up the function
|
||||
// working properly.
|
||||
if (window.ga) {
|
||||
this.activeRoomStore = options.activeRoomStore;
|
||||
|
||||
this.listenTo(options.activeRoomStore, "change",
|
||||
this._onActiveRoomStoreChange.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns initial state data for this store. These are mainly reflections
|
||||
* of activeRoomStore so we can match initial states for change tracking
|
||||
* purposes.
|
||||
*
|
||||
* @return {Object} The initial store state.
|
||||
*/
|
||||
getInitialStoreState: function() {
|
||||
return {
|
||||
audioMuted: false,
|
||||
roomState: ROOM_STATES.INIT,
|
||||
videoMuted: false
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves an event to ga.
|
||||
*
|
||||
* @param {String} category The category for the event.
|
||||
* @param {String} action The type of action.
|
||||
* @param {String} label The label detailing the action.
|
||||
*/
|
||||
_storeEvent: function(category, action, label) {
|
||||
// ga might not be defined if donottrack is enabled, see index.html.
|
||||
if (!window.ga) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now all we need to do is forward onto ga.
|
||||
window.ga("send", "event", category, action, label);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles media permssion being obtained.
|
||||
*/
|
||||
gotMediaPermission: function() {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
|
||||
"Media granted");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the user clicking the join room button.
|
||||
*/
|
||||
joinRoom: function() {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
|
||||
"Join the conversation");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the user clicking the leave room button.
|
||||
*/
|
||||
leaveRoom: function() {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
|
||||
"Leave conversation");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles notification that two-way media has been achieved.
|
||||
*/
|
||||
mediaConnected: function() {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
|
||||
"Media connected")
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles recording link clicks.
|
||||
*
|
||||
* @param {sharedActions.RecordClick} actionData The data associated with
|
||||
* the link.
|
||||
*/
|
||||
recordClick: function(actionData) {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.linkClick,
|
||||
actionData.linkInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles notifications that the activeRoomStore has changed, updating
|
||||
* the metrics for room state and mute state as necessary.
|
||||
*/
|
||||
_onActiveRoomStoreChange: function() {
|
||||
var roomStore = this.activeRoomStore.getStoreState();
|
||||
|
||||
this._checkRoomState(roomStore.roomState, roomStore.failureReason);
|
||||
|
||||
this._checkMuteState("audio", roomStore.audioMuted);
|
||||
this._checkMuteState("video", roomStore.videoMuted);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles checking of the room state to look for events we need to send.
|
||||
*
|
||||
* @param {String} roomState The new room state.
|
||||
* @param {String} failureReason Optional, if the room is in the failure
|
||||
* state, this should contain the reason for
|
||||
* the failure.
|
||||
*/
|
||||
_checkRoomState: function(roomState, failureReason) {
|
||||
if (this._storeState.roomState === roomState) {
|
||||
return;
|
||||
}
|
||||
this._storeState.roomState = roomState;
|
||||
|
||||
if (roomState === ROOM_STATES.FAILED &&
|
||||
failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID) {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
|
||||
"Link expired or invalid");
|
||||
}
|
||||
|
||||
if (roomState === ROOM_STATES.FULL) {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
|
||||
"Room full");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles check of the mute state to look for events we need to send.
|
||||
*
|
||||
* @param {String} type The type of mute being adjusted, i.e. "audio" or
|
||||
* "video".
|
||||
* @param {Boolean} muted The new state of mute
|
||||
*/
|
||||
_checkMuteState: function(type, muted) {
|
||||
var muteItem = type + "Muted";
|
||||
if (this._storeState[muteItem] === muted) {
|
||||
return;
|
||||
}
|
||||
this._storeState[muteItem] = muted;
|
||||
|
||||
var muteType = type === "audio" ? METRICS_GA_ACTIONS.audioMute : METRICS_GA_ACTIONS.faceMute;
|
||||
var muteState = muted ? "mute" : "unmute";
|
||||
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, muteType, muteState);
|
||||
}
|
||||
});
|
||||
|
||||
return StandaloneMetricsStore;
|
||||
})();
|
|
@ -160,11 +160,23 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
});
|
||||
|
||||
var StandaloneRoomHeader = React.createClass({displayName: "StandaloneRoomHeader",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
},
|
||||
|
||||
recordClick: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: "Support link click"
|
||||
}));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("header", null,
|
||||
React.createElement("h1", null, mozL10n.get("clientShortname2")),
|
||||
React.createElement("a", {target: "_blank", href: loop.config.generalSupportUrl},
|
||||
React.createElement("a", {href: loop.config.generalSupportUrl,
|
||||
onClick: this.recordClick,
|
||||
target: "_blank"},
|
||||
React.createElement("i", {className: "icon icon-help"})
|
||||
)
|
||||
)
|
||||
|
@ -173,7 +185,14 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
});
|
||||
|
||||
var StandaloneRoomFooter = React.createClass({displayName: "StandaloneRoomFooter",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
},
|
||||
|
||||
_getContent: function() {
|
||||
// We use this technique of static markup as it means we get
|
||||
// just one overall string for L10n to define the structure of
|
||||
// the whole item.
|
||||
return mozL10n.get("legal_text_and_links", {
|
||||
"clientShortname": mozL10n.get("clientShortname2"),
|
||||
"terms_of_use_url": React.renderToStaticMarkup(
|
||||
|
@ -189,10 +208,21 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
});
|
||||
},
|
||||
|
||||
recordClick: function(event) {
|
||||
// Check for valid href, as this is clicking on the paragraph -
|
||||
// so the user may be clicking on the text rather than the link.
|
||||
if (event.target && event.target.href) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: event.target.href
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("footer", null,
|
||||
React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()}}),
|
||||
React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()},
|
||||
onClick: this.recordClick}),
|
||||
React.createElement("div", {className: "footer-logo"})
|
||||
)
|
||||
);
|
||||
|
@ -201,10 +231,17 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
|
||||
var StandaloneRoomContextItem = React.createClass({displayName: "StandaloneRoomContextItem",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
receivingScreenShare: React.PropTypes.bool,
|
||||
roomContextUrl: React.PropTypes.object
|
||||
},
|
||||
|
||||
recordClick: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: "Shared URL"
|
||||
}));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.roomContextUrl ||
|
||||
!this.props.roomContextUrl.location) {
|
||||
|
@ -225,7 +262,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
React.createElement("img", {src: this.props.roomContextUrl.thumbnail}),
|
||||
React.createElement("div", {className: "standalone-context-url-description-wrapper"},
|
||||
this.props.roomContextUrl.description,
|
||||
React.createElement("br", null), React.createElement("a", {href: location}, location)
|
||||
React.createElement("br", null), React.createElement("a", {href: location,
|
||||
onClick: this.recordClick,
|
||||
target: "_blank"}, location)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -234,6 +273,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
|
||||
var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
receivingScreenShare: React.PropTypes.bool.isRequired,
|
||||
roomContextUrls: React.PropTypes.array,
|
||||
roomName: React.PropTypes.string,
|
||||
|
@ -259,6 +299,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
React.createElement("div", {className: "standalone-room-info"},
|
||||
React.createElement("h2", {className: "room-name"}, this.props.roomName),
|
||||
React.createElement(StandaloneRoomContextItem, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
receivingScreenShare: this.props.receivingScreenShare,
|
||||
roomContextUrl: roomContextUrl})
|
||||
)
|
||||
|
@ -517,7 +558,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
return (
|
||||
React.createElement("div", {className: "room-conversation-wrapper"},
|
||||
React.createElement("div", {className: "beta-logo"}),
|
||||
React.createElement(StandaloneRoomHeader, null),
|
||||
React.createElement(StandaloneRoomHeader, {dispatcher: this.props.dispatcher}),
|
||||
React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState,
|
||||
failureReason: this.state.failureReason,
|
||||
joinRoom: this.joinRoom,
|
||||
|
@ -527,6 +568,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
React.createElement("div", {className: "video-layout-wrapper"},
|
||||
React.createElement("div", {className: "conversation room-conversation"},
|
||||
React.createElement(StandaloneRoomContextView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
receivingScreenShare: this.state.receivingScreenShare,
|
||||
roomContextUrls: this.state.roomContextUrls,
|
||||
roomName: this.state.roomName,
|
||||
|
@ -556,7 +598,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
React.createElement(loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView, {
|
||||
marketplaceSrc: this.state.marketplaceSrc,
|
||||
onMarketplaceMessage: this.state.onMarketplaceMessage}),
|
||||
React.createElement(StandaloneRoomFooter, null)
|
||||
React.createElement(StandaloneRoomFooter, {dispatcher: this.props.dispatcher})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -564,6 +606,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
|
||||
return {
|
||||
StandaloneRoomContextView: StandaloneRoomContextView,
|
||||
StandaloneRoomFooter: StandaloneRoomFooter,
|
||||
StandaloneRoomHeader: StandaloneRoomHeader,
|
||||
StandaloneRoomView: StandaloneRoomView
|
||||
};
|
||||
})(navigator.mozL10n);
|
||||
|
|
|
@ -160,11 +160,23 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
});
|
||||
|
||||
var StandaloneRoomHeader = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
},
|
||||
|
||||
recordClick: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: "Support link click"
|
||||
}));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<header>
|
||||
<h1>{mozL10n.get("clientShortname2")}</h1>
|
||||
<a target="_blank" href={loop.config.generalSupportUrl}>
|
||||
<a href={loop.config.generalSupportUrl}
|
||||
onClick={this.recordClick}
|
||||
target="_blank">
|
||||
<i className="icon icon-help"></i>
|
||||
</a>
|
||||
</header>
|
||||
|
@ -173,7 +185,14 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
});
|
||||
|
||||
var StandaloneRoomFooter = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
},
|
||||
|
||||
_getContent: function() {
|
||||
// We use this technique of static markup as it means we get
|
||||
// just one overall string for L10n to define the structure of
|
||||
// the whole item.
|
||||
return mozL10n.get("legal_text_and_links", {
|
||||
"clientShortname": mozL10n.get("clientShortname2"),
|
||||
"terms_of_use_url": React.renderToStaticMarkup(
|
||||
|
@ -189,10 +208,21 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
});
|
||||
},
|
||||
|
||||
recordClick: function(event) {
|
||||
// Check for valid href, as this is clicking on the paragraph -
|
||||
// so the user may be clicking on the text rather than the link.
|
||||
if (event.target && event.target.href) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: event.target.href
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<footer>
|
||||
<p dangerouslySetInnerHTML={{__html: this._getContent()}}></p>
|
||||
<p dangerouslySetInnerHTML={{__html: this._getContent()}}
|
||||
onClick={this.recordClick}></p>
|
||||
<div className="footer-logo" />
|
||||
</footer>
|
||||
);
|
||||
|
@ -201,10 +231,17 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
|
||||
var StandaloneRoomContextItem = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
receivingScreenShare: React.PropTypes.bool,
|
||||
roomContextUrl: React.PropTypes.object
|
||||
},
|
||||
|
||||
recordClick: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: "Shared URL"
|
||||
}));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.roomContextUrl ||
|
||||
!this.props.roomContextUrl.location) {
|
||||
|
@ -225,7 +262,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
<img src={this.props.roomContextUrl.thumbnail} />
|
||||
<div className="standalone-context-url-description-wrapper">
|
||||
{this.props.roomContextUrl.description}
|
||||
<br /><a href={location}>{location}</a>
|
||||
<br /><a href={location}
|
||||
onClick={this.recordClick}
|
||||
target="_blank">{location}</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -234,6 +273,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
|
||||
var StandaloneRoomContextView = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
receivingScreenShare: React.PropTypes.bool.isRequired,
|
||||
roomContextUrls: React.PropTypes.array,
|
||||
roomName: React.PropTypes.string,
|
||||
|
@ -259,6 +299,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
<div className="standalone-room-info">
|
||||
<h2 className="room-name">{this.props.roomName}</h2>
|
||||
<StandaloneRoomContextItem
|
||||
dispatcher={this.props.dispatcher}
|
||||
receivingScreenShare={this.props.receivingScreenShare}
|
||||
roomContextUrl={roomContextUrl} />
|
||||
</div>
|
||||
|
@ -517,7 +558,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
return (
|
||||
<div className="room-conversation-wrapper">
|
||||
<div className="beta-logo" />
|
||||
<StandaloneRoomHeader />
|
||||
<StandaloneRoomHeader dispatcher={this.props.dispatcher} />
|
||||
<StandaloneRoomInfoArea roomState={this.state.roomState}
|
||||
failureReason={this.state.failureReason}
|
||||
joinRoom={this.joinRoom}
|
||||
|
@ -527,6 +568,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
<div className="video-layout-wrapper">
|
||||
<div className="conversation room-conversation">
|
||||
<StandaloneRoomContextView
|
||||
dispatcher={this.props.dispatcher}
|
||||
receivingScreenShare={this.state.receivingScreenShare}
|
||||
roomContextUrls={this.state.roomContextUrls}
|
||||
roomName={this.state.roomName}
|
||||
|
@ -556,7 +598,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
<loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
|
||||
marketplaceSrc={this.state.marketplaceSrc}
|
||||
onMarketplaceMessage={this.state.onMarketplaceMessage} />
|
||||
<StandaloneRoomFooter />
|
||||
<StandaloneRoomFooter dispatcher={this.props.dispatcher} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -564,6 +606,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
|
||||
return {
|
||||
StandaloneRoomContextView: StandaloneRoomContextView,
|
||||
StandaloneRoomFooter: StandaloneRoomFooter,
|
||||
StandaloneRoomHeader: StandaloneRoomHeader,
|
||||
StandaloneRoomView: StandaloneRoomView
|
||||
};
|
||||
})(navigator.mozL10n);
|
||||
|
|
|
@ -1087,8 +1087,16 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
|||
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||
feedbackClient: feedbackClient
|
||||
});
|
||||
var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
|
||||
activeRoomStore: activeRoomStore
|
||||
});
|
||||
|
||||
loop.store.StoreMixin.register({feedbackStore: feedbackStore});
|
||||
loop.store.StoreMixin.register({
|
||||
feedbackStore: feedbackStore,
|
||||
// This isn't used in any views, but is saved here to ensure it
|
||||
// is kept alive.
|
||||
standaloneMetricsStore: standaloneMetricsStore
|
||||
});
|
||||
|
||||
window.addEventListener("unload", function() {
|
||||
dispatcher.dispatch(new sharedActions.WindowUnload());
|
||||
|
|
|
@ -1087,8 +1087,16 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
|||
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||
feedbackClient: feedbackClient
|
||||
});
|
||||
var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
|
||||
activeRoomStore: activeRoomStore
|
||||
});
|
||||
|
||||
loop.store.StoreMixin.register({feedbackStore: feedbackStore});
|
||||
loop.store.StoreMixin.register({
|
||||
feedbackStore: feedbackStore,
|
||||
// This isn't used in any views, but is saved here to ensure it
|
||||
// is kept alive.
|
||||
standaloneMetricsStore: standaloneMetricsStore
|
||||
});
|
||||
|
||||
window.addEventListener("unload", function() {
|
||||
dispatcher.dispatch(new sharedActions.WindowUnload());
|
||||
|
|
|
@ -60,12 +60,14 @@
|
|||
<script src="../../standalone/content/js/standaloneMozLoop.js"></script>
|
||||
<script src="../../standalone/content/js/fxOSMarketplace.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneMetricsStore.js"></script>
|
||||
<script src="../../standalone/content/js/webapp.js"></script>
|
||||
<!-- Test scripts -->
|
||||
<script src="standalone_client_test.js"></script>
|
||||
<script src="standaloneAppStore_test.js"></script>
|
||||
<script src="standaloneMozLoop_test.js"></script>
|
||||
<script src="standaloneRoomViews_test.js"></script>
|
||||
<script src="standaloneMetricsStore_test.js"></script>
|
||||
<script src="webapp_test.js"></script>
|
||||
<script src="multiplexGum_test.js"></script>
|
||||
<script>
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/* 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/. */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.store.StandaloneMetricsStore", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox, dispatcher, store, fakeActiveRoomStore;
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var METRICS_GA_CATEGORY = loop.store.METRICS_GA_CATEGORY;
|
||||
var METRICS_GA_ACTIONS = loop.store.METRICS_GA_ACTIONS;
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
dispatcher = new loop.Dispatcher();
|
||||
|
||||
window.ga = sinon.stub();
|
||||
|
||||
var fakeStore = loop.store.createStore({
|
||||
getInitialStoreState: function() {
|
||||
return {
|
||||
audioMuted: false,
|
||||
roomState: ROOM_STATES.INIT,
|
||||
videoMuted: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
fakeActiveRoomStore = new fakeStore(dispatcher);
|
||||
|
||||
store = new loop.store.StandaloneMetricsStore(dispatcher, {
|
||||
activeRoomStore: fakeActiveRoomStore
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
delete window.ga;
|
||||
});
|
||||
|
||||
describe("Action Handlers", function() {
|
||||
beforeEach(function() {
|
||||
});
|
||||
|
||||
it("should log an event on GotMediaPermission", function() {
|
||||
store.gotMediaPermission();
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
|
||||
"Media granted");
|
||||
});
|
||||
|
||||
it("should log an event on JoinRoom", function() {
|
||||
store.joinRoom();
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
|
||||
"Join the conversation");
|
||||
});
|
||||
|
||||
it("should log an event on LeaveRoom", function() {
|
||||
store.leaveRoom();
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
|
||||
"Leave conversation");
|
||||
});
|
||||
|
||||
it("should log an event on MediaConnected", function() {
|
||||
store.mediaConnected();
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
|
||||
"Media connected");
|
||||
});
|
||||
|
||||
it("should log an event on RecordClick", function() {
|
||||
store.recordClick(new sharedActions.RecordClick({
|
||||
linkInfo: "fake"
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.linkClick,
|
||||
"fake");
|
||||
})
|
||||
});
|
||||
|
||||
describe("Store Change Handlers", function() {
|
||||
it("should log an event on room full", function() {
|
||||
fakeActiveRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
|
||||
"Room full");
|
||||
});
|
||||
|
||||
it("should log an event when the room is expired or invalid", function() {
|
||||
fakeActiveRoomStore.setStoreState({
|
||||
roomState: ROOM_STATES.FAILED,
|
||||
failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
|
||||
"Link expired or invalid");
|
||||
});
|
||||
|
||||
it("should log an event when video mute is changed", function() {
|
||||
fakeActiveRoomStore.setStoreState({
|
||||
videoMuted: true
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.faceMute,
|
||||
"mute");
|
||||
});
|
||||
|
||||
it("should log an event when audio mute is changed", function() {
|
||||
fakeActiveRoomStore.setStoreState({
|
||||
audioMuted: true
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.audioMute,
|
||||
"mute");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -45,7 +45,10 @@ describe("loop.standaloneRoomViews", function() {
|
|||
});
|
||||
|
||||
function mountTestComponent(extraProps) {
|
||||
var props = _.extend({ receivingScreenShare: false }, extraProps);
|
||||
var props = _.extend({
|
||||
dispatcher: dispatcher,
|
||||
receivingScreenShare: false
|
||||
}, extraProps);
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(
|
||||
loop.standaloneRoomViews.StandaloneRoomContextView, props));
|
||||
|
@ -98,6 +101,48 @@ describe("loop.standaloneRoomViews", function() {
|
|||
|
||||
expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null);
|
||||
});
|
||||
|
||||
it("should dispatch a RecordClick action when the link is clicked", function() {
|
||||
var view = mountTestComponent({
|
||||
roomName: "Mark's room",
|
||||
roomContextUrls: [{
|
||||
description: "Mark's super page",
|
||||
location: "http://invalid.com",
|
||||
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||
}]
|
||||
});
|
||||
|
||||
TestUtils.Simulate.click(view.getDOMNode()
|
||||
.querySelector(".standalone-context-url-description-wrapper > a"));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.RecordClick({
|
||||
linkInfo: "Shared URL"
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("StandaloneRoomHeader", function() {
|
||||
function mountTestComponent() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(
|
||||
loop.standaloneRoomViews.StandaloneRoomHeader, {
|
||||
dispatcher: dispatcher
|
||||
}));
|
||||
}
|
||||
|
||||
it("should dispatch a RecordClick action when the support link is clicked", function() {
|
||||
var view = mountTestComponent();
|
||||
|
||||
TestUtils.Simulate.click(view.getDOMNode().querySelector("a"));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.RecordClick({
|
||||
linkInfo: "Support link click"
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("StandaloneRoomView", function() {
|
||||
|
|
|
@ -34,6 +34,25 @@ function testMalware(event) {
|
|||
var style = content.getComputedStyle(el, null);
|
||||
is(style.display, "inline-block", "Ignore Warning button should be display:inline-block for malware");
|
||||
|
||||
// Now launch the unwanted software test
|
||||
window.addEventListener("DOMContentLoaded", testUnwanted, true);
|
||||
content.location = "http://www.itisatrap.org/firefox/unwanted.html";
|
||||
}
|
||||
|
||||
function testUnwanted(event) {
|
||||
if (event.target != gBrowser.selectedBrowser.contentDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.removeEventListener("DOMContentLoaded", testUnwanted, true);
|
||||
|
||||
// Confirm that "Ignore this warning" is visible - bug 422410
|
||||
var el = content.document.getElementById("ignoreWarningButton");
|
||||
ok(el, "Ignore warning button should be present for unwanted software");
|
||||
|
||||
var style = content.getComputedStyle(el, null);
|
||||
is(style.display, "inline-block", "Ignore Warning button should be display:inline-block for unwanted software");
|
||||
|
||||
// Now launch the phishing test
|
||||
window.addEventListener("DOMContentLoaded", testPhishing, true);
|
||||
content.location = "http://www.itisatrap.org/firefox/its-a-trap.html";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Force SafeBrowsing to be initialized for the tests
|
||||
Services.prefs.setCharPref("urlclassifier.malwareTable", "test-malware-simple");
|
||||
Services.prefs.setCharPref("urlclassifier.malwareTable", "test-malware-simple,test-unwanted-simple");
|
||||
Services.prefs.setCharPref("urlclassifier.phishTable", "test-phish-simple");
|
||||
SafeBrowsing.init();
|
||||
|
||||
|
|
|
@ -11,9 +11,6 @@ add_task(function*() {
|
|||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {controller, inspector} = yield openAnimationInspector();
|
||||
|
||||
is(controller.animationPlayers.length, 0,
|
||||
"There are no AnimationPlayerFront objects at first");
|
||||
|
||||
info("Selecting an animated node");
|
||||
// selectNode waits for the inspector-updated event before resolving, which
|
||||
// means the controller.PLAYERS_UPDATED_EVENT event has been emitted before
|
||||
|
|
|
@ -10,15 +10,17 @@ add_task(function*() {
|
|||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel, controller} = yield openAnimationInspector();
|
||||
|
||||
info("Apply 2 finite animations to the test node");
|
||||
info("Select the test node");
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
info("Apply 2 finite animations to the test node and wait for the widgets to appear");
|
||||
let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
yield executeInContent("devtools:test:setAttribute", {
|
||||
selector: ".still",
|
||||
attributeName: "class",
|
||||
attributeValue: "ball still multi-finite"
|
||||
});
|
||||
|
||||
info("Select the test node");
|
||||
yield selectNode(".still", inspector);
|
||||
yield onUiUpdated;
|
||||
|
||||
is(controller.animationPlayers.length, 2, "2 animation players exist");
|
||||
|
||||
|
|
|
@ -14,15 +14,17 @@ add_task(function*() {
|
|||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Start an animation on the test node");
|
||||
info("Select the test node");
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
info("Start an animation on the test node and wait for the widget to appear");
|
||||
let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
yield executeInContent("devtools:test:setAttribute", {
|
||||
selector: ".still",
|
||||
attributeName: "class",
|
||||
attributeValue: "ball still short"
|
||||
});
|
||||
|
||||
info("Select the node");
|
||||
yield selectNode(".still", inspector);
|
||||
yield onUiUpdated;
|
||||
|
||||
info("Wait until the animation ends");
|
||||
let widget = panel.playerWidgets[0];
|
||||
|
|
|
@ -31,7 +31,8 @@ add_task(function*() {
|
|||
info("Faking an older server version by setting " +
|
||||
"AnimationsController.hasSetCurrentTime to false");
|
||||
|
||||
yield selectNode("body", inspector);
|
||||
// Selecting <div.still> to make sure no widgets are displayed in the panel.
|
||||
yield selectNode(".still", inspector);
|
||||
controller.hasSetCurrentTime = false;
|
||||
|
||||
info("Selecting the animated node again");
|
||||
|
@ -46,13 +47,12 @@ add_task(function*() {
|
|||
ok(container.children[0].classList.contains("toggle"),
|
||||
"The first button is the play/pause button");
|
||||
|
||||
yield selectNode("body", inspector);
|
||||
yield selectNode(".still", inspector);
|
||||
controller.hasSetCurrentTime = true;
|
||||
|
||||
info("Faking an older server version by setting " +
|
||||
"AnimationsController.hasSetPlaybackRate to false");
|
||||
|
||||
yield selectNode("body", inspector);
|
||||
controller.hasSetPlaybackRate = false;
|
||||
|
||||
info("Selecting the animated node again");
|
||||
|
@ -65,6 +65,6 @@ add_task(function*() {
|
|||
ok(!container.querySelector("select"),
|
||||
"The playback rate select does not exist");
|
||||
|
||||
yield selectNode("body", inspector);
|
||||
yield selectNode(".still", inspector);
|
||||
controller.hasSetPlaybackRate = true;
|
||||
});
|
||||
|
|
|
@ -10,6 +10,9 @@ add_task(function*() {
|
|||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Select the non-animated test node");
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
ok(!panel.toggleAllButtonEl.classList.contains("paused"),
|
||||
"The toggle button is in its running state by default");
|
||||
|
||||
|
|
|
@ -882,6 +882,14 @@ InplaceEditor.prototype = {
|
|||
increment *= smallIncrement;
|
||||
}
|
||||
|
||||
// Use default cursor movement rather than providing auto-suggestions.
|
||||
if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_HOME
|
||||
|| aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_END
|
||||
|| aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
|
||||
|| aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
|
||||
this._preventSuggestions = true;
|
||||
}
|
||||
|
||||
let cycling = false;
|
||||
if (increment && this._incrementValue(increment) ) {
|
||||
this._updateSize();
|
||||
|
|
|
@ -27,6 +27,9 @@ support-files =
|
|||
[browser_filter-editor-05.js]
|
||||
[browser_filter-editor-06.js]
|
||||
[browser_filter-editor-07.js]
|
||||
[browser_filter-editor-08.js]
|
||||
[browser_filter-editor-09.js]
|
||||
[browser_filter-editor-10.js]
|
||||
[browser_flame-graph-01.js]
|
||||
[browser_flame-graph-02.js]
|
||||
[browser_flame-graph-03a.js]
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the Filter Editor Widget inputs increase/decrease value using
|
||||
// arrow keys, applying multiplier using alt/shift on number-type filters
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/filter-frame.xhtml";
|
||||
const {CSSFilterEditorWidget} = require("devtools/shared/widgets/FilterWidget");
|
||||
|
||||
const FAST_VALUE_MULTIPLIER = 10;
|
||||
const SLOW_VALUE_MULTIPLIER = 0.1;
|
||||
const DEFAULT_VALUE_MULTIPLIER = 1;
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
let [host, win, doc] = yield createHost("bottom", TEST_URI);
|
||||
|
||||
const container = doc.querySelector("#container");
|
||||
const initialValue = "blur(2px)";
|
||||
let widget = new CSSFilterEditorWidget(container, initialValue);
|
||||
|
||||
let value = 2;
|
||||
|
||||
triggerKey = triggerKey.bind(widget);
|
||||
|
||||
info("Test simple arrow keys");
|
||||
triggerKey(40);
|
||||
|
||||
value -= DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), `${value}px`,
|
||||
"Should decrease value using down arrow");
|
||||
|
||||
triggerKey(38);
|
||||
|
||||
value += DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), `${value}px`,
|
||||
"Should decrease value using down arrow");
|
||||
|
||||
info("Test shift key multiplier");
|
||||
triggerKey(38, "shiftKey");
|
||||
|
||||
value += FAST_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), `${value}px`,
|
||||
"Should increase value by fast multiplier using up arrow");
|
||||
|
||||
triggerKey(40, "shiftKey");
|
||||
|
||||
value -= FAST_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), `${value}px`,
|
||||
"Should decrease value by fast multiplier using down arrow");
|
||||
|
||||
info("Test alt key multiplier");
|
||||
triggerKey(38, "altKey");
|
||||
|
||||
value += SLOW_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), `${value}px`,
|
||||
"Should increase value by slow multiplier using up arrow");
|
||||
|
||||
triggerKey(40, "altKey");
|
||||
|
||||
value -= SLOW_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), `${value}px`,
|
||||
"Should decrease value by slow multiplier using down arrow");
|
||||
|
||||
triggerKey = null;
|
||||
});
|
||||
|
||||
// Triggers the specified keyCode and modifier key on
|
||||
// first filter's input
|
||||
function triggerKey(key, modifier) {
|
||||
const filter = this.el.querySelector(".filters").children[0];
|
||||
const input = filter.querySelector("input");
|
||||
|
||||
this._keyDown({
|
||||
target: input,
|
||||
keyCode: key,
|
||||
[modifier]: true,
|
||||
preventDefault: function() {}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the Filter Editor Widget inputs increase/decrease value when cursor is
|
||||
// on a number using arrow keys, applying multiplier using alt/shift on strings
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/filter-frame.xhtml";
|
||||
const {CSSFilterEditorWidget} = require("devtools/shared/widgets/FilterWidget");
|
||||
|
||||
const FAST_VALUE_MULTIPLIER = 10;
|
||||
const SLOW_VALUE_MULTIPLIER = 0.1;
|
||||
const DEFAULT_VALUE_MULTIPLIER = 1;
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
let [host, win, doc] = yield createHost("bottom", TEST_URI);
|
||||
|
||||
const container = doc.querySelector("#container");
|
||||
const initialValue = "drop-shadow(rgb(0, 0, 0) 1px 1px 0px)";
|
||||
let widget = new CSSFilterEditorWidget(container, initialValue);
|
||||
widget.el.querySelector("input").setSelectionRange(13, 13);
|
||||
|
||||
let value = 1;
|
||||
|
||||
triggerKey = triggerKey.bind(widget);
|
||||
|
||||
info("Test simple arrow keys");
|
||||
triggerKey(40);
|
||||
|
||||
value -= DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should decrease value using down arrow");
|
||||
|
||||
triggerKey(38);
|
||||
|
||||
value += DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should decrease value using down arrow");
|
||||
|
||||
info("Test shift key multiplier");
|
||||
triggerKey(38, "shiftKey");
|
||||
|
||||
value += FAST_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should increase value by fast multiplier using up arrow");
|
||||
|
||||
triggerKey(40, "shiftKey");
|
||||
|
||||
value -= FAST_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should decrease value by fast multiplier using down arrow");
|
||||
|
||||
info("Test alt key multiplier");
|
||||
triggerKey(38, "altKey");
|
||||
|
||||
value += SLOW_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should increase value by slow multiplier using up arrow");
|
||||
|
||||
triggerKey(40, "altKey");
|
||||
|
||||
value -= SLOW_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should decrease value by slow multiplier using down arrow");
|
||||
|
||||
triggerKey(40, "shiftKey");
|
||||
|
||||
value -= FAST_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should decrease to negative");
|
||||
|
||||
triggerKey(40);
|
||||
|
||||
value -= DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should decrease negative numbers correctly");
|
||||
|
||||
triggerKey(38);
|
||||
|
||||
value += DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should increase negative values correctly");
|
||||
|
||||
triggerKey(40, "altKey");
|
||||
triggerKey(40, "altKey");
|
||||
|
||||
value -= SLOW_VALUE_MULTIPLIER * 2;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should decrease float numbers correctly");
|
||||
|
||||
triggerKey(38, "altKey");
|
||||
|
||||
value += SLOW_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should increase float numbers correctly");
|
||||
|
||||
triggerKey = null;
|
||||
});
|
||||
|
||||
// Triggers the specified keyCode and modifier key on
|
||||
// first filter's input
|
||||
function triggerKey(key, modifier) {
|
||||
const filter = this.el.querySelector(".filters").children[0];
|
||||
const input = filter.querySelector("input");
|
||||
|
||||
this._keyDown({
|
||||
target: input,
|
||||
keyCode: key,
|
||||
[modifier]: true,
|
||||
preventDefault: function() {}
|
||||
});
|
||||
}
|
||||
|
||||
function val(value) {
|
||||
let v = value.toFixed(1);
|
||||
|
||||
if (v.indexOf(".0") > -1) {
|
||||
v = v.slice(0, -2);
|
||||
}
|
||||
return `rgb(0, 0, 0) ${v}px 1px 0px`;
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the Filter Editor Widget inputs increase/decrease value when cursor is
|
||||
// on a number using arrow keys if cursor is behind/mid/after the number strings
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/filter-frame.xhtml";
|
||||
const {CSSFilterEditorWidget} = require("devtools/shared/widgets/FilterWidget");
|
||||
|
||||
const FAST_VALUE_MULTIPLIER = 10;
|
||||
const SLOW_VALUE_MULTIPLIER = 0.1;
|
||||
const DEFAULT_VALUE_MULTIPLIER = 1;
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
let [host, win, doc] = yield createHost("bottom", TEST_URI);
|
||||
|
||||
const container = doc.querySelector("#container");
|
||||
const initialValue = "drop-shadow(rgb(0, 0, 0) 10px 1px 0px)";
|
||||
let widget = new CSSFilterEditorWidget(container, initialValue);
|
||||
const input = widget.el.querySelector("input");
|
||||
|
||||
let value = 10;
|
||||
|
||||
triggerKey = triggerKey.bind(widget);
|
||||
|
||||
info("Test increment/decrement of string-type numbers without selection");
|
||||
|
||||
input.setSelectionRange(14, 14);
|
||||
triggerKey(40);
|
||||
|
||||
value -= DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should work with cursor in the middle of number");
|
||||
|
||||
input.setSelectionRange(13, 13);
|
||||
triggerKey(38);
|
||||
|
||||
value += DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should work with cursor before the number");
|
||||
|
||||
input.setSelectionRange(15, 15);
|
||||
triggerKey(40);
|
||||
|
||||
value -= DEFAULT_VALUE_MULTIPLIER;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should work with cursor after the number");
|
||||
|
||||
info("Test increment/decrement of string-type numbers with a selection");
|
||||
|
||||
input.setSelectionRange(13, 15);
|
||||
triggerKey(38);
|
||||
input.setSelectionRange(13, 18);
|
||||
triggerKey(38);
|
||||
|
||||
value += DEFAULT_VALUE_MULTIPLIER * 2;
|
||||
is(widget.getValueAt(0), val(value),
|
||||
"Should work if a there is a selection, starting with the number");
|
||||
|
||||
triggerKey = null;
|
||||
});
|
||||
|
||||
// Triggers the specified keyCode and modifier key on
|
||||
// first filter's input
|
||||
function triggerKey(key, modifier) {
|
||||
const filter = this.el.querySelector(".filters").children[0];
|
||||
const input = filter.querySelector("input");
|
||||
|
||||
this._keyDown({
|
||||
target: input,
|
||||
keyCode: key,
|
||||
[modifier]: true,
|
||||
preventDefault: function() {}
|
||||
});
|
||||
}
|
||||
|
||||
function val(value) {
|
||||
let v = value.toFixed(1);
|
||||
|
||||
if (v.indexOf(".0") > -1) {
|
||||
v = v.slice(0, -2);
|
||||
}
|
||||
return `rgb(0, 0, 0) ${v}px 1px 0px`;
|
||||
}
|
|
@ -113,6 +113,7 @@ function CSSFilterEditorWidget(el, value = "") {
|
|||
this._mouseMove = this._mouseMove.bind(this);
|
||||
this._mouseUp = this._mouseUp.bind(this);
|
||||
this._mouseDown = this._mouseDown.bind(this);
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this._input = this._input.bind(this);
|
||||
|
||||
this._initMarkup();
|
||||
|
@ -195,6 +196,7 @@ CSSFilterEditorWidget.prototype = {
|
|||
this.addButton.addEventListener("click", this._addButtonClick);
|
||||
this.list.addEventListener("click", this._removeButtonClick);
|
||||
this.list.addEventListener("mousedown", this._mouseDown);
|
||||
this.list.addEventListener("keydown", this._keyDown);
|
||||
|
||||
// These events are event delegators for
|
||||
// drag-drop re-ordering and label-dragging
|
||||
|
@ -205,9 +207,77 @@ CSSFilterEditorWidget.prototype = {
|
|||
this.list.addEventListener("input", this._input);
|
||||
},
|
||||
|
||||
_getFilterElementIndex: function(el) {
|
||||
return [...this.list.children].indexOf(el);
|
||||
},
|
||||
|
||||
_keyDown: function(e) {
|
||||
if (e.target.tagName.toLowerCase() !== "input" ||
|
||||
(e.keyCode !== 40 && e.keyCode !== 38)) {
|
||||
return;
|
||||
}
|
||||
let input = e.target;
|
||||
|
||||
const direction = e.keyCode === 40 ? -1 : 1;
|
||||
|
||||
let multiplier = DEFAULT_VALUE_MULTIPLIER;
|
||||
if (e.altKey) {
|
||||
multiplier = SLOW_VALUE_MULTIPLIER;
|
||||
} else if (e.shiftKey) {
|
||||
multiplier = FAST_VALUE_MULTIPLIER;
|
||||
}
|
||||
|
||||
const filterEl = e.target.closest(".filter");
|
||||
const index = this._getFilterElementIndex(filterEl);
|
||||
const filter = this.filters[index];
|
||||
|
||||
// Filters that have units are number-type filters. For them,
|
||||
// the value can be incremented/decremented simply.
|
||||
// For other types of filters (e.g. drop-shadow) we need to check
|
||||
// if the keypress happened close to a number first.
|
||||
if (filter.unit) {
|
||||
let startValue = parseFloat(e.target.value);
|
||||
let value = startValue + direction * multiplier;
|
||||
|
||||
const [min, max] = this._definition(filter.name).range;
|
||||
value = value < min ? min :
|
||||
value > max ? max : value;
|
||||
|
||||
input.value = fixFloat(value);
|
||||
|
||||
this.updateValueAt(index, value);
|
||||
} else {
|
||||
let selectionStart = input.selectionStart;
|
||||
let num = getNeighbourNumber(input.value, selectionStart);
|
||||
if (!num) {
|
||||
return;
|
||||
}
|
||||
|
||||
let {start, end, value} = num;
|
||||
|
||||
let split = input.value.split("");
|
||||
let computed = fixFloat(value + direction * multiplier),
|
||||
dotIndex = computed.indexOf(".0");
|
||||
if (dotIndex > -1) {
|
||||
computed = computed.slice(0, -2);
|
||||
|
||||
selectionStart = selectionStart > start + dotIndex ?
|
||||
start + dotIndex :
|
||||
selectionStart;
|
||||
}
|
||||
split.splice(start, end - start, computed);
|
||||
|
||||
value = split.join("");
|
||||
input.value = value;
|
||||
this.updateValueAt(index, value);
|
||||
input.setSelectionRange(selectionStart, selectionStart);
|
||||
}
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
_input: function(e) {
|
||||
let filterEl = e.target.closest(".filter"),
|
||||
index = [...this.list.children].indexOf(filterEl),
|
||||
index = this._getFilterElementIndex(filterEl),
|
||||
filter = this.filters[index],
|
||||
def = this._definition(filter.name);
|
||||
|
||||
|
@ -231,7 +301,7 @@ CSSFilterEditorWidget.prototype = {
|
|||
} else if (e.target.classList.contains("devtools-draglabel")) {
|
||||
let label = e.target,
|
||||
input = filterEl.querySelector("input"),
|
||||
index = [...this.list.children].indexOf(filterEl);
|
||||
index = this._getFilterElementIndex(filterEl);
|
||||
|
||||
this._dragging = {
|
||||
index, label, input,
|
||||
|
@ -273,7 +343,7 @@ CSSFilterEditorWidget.prototype = {
|
|||
}
|
||||
|
||||
let filterEl = e.target.closest(".filter");
|
||||
let index = [...this.list.children].indexOf(filterEl);
|
||||
let index = this._getFilterElementIndex(filterEl);
|
||||
this.removeAt(index);
|
||||
},
|
||||
|
||||
|
@ -622,6 +692,7 @@ CSSFilterEditorWidget.prototype = {
|
|||
this.addButton.removeEventListener("click", this._addButtonClick);
|
||||
this.list.removeEventListener("click", this._removeButtonClick);
|
||||
this.list.removeEventListener("mousedown", this._mouseDown);
|
||||
this.list.removeEventListener("keydown", this._keyDown);
|
||||
|
||||
// These events are used for drag drop re-ordering
|
||||
this.win.removeEventListener("mousemove", this._mouseMove);
|
||||
|
@ -718,3 +789,40 @@ function tokenizeComputedFilter(css) {
|
|||
|
||||
return filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds neighbour number characters of an index in a string
|
||||
* the numbers may be floats (containing dots)
|
||||
* It's assumed that the value given to this function is a valid number
|
||||
*
|
||||
* @param {String} string
|
||||
* The string containing numbers
|
||||
* @param {Number} index
|
||||
* The index to look for neighbours for
|
||||
* @return {Object}
|
||||
* returns null if no number is found
|
||||
* value: The number found
|
||||
* start: The number's starting index
|
||||
* end: The number's ending index
|
||||
*/
|
||||
function getNeighbourNumber(string, index) {
|
||||
if (!/\d/.test(string)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let left = /-?[0-9.]*$/.exec(string.slice(0, index)),
|
||||
right = /-?[0-9.]*/.exec(string.slice(index));
|
||||
|
||||
left = left ? left[0] : "";
|
||||
right = right ? right[0] : "";
|
||||
|
||||
if (!right && !left) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
value: fixFloat(left + right, true),
|
||||
start: index - left.length,
|
||||
end: index + right.length
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,11 +5,9 @@
|
|||
// A test to check the 'Open Link in new tab' functionality in the
|
||||
// context menu item for stylesheets (bug 992947).
|
||||
const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
|
||||
waitForExplicitFinish();
|
||||
|
||||
add_task(function*() {
|
||||
let panel = yield addTabAndOpenStyleEditors(2, null, TESTCASE_URI);
|
||||
let ui = panel.UI;
|
||||
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
|
||||
|
||||
yield rightClickStyleSheet(ui, ui.editors[0]);
|
||||
is(ui._openLinkNewTabItem.getAttribute("disabled"), "false", "The menu item is not disabled");
|
||||
|
@ -73,7 +71,7 @@ function rightClickStyleSheet(ui, editor) {
|
|||
EventUtils.synthesizeMouseAtCenter(
|
||||
editor.summary.querySelector(".stylesheet-name"),
|
||||
{button: 2, type: "contextmenu"},
|
||||
gPanelWindow);
|
||||
ui._window);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
@ -91,7 +89,7 @@ function rightClickInlineStyleSheet(ui, editor) {
|
|||
EventUtils.synthesizeMouseAtCenter(
|
||||
editor.summary.querySelector(".stylesheet-name"),
|
||||
{button: 2, type: "contextmenu"},
|
||||
gPanelWindow);
|
||||
ui._window);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
@ -109,7 +107,7 @@ function rightClickNoStyleSheet(ui) {
|
|||
EventUtils.synthesizeMouseAtCenter(
|
||||
ui._panelDoc.querySelector("#splitview-tpl-summary-stylesheet"),
|
||||
{button: 2, type: "contextmenu"},
|
||||
gPanelWindow);
|
||||
ui._window);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ let TargetFactory = devtools.TargetFactory;
|
|||
let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
|
||||
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
|
||||
let gPanelWindow;
|
||||
|
||||
|
||||
// Import the GCLI test helper
|
||||
|
@ -64,7 +63,6 @@ function navigateTo(url) {
|
|||
|
||||
function* cleanup()
|
||||
{
|
||||
gPanelWindow = null;
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
let target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
yield gDevTools.closeToolbox(target);
|
||||
|
@ -87,56 +85,6 @@ let openStyleEditorForURL = Task.async(function* (url, win) {
|
|||
return { tab, toolbox, panel, ui };
|
||||
});
|
||||
|
||||
function addTabAndOpenStyleEditors(count, callback, uri) {
|
||||
let deferred = promise.defer();
|
||||
let currentCount = 0;
|
||||
let panel;
|
||||
addTabAndCheckOnStyleEditorAdded(p => panel = p, function (editor) {
|
||||
currentCount++;
|
||||
info(currentCount + " of " + count + " editors opened: "
|
||||
+ editor.styleSheet.href);
|
||||
if (currentCount == count) {
|
||||
if (callback) {
|
||||
callback(panel);
|
||||
}
|
||||
deferred.resolve(panel);
|
||||
}
|
||||
});
|
||||
|
||||
if (uri) {
|
||||
content.location = uri;
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function addTabAndCheckOnStyleEditorAdded(callbackOnce, callbackOnAdded) {
|
||||
gBrowser.selectedTab = gBrowser.addTab();
|
||||
gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
|
||||
gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
|
||||
openStyleEditorInWindow(window, function (panel) {
|
||||
// Execute the individual callback with the panel argument.
|
||||
callbackOnce(panel);
|
||||
// Report editors that already opened while loading.
|
||||
for (let editor of panel.UI.editors) {
|
||||
callbackOnAdded(editor);
|
||||
}
|
||||
// Report new editors added afterwards.
|
||||
panel.UI.on("editor-added", (event, editor) => callbackOnAdded(editor));
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
function openStyleEditorInWindow(win, callback) {
|
||||
let target = TargetFactory.forTab(win.gBrowser.selectedTab);
|
||||
win.gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
|
||||
let panel = toolbox.getCurrentPanel();
|
||||
gPanelWindow = panel._panelWin;
|
||||
|
||||
panel.UI._alwaysDisableAnimations = true;
|
||||
callback(panel);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads shared/frame-script-utils.js in the specified tab.
|
||||
*
|
||||
|
|
|
@ -42,6 +42,10 @@ let testData = [
|
|||
["VK_BACK_SPACE", "di", -1, 0],
|
||||
["VK_BACK_SPACE", "d", -1, 0],
|
||||
["VK_BACK_SPACE", "", -1, 0],
|
||||
["VK_HOME", "", -1, 0],
|
||||
["VK_END", "", -1, 0],
|
||||
["VK_PAGE_UP", "", -1, 0],
|
||||
["VK_PAGE_DOWN", "", -1, 0],
|
||||
["f", "fill", 0, MAX_ENTRIES],
|
||||
["i", "fill", 0, 4],
|
||||
["VK_LEFT", "fill", -1, 0],
|
||||
|
@ -76,8 +80,8 @@ function* testCompletion([key, completion, index, total], editor, view) {
|
|||
|
||||
let onSuggest;
|
||||
|
||||
if (/(left|right|back_space|escape)/ig.test(key)) {
|
||||
info("Adding event listener for left|right|back_space|escape keys");
|
||||
if (/(left|right|back_space|escape|home|end|page_up|page_down)/ig.test(key)) {
|
||||
info("Adding event listener for left|right|back_space|escape|home|end|page_up|page_down keys");
|
||||
onSuggest = once(editor.input, "keypress");
|
||||
} else {
|
||||
info("Waiting for after-suggest event on the editor");
|
||||
|
|
|
@ -218,6 +218,7 @@
|
|||
@RESPATH@/components/dom_offline.xpt
|
||||
@RESPATH@/components/dom_json.xpt
|
||||
@RESPATH@/components/dom_power.xpt
|
||||
@RESPATH@/components/dom_push.xpt
|
||||
@RESPATH@/components/dom_quota.xpt
|
||||
@RESPATH@/components/dom_range.xpt
|
||||
@RESPATH@/components/dom_security.xpt
|
||||
|
@ -558,7 +559,7 @@
|
|||
@RESPATH@/components/AlarmsManager.manifest
|
||||
@RESPATH@/components/Push.js
|
||||
@RESPATH@/components/Push.manifest
|
||||
@RESPATH@/components/PushServiceLauncher.js
|
||||
@RESPATH@/components/PushNotificationService.js
|
||||
|
||||
@RESPATH@/components/SlowScriptDebug.manifest
|
||||
@RESPATH@/components/SlowScriptDebug.js
|
||||
|
|
|
@ -398,6 +398,7 @@ safebrowsing.notAForgeryButton.accessKey=F
|
|||
safebrowsing.reportedAttackSite=Reported Attack Site!
|
||||
safebrowsing.notAnAttackButton.label=This isn't an attack site…
|
||||
safebrowsing.notAnAttackButton.accessKey=A
|
||||
safebrowsing.reportedUnwantedSite=Reported Unwanted Software Site!
|
||||
|
||||
# Ctrl-Tab
|
||||
# LOCALIZATION NOTE (ctrlTab.listAllTabs.label): #1 represents the number
|
||||
|
|
|
@ -12,6 +12,11 @@
|
|||
<!ENTITY safeb.blocked.malwarePage.shortDesc "This web page at <span id='malware_sitename'/> has been reported as an attack page and has been blocked based on your security preferences.">
|
||||
<!ENTITY safeb.blocked.malwarePage.longDesc "<p>Attack pages try to install programs that steal private information, use your computer to attack others, or damage your system.</p><p>Some attack pages intentionally distribute harmful software, but many are compromised without the knowledge or permission of their owners.</p>">
|
||||
|
||||
<!ENTITY safeb.blocked.unwantedPage.title "Reported Unwanted Software Page!">
|
||||
<!-- Localization note (safeb.blocked.malware.shortDesc) - Please don't translate the contents of the <span id="unwanted_sitename"/> tag. It will be replaced at runtime with a domain name (e.g. www.badsite.com) -->
|
||||
<!ENTITY safeb.blocked.unwantedPage.shortDesc "This web page at <span id='unwanted_sitename'/> has been reported to contain unwanted software and has been blocked based on your security preferences.">
|
||||
<!ENTITY safeb.blocked.unwantedPage.longDesc "<p>Unwanted software pages try to install software that can be deceptive and affect your system in unexpected ways.</p>">
|
||||
|
||||
<!ENTITY safeb.blocked.phishingPage.title "Reported Web Forgery!">
|
||||
<!-- Localization note (safeb.blocked.phishing.shortDesc) - Please don't translate the contents of the <span id="phishing_sitename"/> tag. It will be replaced at runtime with a domain name (e.g. www.badsite.com) -->
|
||||
<!ENTITY safeb.blocked.phishingPage.shortDesc "This web page at <span id='phishing_sitename'/> has been reported as a web forgery and has been blocked based on your security preferences.">
|
||||
|
|
|
@ -30,6 +30,7 @@ externalProtocolUnknown=<Unknown>
|
|||
externalProtocolChkMsg=Remember my choice for all links of this type.
|
||||
externalProtocolLaunchBtn=Launch application
|
||||
malwareBlocked=The site at %S has been reported as an attack site and has been blocked based on your security preferences.
|
||||
unwantedBlocked=The site at %S has been reported as serving unwanted software and has been blocked based on your security preferences.
|
||||
phishingBlocked=The website at %S has been reported as a web forgery designed to trick users into sharing personal or financial information.
|
||||
cspBlocked=This page has a content security policy that prevents it from being loaded in this way.
|
||||
corruptedContentError=The page you are trying to view cannot be shown because an error in the data transmission was detected.
|
||||
|
|
|
@ -164,6 +164,11 @@ be temporary, and you can try again later.</li>
|
|||
<p>Website owners who believe their site has been reported as an attack site in error may <a href='http://www.stopbadware.org/home/reviewinfo' >request a review</a>.</p>
|
||||
">
|
||||
|
||||
<!ENTITY unwantedBlocked.title "Suspected Unwanted Software Site!">
|
||||
<!ENTITY unwantedBlocked.longDesc "
|
||||
<p>Unwanted software pages try to install software that can be deceptive and affect your system in unexpected ways.</p>
|
||||
">
|
||||
|
||||
<!ENTITY phishingBlocked.title "Suspected Web Forgery!">
|
||||
<!ENTITY phishingBlocked.longDesc "
|
||||
<p>Entering any personal information on this page may result in identity theft or other fraud.</p>
|
||||
|
|
|
@ -341,20 +341,15 @@ let Content = {
|
|||
}
|
||||
} else if (/^about:blocked/.test(errorDoc.documentURI)) {
|
||||
// The event came from a button on a malware/phishing block page
|
||||
// First check whether it's malware or phishing, so that we can
|
||||
// use the right strings/links.
|
||||
let isMalware = /e=malwareBlocked/.test(errorDoc.documentURI);
|
||||
|
||||
if (ot == errorDoc.getElementById("getMeOutButton")) {
|
||||
sendAsyncMessage("Browser:BlockedSite",
|
||||
{ url: errorDoc.location.href, action: "leave" });
|
||||
} else if (ot == errorDoc.getElementById("reportButton")) {
|
||||
// This is the "Why is this site blocked" button. For malware,
|
||||
// we can fetch a site-specific report, for phishing, we redirect
|
||||
// to the generic page describing phishing protection.
|
||||
let action = isMalware ? "report-malware" : "report-phishing";
|
||||
// This is the "Why is this site blocked" button. We redirect
|
||||
// to the generic page describing phishing/malware protection.
|
||||
sendAsyncMessage("Browser:BlockedSite",
|
||||
{ url: errorDoc.location.href, action: action });
|
||||
{ url: errorDoc.location.href, action: "report-phishing" });
|
||||
} else if (ot == errorDoc.getElementById("ignoreWarningButton")) {
|
||||
// Allow users to override and continue through to the site,
|
||||
// but add a notify bar as a reminder, so that they don't lose
|
||||
|
|
|
@ -240,6 +240,10 @@ let SelfSupportBackendInternal = {
|
|||
// Fetch the Self Support URL from the preferences.
|
||||
let unformattedURL = Preferences.get(PREF_URL, null);
|
||||
let url = Services.urlFormatter.formatURL(unformattedURL);
|
||||
if (!url.startsWith("https:")) {
|
||||
this._log.error("_loadSelfSupport - Non HTTPS URL provided: " + url);
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.config("_loadSelfSupport - URL " + url);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ const PREF_UITOUR_ENABLED = "browser.uitour.enabled";
|
|||
const TEST_WAIT_RETRIES = 60;
|
||||
|
||||
const TEST_PAGE_URL = getRootDirectory(gTestPath) + "uitour.html";
|
||||
const TEST_PAGE_URL_HTTPS = TEST_PAGE_URL.replace("chrome://mochitests/content/", "https://example.com/");
|
||||
|
||||
/**
|
||||
* Find a browser, with an IFRAME as parent, who has aURL as the source attribute.
|
||||
|
@ -104,9 +105,14 @@ add_task(function* setupEnvironment() {
|
|||
// is enabled.
|
||||
Preferences.set(PREF_SELFSUPPORT_ENABLED, true);
|
||||
Preferences.set(PREF_UITOUR_ENABLED, true);
|
||||
Preferences.set(PREF_SELFSUPPORT_URL, TEST_PAGE_URL);
|
||||
Preferences.set(PREF_SELFSUPPORT_URL, TEST_PAGE_URL_HTTPS);
|
||||
|
||||
// Whitelist the HTTPS page to use UITour.
|
||||
let pageURI = Services.io.newURI(TEST_PAGE_URL_HTTPS, null, null);
|
||||
Services.perms.add(pageURI, "uitour", Services.perms.ALLOW_ACTION);
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
Services.perms.remove("example.com", "uitour");
|
||||
Preferences.set(PREF_SELFSUPPORT_ENABLED, selfSupportEnabled);
|
||||
Preferences.set(PREF_UITOUR_ENABLED, uitourEnabled);
|
||||
Preferences.set(PREF_SELFSUPPORT_URL, selfSupportURL);
|
||||
|
@ -126,7 +132,7 @@ add_task(function* test_selfSupport() {
|
|||
|
||||
// Wait for the SelfSupport page to load.
|
||||
info("Waiting for the SelfSupport local page to load.");
|
||||
let selfSupportBrowser = yield promiseSelfSupportLoad(TEST_PAGE_URL);
|
||||
let selfSupportBrowser = yield promiseSelfSupportLoad(TEST_PAGE_URL_HTTPS);
|
||||
Assert.ok(!!selfSupportBrowser, "SelfSupport browser must exist.");
|
||||
|
||||
// Get a reference to the UITour API.
|
||||
|
@ -146,13 +152,34 @@ add_task(function* test_selfSupport() {
|
|||
|
||||
// Wait until SelfSupport closes.
|
||||
info("Waiting for the SelfSupport to close.");
|
||||
yield promiseSelfSupportClose(TEST_PAGE_URL);
|
||||
yield promiseSelfSupportClose(TEST_PAGE_URL_HTTPS);
|
||||
|
||||
// Find the SelfSupport browser, again. We don't expect to find it.
|
||||
selfSupportBrowser = findSelfSupportBrowser(TEST_PAGE_URL);
|
||||
selfSupportBrowser = findSelfSupportBrowser(TEST_PAGE_URL_HTTPS);
|
||||
Assert.ok(!selfSupportBrowser, "SelfSupport browser must not exist.");
|
||||
|
||||
// We shouldn't need this, but let's keep it to make sure closing SelfSupport twice
|
||||
// doesn't create any problem.
|
||||
SelfSupportBackend.uninit();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that SelfSupportBackend only allows HTTPS.
|
||||
*/
|
||||
add_task(function* test_selfSupport_noHTTPS() {
|
||||
Preferences.set(PREF_SELFSUPPORT_URL, TEST_PAGE_URL);
|
||||
|
||||
SelfSupportBackend.init();
|
||||
|
||||
// SelfSupportBackend waits for "sessionstore-windows-restored" to start loading. Send it.
|
||||
info("Sending sessionstore-windows-restored");
|
||||
Services.obs.notifyObservers(null, "sessionstore-windows-restored", null);
|
||||
|
||||
// Find the SelfSupport browser. We don't expect to find it since we are not using https.
|
||||
let selfSupportBrowser = findSelfSupportBrowser(TEST_PAGE_URL);
|
||||
Assert.ok(!selfSupportBrowser, "SelfSupport browser must not exist.");
|
||||
|
||||
// We shouldn't need this, but let's keep it to make sure closing SelfSupport twice
|
||||
// doesn't create any problem.
|
||||
SelfSupportBackend.uninit();
|
||||
})
|
||||
|
|
|
@ -9,10 +9,8 @@ scrollbar {
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Scrollbar code will reset the margin to the correct side depending on
|
||||
layout.scrollbar.side pref */
|
||||
scrollbar[orient="vertical"] {
|
||||
margin-left: -10px;
|
||||
-moz-margin-start: -10px;
|
||||
min-width: 10px;
|
||||
max-width: 10px;
|
||||
}
|
||||
|
|
|
@ -10,10 +10,8 @@ scrollbar {
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Scrollbar code will reset the margin to the correct side depending on
|
||||
layout.scrollbar.side pref */
|
||||
scrollbar[orient="vertical"] {
|
||||
margin-left: -8px;
|
||||
-moz-margin-start: -8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
%define menuStateHover :not(:-moz-any([disabled],:active))[_moz-menuactive]
|
||||
%define buttonStateActive :not([disabled]):-moz-any([open],:hover:active)
|
||||
%define menuStateActive :not([disabled])[_moz-menuactive]:active
|
||||
%define menuStateMenuActive :not([disabled])[_moz-menuactive]
|
||||
|
||||
%include ../browser.inc
|
||||
|
||||
|
|
|
@ -233,13 +233,13 @@ menu.subviewbutton > .menu-right:-moz-locale-dir(rtl) {
|
|||
color: ButtonText;
|
||||
}
|
||||
|
||||
.subviewbutton@menuStateHover@,
|
||||
.subviewbutton@menuStateMenuActive@,
|
||||
menuitem.panel-subview-footer@menuStateHover@,
|
||||
.subviewbutton.panel-subview-footer@buttonStateHover@,
|
||||
.subviewbutton.panel-subview-footer@buttonStateActive@,
|
||||
.subviewbutton@menuStateHover@ > .menu-accel-container,
|
||||
.PanelUI-subView .subviewbutton[shortcut]@buttonStateHover@::after,
|
||||
#BMB_bookmarksPopup .panel-subview-footer@menuStateHover@ > .menu-text {
|
||||
#BMB_bookmarksPopup .panel-subview-footer@menuStateMenuActive@ > .menu-text {
|
||||
background-color: Highlight;
|
||||
color: highlighttext !important;
|
||||
}
|
||||
|
|
|
@ -9,10 +9,8 @@ scrollbar {
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Scrollbar code will reset the margin to the correct side depending on
|
||||
layout.scrollbar.side pref */
|
||||
scrollbar[orient="vertical"] {
|
||||
margin-left: -10px;
|
||||
-moz-margin-start: -10px;
|
||||
min-width: 10px;
|
||||
max-width: 10px;
|
||||
}
|
||||
|
|
|
@ -101,12 +101,15 @@ def processSingleLeakFile(leakLogFileName, processType, leakThreshold, ignoreMis
|
|||
"""Process a single leak log.
|
||||
"""
|
||||
|
||||
# Per-Inst Leaked Total Rem ...
|
||||
# 0 TOTAL 17 192 419115886 2 ...
|
||||
# 833 nsTimerImpl 60 120 24726 2 ...
|
||||
lineRe = re.compile(r"^\s*\d+\s+(?P<name>\S+)\s+"
|
||||
r"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
|
||||
r"-?\d+\s+(?P<numLeaked>-?\d+)")
|
||||
# | |Per-Inst Leaked| Total Rem|
|
||||
# 0 |TOTAL | 17 192| 419115886 2|
|
||||
# 833 |nsTimerImpl | 60 120| 24726 2|
|
||||
# 930 |Foo<Bar, Bar> | 32 8| 100 1|
|
||||
lineRe = re.compile(r"^\s*\d+ \|"
|
||||
r"(?P<name>[^|]+)\|"
|
||||
r"\s*(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s*\|"
|
||||
r"\s*-?\d+\s+(?P<numLeaked>-?\d+)")
|
||||
# The class name can contain spaces. We remove trailing whitespace later.
|
||||
|
||||
processString = "%s process:" % processType
|
||||
crashedOnPurpose = False
|
||||
|
@ -125,7 +128,7 @@ def processSingleLeakFile(leakLogFileName, processType, leakThreshold, ignoreMis
|
|||
# eg: the leak table header row
|
||||
log.info(line.rstrip())
|
||||
continue
|
||||
name = matches.group("name")
|
||||
name = matches.group("name").rstrip()
|
||||
size = int(matches.group("size"))
|
||||
bytesLeaked = int(matches.group("bytesLeaked"))
|
||||
numLeaked = int(matches.group("numLeaked"))
|
||||
|
|
|
@ -84,6 +84,11 @@ private:
|
|||
virtual void run(const MatchFinder::MatchResult &Result);
|
||||
};
|
||||
|
||||
class ExplicitOperatorBoolChecker : public MatchFinder::MatchCallback {
|
||||
public:
|
||||
virtual void run(const MatchFinder::MatchResult &Result);
|
||||
};
|
||||
|
||||
ScopeChecker stackClassChecker;
|
||||
ScopeChecker globalClassChecker;
|
||||
NonHeapClassChecker nonheapClassChecker;
|
||||
|
@ -92,16 +97,17 @@ private:
|
|||
NaNExprChecker nanExprChecker;
|
||||
NoAddRefReleaseOnReturnChecker noAddRefReleaseOnReturnChecker;
|
||||
RefCountedInsideLambdaChecker refCountedInsideLambdaChecker;
|
||||
ExplicitOperatorBoolChecker explicitOperatorBoolChecker;
|
||||
MatchFinder astMatcher;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
bool isInIgnoredNamespace(const Decl *decl) {
|
||||
std::string getDeclarationNamespace(const Decl *decl) {
|
||||
const DeclContext *DC = decl->getDeclContext()->getEnclosingNamespaceContext();
|
||||
const NamespaceDecl *ND = dyn_cast<NamespaceDecl>(DC);
|
||||
if (!ND) {
|
||||
return false;
|
||||
return "";
|
||||
}
|
||||
|
||||
while (const DeclContext *ParentDC = ND->getParent()) {
|
||||
|
@ -112,8 +118,15 @@ bool isInIgnoredNamespace(const Decl *decl) {
|
|||
}
|
||||
|
||||
const auto& name = ND->getName();
|
||||
return name;
|
||||
}
|
||||
|
||||
bool isInIgnoredNamespaceForImplicitCtor(const Decl *decl) {
|
||||
std::string name = getDeclarationNamespace(decl);
|
||||
if (name == "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// namespace std and icu are ignored for now
|
||||
return name == "std" || // standard C++ lib
|
||||
name == "__gnu_cxx" || // gnu C++ lib
|
||||
name == "boost" || // boost
|
||||
|
@ -129,7 +142,19 @@ bool isInIgnoredNamespace(const Decl *decl) {
|
|||
name == "testing"; // gtest
|
||||
}
|
||||
|
||||
bool isIgnoredPath(const Decl *decl) {
|
||||
bool isInIgnoredNamespaceForImplicitConversion(const Decl *decl) {
|
||||
std::string name = getDeclarationNamespace(decl);
|
||||
if (name == "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return name == "std" || // standard C++ lib
|
||||
name == "__gnu_cxx" || // gnu C++ lib
|
||||
name == "google_breakpad" || // breakpad
|
||||
name == "testing"; // gtest
|
||||
}
|
||||
|
||||
bool isIgnoredPathForImplicitCtor(const Decl *decl) {
|
||||
decl = decl->getCanonicalDecl();
|
||||
SourceLocation Loc = decl->getLocation();
|
||||
const SourceManager &SM = decl->getASTContext().getSourceManager();
|
||||
|
@ -150,9 +175,30 @@ bool isIgnoredPath(const Decl *decl) {
|
|||
return false;
|
||||
}
|
||||
|
||||
bool isInterestingDecl(const Decl *decl) {
|
||||
return !isInIgnoredNamespace(decl) &&
|
||||
!isIgnoredPath(decl);
|
||||
bool isIgnoredPathForImplicitConversion(const Decl *decl) {
|
||||
decl = decl->getCanonicalDecl();
|
||||
SourceLocation Loc = decl->getLocation();
|
||||
const SourceManager &SM = decl->getASTContext().getSourceManager();
|
||||
SmallString<1024> FileName = SM.getFilename(Loc);
|
||||
llvm::sys::fs::make_absolute(FileName);
|
||||
llvm::sys::path::reverse_iterator begin = llvm::sys::path::rbegin(FileName),
|
||||
end = llvm::sys::path::rend(FileName);
|
||||
for (; begin != end; ++begin) {
|
||||
if (begin->compare_lower(StringRef("graphite2")) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isInterestingDeclForImplicitCtor(const Decl *decl) {
|
||||
return !isInIgnoredNamespaceForImplicitCtor(decl) &&
|
||||
!isIgnoredPathForImplicitCtor(decl);
|
||||
}
|
||||
|
||||
bool isInterestingDeclForImplicitConversion(const Decl *decl) {
|
||||
return !isInIgnoredNamespaceForImplicitConversion(decl) &&
|
||||
!isIgnoredPathForImplicitConversion(decl);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -232,7 +278,7 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
if (!d->isAbstract() && isInterestingDecl(d)) {
|
||||
if (!d->isAbstract() && isInterestingDeclForImplicitCtor(d)) {
|
||||
for (CXXRecordDecl::ctor_iterator ctor = d->ctor_begin(),
|
||||
e = d->ctor_end(); ctor != e; ++ctor) {
|
||||
// Ignore non-converting ctors
|
||||
|
@ -416,6 +462,16 @@ bool isClassRefCounted(QualType T) {
|
|||
return clazz ? isClassRefCounted(clazz) : RegularClass;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
bool IsInSystemHeader(const ASTContext &AC, const T &D) {
|
||||
auto &SourceManager = AC.getSourceManager();
|
||||
auto ExpansionLoc = SourceManager.getExpansionLoc(D.getLocStart());
|
||||
if (ExpansionLoc.isInvalid()) {
|
||||
return false;
|
||||
}
|
||||
return SourceManager.isInSystemHeader(ExpansionLoc);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace clang {
|
||||
|
@ -515,12 +571,7 @@ AST_MATCHER(QualType, isFloat) {
|
|||
/// isExpansionInSystemHeader in newer clangs, but modified in order to work
|
||||
/// with old clangs that we use on infra.
|
||||
AST_MATCHER(BinaryOperator, isInSystemHeader) {
|
||||
auto &SourceManager = Finder->getASTContext().getSourceManager();
|
||||
auto ExpansionLoc = SourceManager.getExpansionLoc(Node.getLocStart());
|
||||
if (ExpansionLoc.isInvalid()) {
|
||||
return false;
|
||||
}
|
||||
return SourceManager.isInSystemHeader(ExpansionLoc);
|
||||
return IsInSystemHeader(Finder->getASTContext(), Node);
|
||||
}
|
||||
|
||||
/// This matcher will match locations in SkScalar.h. This header contains a
|
||||
|
@ -649,6 +700,13 @@ DiagnosticsMatcher::DiagnosticsMatcher()
|
|||
hasDescendant(declRefExpr(hasType(pointerType(pointee(isRefCounted())))).bind("node"))
|
||||
),
|
||||
&refCountedInsideLambdaChecker);
|
||||
|
||||
// Older clang versions such as the ones used on the infra recognize these
|
||||
// conversions as 'operator _Bool', but newer clang versions recognize these
|
||||
// as 'operator bool'.
|
||||
astMatcher.addMatcher(methodDecl(anyOf(hasName("operator bool"),
|
||||
hasName("operator _Bool"))).bind("node"),
|
||||
&explicitOperatorBoolChecker);
|
||||
}
|
||||
|
||||
void DiagnosticsMatcher::ScopeChecker::run(
|
||||
|
@ -861,6 +919,25 @@ void DiagnosticsMatcher::RefCountedInsideLambdaChecker::run(
|
|||
Diag.Report(node->getLocStart(), noteID);
|
||||
}
|
||||
|
||||
void DiagnosticsMatcher::ExplicitOperatorBoolChecker::run(
|
||||
const MatchFinder::MatchResult &Result) {
|
||||
DiagnosticsEngine &Diag = Result.Context->getDiagnostics();
|
||||
unsigned errorID = Diag.getDiagnosticIDs()->getCustomDiagID(
|
||||
DiagnosticIDs::Error, "bad implicit conversion operator for %0");
|
||||
unsigned noteID = Diag.getDiagnosticIDs()->getCustomDiagID(
|
||||
DiagnosticIDs::Note, "consider adding the explicit keyword to %0");
|
||||
const CXXConversionDecl *method = Result.Nodes.getNodeAs<CXXConversionDecl>("node");
|
||||
const CXXRecordDecl *clazz = method->getParent();
|
||||
|
||||
if (!method->isExplicitSpecified() &&
|
||||
!MozChecker::hasCustomAnnotation(method, "moz_implicit") &&
|
||||
!IsInSystemHeader(method->getASTContext(), *method) &&
|
||||
isInterestingDeclForImplicitConversion(method)) {
|
||||
Diag.Report(method->getLocStart(), errorID) << clazz;
|
||||
Diag.Report(method->getLocStart(), noteID) << "'operator bool'";
|
||||
}
|
||||
}
|
||||
|
||||
class MozCheckAction : public PluginASTAction {
|
||||
public:
|
||||
ASTConsumerPtr CreateASTConsumer(CompilerInstance &CI, StringRef fileName) override {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
#define MOZ_IMPLICIT __attribute__((annotate("moz_implicit")))
|
||||
|
||||
struct Bad {
|
||||
operator bool(); // expected-error {{bad implicit conversion operator for 'Bad'}} expected-note {{consider adding the explicit keyword to 'operator bool'}}
|
||||
};
|
||||
struct Good {
|
||||
explicit operator bool();
|
||||
};
|
||||
struct Okay {
|
||||
MOZ_IMPLICIT operator bool();
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
SOURCES += [
|
||||
'TestBadImplicitConversionCtor.cpp',
|
||||
'TestCustomHeap.cpp',
|
||||
'TestExplicitOperatorBool.cpp',
|
||||
'TestGlobalClass.cpp',
|
||||
'TestMustOverride.cpp',
|
||||
'TestNANTestingExpr.cpp',
|
||||
|
|
|
@ -180,7 +180,13 @@ class RemoteAutomation(Automation):
|
|||
self.checkForANRs()
|
||||
self.checkForTombstones()
|
||||
|
||||
logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
|
||||
try:
|
||||
logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
|
||||
except DMError:
|
||||
print "getLogcat threw DMError; re-trying just once..."
|
||||
time.sleep(1)
|
||||
logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
|
||||
|
||||
javaException = mozcrash.check_for_java_exception(logcat)
|
||||
if javaException:
|
||||
return True
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
# It is referred to by the following page, so if this file moves, that page must
|
||||
# be modified accordingly:
|
||||
#
|
||||
# http://developer.mozilla.org/en/docs/Mochitest#How_do_I_test_issues_which_only_show_up_when_tests_are_run_across_domains.3F
|
||||
# https://developer.mozilla.org/en/docs/Mochitest#How_do_I_test_issues_which_only_show_up_when_tests_are_run_across_domains.3F
|
||||
#
|
||||
# Empty lines and lines which begin with "#" are ignored and may be used for
|
||||
# storing comments. All other lines consist of an origin followed by whitespace
|
||||
|
|
|
@ -13,10 +13,6 @@
|
|||
|
||||
namespace mozilla {
|
||||
|
||||
namespace dom {
|
||||
class nsIContentParent;
|
||||
};
|
||||
|
||||
namespace ipc {
|
||||
class URIParams;
|
||||
};
|
||||
|
|
|
@ -9,8 +9,6 @@
|
|||
#include "jsapi.h"
|
||||
#include "nsIPrincipal.h"
|
||||
|
||||
class nsCString;
|
||||
|
||||
struct nsJSPrincipals : nsIPrincipal, JSPrincipals
|
||||
{
|
||||
static bool Subsume(JSPrincipals *jsprin, JSPrincipals *other);
|
||||
|
|
|
@ -15,9 +15,6 @@
|
|||
#include "nsNetUtil.h"
|
||||
#include "nsScriptSecurityManager.h"
|
||||
|
||||
class nsIObjectInputStream;
|
||||
class nsIObjectOutputStream;
|
||||
|
||||
class nsBasePrincipal : public nsJSPrincipals
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -17,13 +17,10 @@
|
|||
|
||||
#include <stdint.h>
|
||||
|
||||
class nsIDocShell;
|
||||
class nsCString;
|
||||
class nsIClassInfo;
|
||||
class nsIIOService;
|
||||
class nsIStringBundle;
|
||||
class nsSystemPrincipal;
|
||||
class ClassInfoData;
|
||||
|
||||
/////////////////////////////
|
||||
// nsScriptSecurityManager //
|
||||
|
|
|
@ -3017,6 +3017,7 @@ then
|
|||
esac
|
||||
LDFLAGS="${_PTHREAD_LDFLAGS} ${LDFLAGS}"
|
||||
AC_SUBST(MOZ_USE_PTHREADS)
|
||||
MOZ_CHECK_HEADERS(pthread.h)
|
||||
fi
|
||||
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
#include "nsISupports.h"
|
||||
|
||||
class nsIURI;
|
||||
class nsString;
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
#include "nsIInterfaceRequestor.h"
|
||||
#include "nsILoadContext.h"
|
||||
|
||||
class mozIApplication;
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
/**
|
||||
|
|
|
@ -5087,7 +5087,8 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
|
|||
}
|
||||
}
|
||||
} else if (NS_ERROR_PHISHING_URI == aError ||
|
||||
NS_ERROR_MALWARE_URI == aError) {
|
||||
NS_ERROR_MALWARE_URI == aError ||
|
||||
NS_ERROR_UNWANTED_URI == aError) {
|
||||
nsAutoCString host;
|
||||
aURI->GetHost(host);
|
||||
CopyUTF8toUTF16(host, formatStrs[0]);
|
||||
|
@ -5106,14 +5107,19 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI,
|
|||
error.AssignLiteral("phishingBlocked");
|
||||
bucketId = IsFrame() ? nsISecurityUITelemetry::WARNING_PHISHING_PAGE_FRAME
|
||||
: nsISecurityUITelemetry::WARNING_PHISHING_PAGE_TOP;
|
||||
} else {
|
||||
} else if (NS_ERROR_MALWARE_URI == aError) {
|
||||
error.AssignLiteral("malwareBlocked");
|
||||
bucketId = IsFrame() ? nsISecurityUITelemetry::WARNING_MALWARE_PAGE_FRAME
|
||||
: nsISecurityUITelemetry::WARNING_MALWARE_PAGE_TOP;
|
||||
} else {
|
||||
error.AssignLiteral("unwantedBlocked");
|
||||
bucketId = IsFrame() ? nsISecurityUITelemetry::WARNING_UNWANTED_PAGE_FRAME
|
||||
: nsISecurityUITelemetry::WARNING_UNWANTED_PAGE_TOP;
|
||||
}
|
||||
|
||||
if (errorPage.EqualsIgnoreCase("blocked"))
|
||||
if (errorPage.EqualsIgnoreCase("blocked")) {
|
||||
Telemetry::Accumulate(Telemetry::SECURITY_UI, bucketId);
|
||||
}
|
||||
|
||||
cssClass.AssignLiteral("blacklist");
|
||||
} else if (NS_ERROR_CONTENT_CRASHED == aError) {
|
||||
|
@ -7824,6 +7830,7 @@ nsDocShell::EndPageLoad(nsIWebProgress* aProgress,
|
|||
aStatus == NS_ERROR_OFFLINE ||
|
||||
aStatus == NS_ERROR_MALWARE_URI ||
|
||||
aStatus == NS_ERROR_PHISHING_URI ||
|
||||
aStatus == NS_ERROR_UNWANTED_URI ||
|
||||
aStatus == NS_ERROR_UNSAFE_CONTENT_TYPE ||
|
||||
aStatus == NS_ERROR_REMOTE_XUL ||
|
||||
aStatus == NS_ERROR_OFFLINE ||
|
||||
|
|
|
@ -88,7 +88,6 @@ class nsIURIFixup;
|
|||
class nsIURILoader;
|
||||
class nsIWebBrowserFind;
|
||||
class nsIWidget;
|
||||
class ProfilerMarkerTracing;
|
||||
|
||||
/* load commands were moved to nsIDocShell.h */
|
||||
/* load types were moved to nsDocShellLoadTypes.h */
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
[scriptable, uuid(AF13EA3A-D488-4308-B843-526E055AB943)]
|
||||
interface nsIDOMNode;
|
||||
|
||||
[scriptable, uuid(35BE2D7E-F29B-48EC-BF7E-80A30A724DE3)]
|
||||
interface nsIContentViewerEdit : nsISupports
|
||||
{
|
||||
void clearSelection();
|
||||
|
@ -27,4 +29,8 @@ interface nsIContentViewerEdit : nsISupports
|
|||
|
||||
AString getContents(in string aMimeType, in boolean aSelectionOnly);
|
||||
readonly attribute boolean canGetContents;
|
||||
|
||||
// Set the node that will be the subject of the editing commands above.
|
||||
// Usually this will be the node that was context-clicked.
|
||||
void setCommandNode(in nsIDOMNode aNode);
|
||||
};
|
||||
|
|
|
@ -13,7 +13,6 @@ class nsIContent;
|
|||
class nsIDocShell;
|
||||
class nsIInputStream;
|
||||
class nsIRequest;
|
||||
class nsString;
|
||||
|
||||
// Interface ID for nsILinkHandler
|
||||
#define NS_ILINKHANDLER_IID \
|
||||
|
|
|
@ -291,6 +291,7 @@
|
|||
<h1 id="et_nssFailure2">&nssFailure2.title;</h1>
|
||||
<h1 id="et_nssBadCert">&nssBadCert.title;</h1>
|
||||
<h1 id="et_malwareBlocked">&malwareBlocked.title;</h1>
|
||||
<h1 id="et_unwantedBlocked">&unwantedBlocked.title;</h1>
|
||||
<h1 id="et_cspBlocked">&cspBlocked.title;</h1>
|
||||
<h1 id="et_remoteXUL">&remoteXUL.title;</h1>
|
||||
<h1 id="et_corruptedContentError">&corruptedContentError.title;</h1>
|
||||
|
@ -317,6 +318,7 @@
|
|||
<div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
|
||||
<div id="ed_nssBadCert">&nssBadCert.longDesc2;</div>
|
||||
<div id="ed_malwareBlocked">&malwareBlocked.longDesc;</div>
|
||||
<div id="ed_unwantedBlocked">&unwantedBlocked.longDesc;</div>
|
||||
<div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
|
||||
<div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
|
||||
<div id="ed_corruptedContentError">&corruptedContentError.longDesc;</div>
|
||||
|
|
|
@ -217,6 +217,32 @@ Animation::GetFinished(ErrorResult& aRv)
|
|||
return mFinished;
|
||||
}
|
||||
|
||||
void
|
||||
Animation::Finish(ErrorResult& aRv)
|
||||
{
|
||||
// https://w3c.github.io/web-animations/#finish-an-animation
|
||||
|
||||
if (mPlaybackRate == 0 ||
|
||||
(mPlaybackRate > 0 && EffectEnd() == TimeDuration::Forever())) {
|
||||
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
TimeDuration limit =
|
||||
mPlaybackRate > 0 ? TimeDuration(EffectEnd()) : TimeDuration(0);
|
||||
|
||||
SetCurrentTime(limit);
|
||||
|
||||
if (mPendingState == PendingState::PlayPending) {
|
||||
CancelPendingTasks();
|
||||
if (mReady) {
|
||||
mReady->MaybeResolve(this);
|
||||
}
|
||||
}
|
||||
UpdateFinishedState(true);
|
||||
PostUpdate();
|
||||
}
|
||||
|
||||
void
|
||||
Animation::Play(LimitBehavior aLimitBehavior)
|
||||
{
|
||||
|
|
|
@ -94,6 +94,7 @@ public:
|
|||
AnimationPlayState PlayState() const;
|
||||
virtual Promise* GetReady(ErrorResult& aRv);
|
||||
virtual Promise* GetFinished(ErrorResult& aRv);
|
||||
virtual void Finish(ErrorResult& aRv);
|
||||
virtual void Play(LimitBehavior aLimitBehavior);
|
||||
virtual void Pause();
|
||||
bool IsRunningOnCompositor() const { return mIsRunningOnCompositor; }
|
||||
|
|
|
@ -83,112 +83,6 @@ const TEN_PCT_POSITION = 110;
|
|||
const FIFTY_PCT_POSITION = 150;
|
||||
const END_POSITION = 200;
|
||||
|
||||
/**
|
||||
* CSS animation events fire asynchronously after we set 'startTime'. This
|
||||
* helper class allows us to handle such events using Promises.
|
||||
*
|
||||
* To use this class:
|
||||
*
|
||||
* var eventWatcher = new EventWatcher(watchedNode, eventTypes);
|
||||
* eventWatcher.waitForEvent(eventType).then(function() {
|
||||
* // Promise fulfilled
|
||||
* checkStuff();
|
||||
* makeSomeChanges();
|
||||
* return eventWatcher.waitForEvent(nextEventType);
|
||||
* }).then(function() {
|
||||
* // Promise fulfilled
|
||||
* checkMoreStuff();
|
||||
* eventWatcher.stopWatching(); // all done - stop listening for events
|
||||
* });
|
||||
*
|
||||
* This class will assert_unreached() if an event occurs when there is no
|
||||
* Promise created by a waitForEvent() call waiting to be fulfilled, or if the
|
||||
* event is of a different type to the type passed to waitForEvent. This helps
|
||||
* provide test coverage to ensure that only events that are expected occur, in
|
||||
* the correct order and with the correct timing. It also helps vastly simplify
|
||||
* the already complex code below by avoiding lots of gnarly error handling
|
||||
* code.
|
||||
*/
|
||||
function EventWatcher(watchedNode, eventTypes)
|
||||
{
|
||||
if (typeof eventTypes == 'string') {
|
||||
eventTypes = [eventTypes];
|
||||
}
|
||||
|
||||
var waitingFor = null;
|
||||
|
||||
function eventHandler(evt) {
|
||||
if (!waitingFor) {
|
||||
assert_unreached('Not expecting event, but got: ' + evt.type +
|
||||
' targeting element #' + evt.target.getAttribute('id'));
|
||||
return;
|
||||
}
|
||||
if (evt.type != waitingFor.types[0]) {
|
||||
assert_unreached('Expected ' + waitingFor.types[0] + ' event but got ' +
|
||||
evt.type + ' event');
|
||||
return;
|
||||
}
|
||||
if (waitingFor.types.length > 1) {
|
||||
// Pop first event from array
|
||||
waitingFor.types.shift();
|
||||
return;
|
||||
}
|
||||
// We need to null out waitingFor before calling the resolve function since
|
||||
// the Promise's resolve handlers may call waitForEvent() which will need
|
||||
// to set waitingFor.
|
||||
var resolveFunc = waitingFor.resolve;
|
||||
waitingFor = null;
|
||||
resolveFunc(evt);
|
||||
}
|
||||
|
||||
for (var i = 0; i < eventTypes.length; i++) {
|
||||
watchedNode.addEventListener(eventTypes[i], eventHandler);
|
||||
}
|
||||
|
||||
this.waitForEvent = function(type) {
|
||||
if (typeof type != 'string') {
|
||||
return Promise.reject('Event type not a string');
|
||||
}
|
||||
return this.waitForEvents([type]);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is useful when two events are expected to fire one immediately after
|
||||
* the other. This happens when we skip over the entire active interval for
|
||||
* instance. In this case an 'animationstart' and an 'animationend' are fired
|
||||
* and due to the asynchronous nature of Promise callbacks this won't work:
|
||||
*
|
||||
* eventWatcher.waitForEvent('animationstart').then(function() {
|
||||
* return waitForEvent('animationend');
|
||||
* }).then(...);
|
||||
*
|
||||
* It doesn't work because the 'animationend' listener is added too late,
|
||||
* because the resolve handler for the first Promise is called asynchronously
|
||||
* some time after the 'animationstart' event is called, rather than at the
|
||||
* time the event reaches the watched element.
|
||||
*/
|
||||
this.waitForEvents = function(types) {
|
||||
if (waitingFor) {
|
||||
return Promise.reject('Already waiting for an event');
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
waitingFor = {
|
||||
types: types,
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
this.stopWatching = function() {
|
||||
for (var i = 0; i < eventTypes.length; i++) {
|
||||
watchedNode.removeEventListener(eventTypes[i], eventHandler);
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// The terms used for the naming of the following helper functions refer to
|
||||
// terms used in the Web Animations specification for specific phases of an
|
||||
// animation. The terms can be found here:
|
||||
|
@ -336,7 +230,7 @@ test(function(t)
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
|
||||
|
@ -347,7 +241,7 @@ async_test(function(t) {
|
|||
|
||||
animation.currentTime =
|
||||
currentTimeForStartOfActiveInterval(animation.timeline);
|
||||
return eventWatcher.waitForEvent('animationstart');
|
||||
return eventWatcher.wait_for('animationstart');
|
||||
})).then(t.step_func(function() {
|
||||
checkStateAtActiveIntervalStartTime(animation);
|
||||
|
||||
|
@ -357,11 +251,9 @@ async_test(function(t) {
|
|||
|
||||
animation.currentTime =
|
||||
currentTimeForEndOfActiveInterval(animation.timeline);
|
||||
return eventWatcher.waitForEvent('animationend');
|
||||
return eventWatcher.wait_for('animationend');
|
||||
})).then(t.step_func(function() {
|
||||
checkStateAtActiveIntervalEndTime(animation);
|
||||
|
||||
eventWatcher.stopWatching();
|
||||
})).catch(t.step_func(function(reason) {
|
||||
assert_unreached(reason);
|
||||
})).then(function() {
|
||||
|
@ -372,7 +264,7 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
|
||||
|
@ -389,8 +281,8 @@ async_test(function(t) {
|
|||
// an 'animationend' event. We need to wait for these events before we start
|
||||
// testing going backwards since EventWatcher will fail the test if it gets
|
||||
// an event that we haven't told it about.
|
||||
eventWatcher.waitForEvents(['animationstart',
|
||||
'animationend']).then(t.step_func(function() {
|
||||
eventWatcher.wait_for(['animationstart',
|
||||
'animationend']).then(t.step_func(function() {
|
||||
assert_true(document.timeline.currentTime - previousTimelineTime <
|
||||
ANIM_DUR_MS,
|
||||
'Sanity check that seeking worked rather than the events ' +
|
||||
|
@ -410,9 +302,9 @@ async_test(function(t) {
|
|||
//
|
||||
// Calling checkStateAtFiftyPctOfActiveInterval will check computed style,
|
||||
// causing computed style to be updated and the 'animationstart' event to
|
||||
// be dispatched synchronously. We need to call waitForEvent first
|
||||
// be dispatched synchronously. We need to call wait_for first
|
||||
// otherwise eventWatcher will assert that the event was unexpected.
|
||||
var promise = eventWatcher.waitForEvent('animationstart');
|
||||
var promise = eventWatcher.wait_for('animationstart');
|
||||
checkStateAtFiftyPctOfActiveInterval(animation);
|
||||
return promise;
|
||||
})).then(t.step_func(function() {
|
||||
|
@ -424,11 +316,9 @@ async_test(function(t) {
|
|||
// Despite going backwards from just after the active interval starts to
|
||||
// the animation start time, we now expect an animationend event
|
||||
// because we went from inside to outside the active interval.
|
||||
return eventWatcher.waitForEvent('animationend');
|
||||
return eventWatcher.wait_for('animationend');
|
||||
})).then(t.step_func(function() {
|
||||
checkStateOnReadyPromiseResolved(animation);
|
||||
|
||||
eventWatcher.stopWatching();
|
||||
})).catch(t.step_func(function(reason) {
|
||||
assert_unreached(reason);
|
||||
})).then(function() {
|
||||
|
@ -454,7 +344,7 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
|
@ -462,14 +352,13 @@ async_test(function(t) {
|
|||
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
}, 'Redundant change, before -> active, then back');
|
||||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
|
@ -477,23 +366,21 @@ async_test(function(t) {
|
|||
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
}, 'Redundant change, before -> after, then back');
|
||||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
eventWatcher.waitForEvent('animationstart').then(function() {
|
||||
eventWatcher.wait_for('animationstart').then(function() {
|
||||
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
|
||||
animation.currentTime = currentTimeForActivePhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
});
|
||||
|
@ -503,16 +390,15 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
eventWatcher.waitForEvent('animationstart').then(function() {
|
||||
eventWatcher.wait_for('animationstart').then(function() {
|
||||
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
|
||||
animation.currentTime = currentTimeForActivePhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
});
|
||||
|
@ -522,17 +408,16 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
eventWatcher.waitForEvents(['animationstart',
|
||||
'animationend']).then(function() {
|
||||
eventWatcher.wait_for(['animationstart',
|
||||
'animationend']).then(function() {
|
||||
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
|
||||
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
});
|
||||
|
@ -542,17 +427,16 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
eventWatcher.waitForEvents(['animationstart',
|
||||
'animationend']).then(function() {
|
||||
eventWatcher.wait_for(['animationstart',
|
||||
'animationend']).then(function() {
|
||||
animation.currentTime = currentTimeForActivePhase(animation.timeline);
|
||||
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="../testcommon.js"></script>
|
||||
<div id="log"></div>
|
||||
<style>
|
||||
|
||||
.animated-div {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@keyframes anim {
|
||||
from { margin-left: 100px; }
|
||||
to { margin-left: 200px; }
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
|
||||
'use strict';
|
||||
|
||||
const ANIM_PROP_VAL = 'anim 100s';
|
||||
const ANIM_DURATION = 100000; // ms
|
||||
|
||||
test(function(t) {
|
||||
var div = addDiv(t);
|
||||
div.style.animation = ANIM_PROP_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.playbackRate = 0;
|
||||
|
||||
var threw = false;
|
||||
try {
|
||||
animation.finish();
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
assert_equals(e.name, 'InvalidStateError',
|
||||
'Exception should be an InvalidStateError exception when ' +
|
||||
'trying to finish an animation with playbackRate == 0');
|
||||
}
|
||||
assert_true(threw,
|
||||
'Expect InvalidStateError exception trying to finish an ' +
|
||||
'animation with playbackRate == 0');
|
||||
}, 'Test exceptions when finishing non-running animation');
|
||||
|
||||
test(function(t) {
|
||||
var div = addDiv(t);
|
||||
div.style.animation = ANIM_PROP_VAL;
|
||||
div.style.animationIterationCount = 'infinite';
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
var threw = false;
|
||||
try {
|
||||
animation.finish();
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
assert_equals(e.name, 'InvalidStateError',
|
||||
'Exception should be an InvalidStateError exception when ' +
|
||||
'trying to finish an infinite animation');
|
||||
}
|
||||
assert_true(threw,
|
||||
'Expect InvalidStateError exception trying to finish an ' +
|
||||
'infinite animation');
|
||||
}, 'Test exceptions when finishing infinite animation');
|
||||
|
||||
test(function(t) {
|
||||
var div = addDiv(t);
|
||||
div.style.animation = ANIM_PROP_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.finish();
|
||||
assert_equals(animation.currentTime, ANIM_DURATION,
|
||||
'After finishing, the currentTime should be set to the end ' +
|
||||
'of the active duration');
|
||||
}, 'Test finishing of animation');
|
||||
|
||||
test(function(t) {
|
||||
var div = addDiv(t);
|
||||
div.style.animation = ANIM_PROP_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.currentTime = ANIM_DURATION + 1000; // 1s past effect end
|
||||
|
||||
animation.finish();
|
||||
assert_equals(animation.currentTime, ANIM_DURATION,
|
||||
'After finishing, the currentTime should be set back to the ' +
|
||||
'end of the active duration');
|
||||
}, 'Test finishing of animation with a current time past the effect end');
|
||||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t);
|
||||
div.style.animation = ANIM_PROP_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.currentTime = ANIM_DURATION;
|
||||
|
||||
animation.finished.then(t.step_func(function() {
|
||||
animation.playbackRate = -1;
|
||||
animation.finish();
|
||||
assert_equals(animation.currentTime, 0,
|
||||
'After finishing a reversed animation the currentTime ' +
|
||||
'should be set to zero');
|
||||
t.done();
|
||||
}));
|
||||
}, 'Test finishing of reversed animation');
|
||||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t);
|
||||
div.style.animation = ANIM_PROP_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.currentTime = ANIM_DURATION;
|
||||
|
||||
animation.finished.then(t.step_func(function() {
|
||||
animation.playbackRate = -1;
|
||||
|
||||
animation.currentTime = -1000;
|
||||
|
||||
animation.finish();
|
||||
assert_equals(animation.currentTime, 0,
|
||||
'After finishing a reversed animation the currentTime ' +
|
||||
'should be set back to zero');
|
||||
t.done();
|
||||
}));
|
||||
}, 'Test finishing of reversed animation with with a current time less ' +
|
||||
'than zero');
|
||||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t);
|
||||
div.style.animation = ANIM_PROP_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.pause();
|
||||
|
||||
animation.ready.then(t.step_func(function() {
|
||||
animation.finish();
|
||||
assert_equals(animation.playState, 'paused',
|
||||
'The play state of a paused animation should remain ' +
|
||||
'"paused" even after finish() is called');
|
||||
t.done();
|
||||
}));
|
||||
}, 'Test paused state after finishing of animation');
|
||||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
div.style.animation = ANIM_PROP_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.ready.then(t.step_func(function() {
|
||||
animation.finish();
|
||||
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
|
||||
assert_equals(marginLeft, 10,
|
||||
'The computed style should be reset when finish() is ' +
|
||||
'called');
|
||||
t.done();
|
||||
}));
|
||||
}, 'Test resetting of computed style');
|
||||
|
||||
</script>
|
|
@ -77,7 +77,7 @@ async_test(function(t) {
|
|||
|
||||
animation.currentTime = ANIM_DURATION;
|
||||
|
||||
animation.finished.then(function() {
|
||||
animation.finished.then(t.step_func(function() {
|
||||
previousFinishedPromise = animation.finished;
|
||||
animation.playbackRate = -1;
|
||||
assert_not_equals(animation.finished, previousFinishedPromise,
|
||||
|
@ -85,7 +85,7 @@ async_test(function(t) {
|
|||
'finished promise');
|
||||
animation.currentTime = 0;
|
||||
return animation.finished;
|
||||
}).then(t.step_func(function() {
|
||||
})).then(t.step_func(function() {
|
||||
previousFinishedPromise = animation.finished;
|
||||
animation.play();
|
||||
assert_not_equals(animation.finished, previousFinishedPromise,
|
||||
|
|
|
@ -44,13 +44,13 @@ async_test(function(t) {
|
|||
var animation = div.getAnimations()[0];
|
||||
|
||||
var originalReadyPromise = animation.ready;
|
||||
animation.ready.then(function() {
|
||||
animation.ready.then(t.step_func(function() {
|
||||
div.style.animationPlayState = 'running';
|
||||
assert_not_equals(animation.ready, originalReadyPromise,
|
||||
'After updating animation-play-state a new ready promise'
|
||||
+ ' object is created');
|
||||
t.done();
|
||||
});
|
||||
}));
|
||||
}, 'A new ready promise is created when setting animation-play-state: running');
|
||||
|
||||
async_test(function(t) {
|
||||
|
@ -58,14 +58,14 @@ async_test(function(t) {
|
|||
div.style.animation = 'abc 100s';
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.ready.then(function() {
|
||||
animation.ready.then(t.step_func(function() {
|
||||
var promiseBeforeCallingPlay = animation.ready;
|
||||
animation.play();
|
||||
assert_equals(animation.ready, promiseBeforeCallingPlay,
|
||||
'Ready promise has same object identity after redundant call'
|
||||
+ ' to play()');
|
||||
t.done();
|
||||
});
|
||||
}));
|
||||
}, 'Redundant calls to play() do not generate new ready promise objects');
|
||||
|
||||
async_test(function(t) {
|
||||
|
@ -73,12 +73,12 @@ async_test(function(t) {
|
|||
div.style.animation = 'abc 100s';
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
animation.ready.then(function(resolvedAnimation) {
|
||||
animation.ready.then(t.step_func(function(resolvedAnimation) {
|
||||
assert_equals(resolvedAnimation, animation,
|
||||
'Object identity of Animation passed to Promise callback'
|
||||
+ ' matches the Animation object owning the Promise');
|
||||
t.done();
|
||||
});
|
||||
}));
|
||||
}, 'The ready promise is fulfilled with its Animation');
|
||||
|
||||
async_test(function(t) {
|
||||
|
|
|
@ -83,112 +83,6 @@ const TEN_PCT_POSITION = 110;
|
|||
const FIFTY_PCT_POSITION = 150;
|
||||
const END_POSITION = 200;
|
||||
|
||||
/**
|
||||
* CSS animation events fire asynchronously after we set 'startTime'. This
|
||||
* helper class allows us to handle such events using Promises.
|
||||
*
|
||||
* To use this class:
|
||||
*
|
||||
* var eventWatcher = new EventWatcher(watchedNode, eventTypes);
|
||||
* eventWatcher.waitForEvent(eventType).then(function() {
|
||||
* // Promise fulfilled
|
||||
* checkStuff();
|
||||
* makeSomeChanges();
|
||||
* return eventWatcher.waitForEvent(nextEventType);
|
||||
* }).then(function() {
|
||||
* // Promise fulfilled
|
||||
* checkMoreStuff();
|
||||
* eventWatcher.stopWatching(); // all done - stop listening for events
|
||||
* });
|
||||
*
|
||||
* This class will assert_unreached() if an event occurs when there is no
|
||||
* Promise created by a waitForEvent() call waiting to be fulfilled, or if the
|
||||
* event is of a different type to the type passed to waitForEvent. This helps
|
||||
* provide test coverage to ensure that only events that are expected occur, in
|
||||
* the correct order and with the correct timing. It also helps vastly simplify
|
||||
* the already complex code below by avoiding lots of gnarly error handling
|
||||
* code.
|
||||
*/
|
||||
function EventWatcher(watchedNode, eventTypes)
|
||||
{
|
||||
if (typeof eventTypes == 'string') {
|
||||
eventTypes = [eventTypes];
|
||||
}
|
||||
|
||||
var waitingFor = null;
|
||||
|
||||
function eventHandler(evt) {
|
||||
if (!waitingFor) {
|
||||
assert_unreached('Not expecting event, but got: ' + evt.type +
|
||||
' targeting element #' + evt.target.getAttribute('id'));
|
||||
return;
|
||||
}
|
||||
if (evt.type != waitingFor.types[0]) {
|
||||
assert_unreached('Expected ' + waitingFor.types[0] + ' event but got ' +
|
||||
evt.type + ' event');
|
||||
return;
|
||||
}
|
||||
if (waitingFor.types.length > 1) {
|
||||
// Pop first event from array
|
||||
waitingFor.types.shift();
|
||||
return;
|
||||
}
|
||||
// We need to null out waitingFor before calling the resolve function since
|
||||
// the Promise's resolve handlers may call waitForEvent() which will need
|
||||
// to set waitingFor.
|
||||
var resolveFunc = waitingFor.resolve;
|
||||
waitingFor = null;
|
||||
resolveFunc(evt);
|
||||
}
|
||||
|
||||
for (var i = 0; i < eventTypes.length; i++) {
|
||||
watchedNode.addEventListener(eventTypes[i], eventHandler);
|
||||
}
|
||||
|
||||
this.waitForEvent = function(type) {
|
||||
if (typeof type != 'string') {
|
||||
return Promise.reject('Event type not a string');
|
||||
}
|
||||
return this.waitForEvents([type]);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is useful when two events are expected to fire one immediately after
|
||||
* the other. This happens when we skip over the entire active interval for
|
||||
* instance. In this case an 'animationstart' and an 'animationend' are fired
|
||||
* and due to the asynchronous nature of Promise callbacks this won't work:
|
||||
*
|
||||
* eventWatcher.waitForEvent('animationstart').then(function() {
|
||||
* return waitForEvent('animationend');
|
||||
* }).then(...);
|
||||
*
|
||||
* It doesn't work because the 'animationend' listener is added too late,
|
||||
* because the resolve handler for the first Promise is called asynchronously
|
||||
* some time after the 'animationstart' event is called, rather than at the
|
||||
* time the event reaches the watched element.
|
||||
*/
|
||||
this.waitForEvents = function(types) {
|
||||
if (waitingFor) {
|
||||
return Promise.reject('Already waiting for an event');
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
waitingFor = {
|
||||
types: types,
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
this.stopWatching = function() {
|
||||
for (var i = 0; i < eventTypes.length; i++) {
|
||||
watchedNode.removeEventListener(eventTypes[i], eventHandler);
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// The terms used for the naming of the following helper functions refer to
|
||||
// terms used in the Web Animations specification for specific phases of an
|
||||
// animation. The terms can be found here:
|
||||
|
@ -390,7 +284,7 @@ test(function(t)
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
|
||||
|
@ -400,7 +294,7 @@ async_test(function(t) {
|
|||
checkStateOnReadyPromiseResolved(animation);
|
||||
|
||||
animation.startTime = startTimeForStartOfActiveInterval(animation.timeline);
|
||||
return eventWatcher.waitForEvent('animationstart');
|
||||
return eventWatcher.wait_for('animationstart');
|
||||
})).then(t.step_func(function() {
|
||||
checkStateAtActiveIntervalStartTime(animation);
|
||||
|
||||
|
@ -409,11 +303,9 @@ async_test(function(t) {
|
|||
checkStateAtFiftyPctOfActiveInterval(animation);
|
||||
|
||||
animation.startTime = startTimeForEndOfActiveInterval(animation.timeline);
|
||||
return eventWatcher.waitForEvent('animationend');
|
||||
return eventWatcher.wait_for('animationend');
|
||||
})).then(t.step_func(function() {
|
||||
checkStateAtActiveIntervalEndTime(animation);
|
||||
|
||||
eventWatcher.stopWatching();
|
||||
})).catch(t.step_func(function(reason) {
|
||||
assert_unreached(reason);
|
||||
})).then(function() {
|
||||
|
@ -424,7 +316,7 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
|
||||
|
@ -438,8 +330,8 @@ async_test(function(t) {
|
|||
// an 'animationend' event. We need to wait for these events before we start
|
||||
// testing going backwards since EventWatcher will fail the test if it gets
|
||||
// an event that we haven't told it about.
|
||||
eventWatcher.waitForEvents(['animationstart',
|
||||
'animationend']).then(t.step_func(function() {
|
||||
eventWatcher.wait_for(['animationstart',
|
||||
'animationend']).then(t.step_func(function() {
|
||||
assert_true(document.timeline.currentTime - previousTimelineTime <
|
||||
ANIM_DUR_MS,
|
||||
'Sanity check that seeking worked rather than the events ' +
|
||||
|
@ -459,9 +351,9 @@ async_test(function(t) {
|
|||
//
|
||||
// Calling checkStateAtFiftyPctOfActiveInterval will check computed style,
|
||||
// causing computed style to be updated and the 'animationstart' event to
|
||||
// be dispatched synchronously. We need to call waitForEvent first
|
||||
// be dispatched synchronously. We need to call wait_for first
|
||||
// otherwise eventWatcher will assert that the event was unexpected.
|
||||
var promise = eventWatcher.waitForEvent('animationstart');
|
||||
var promise = eventWatcher.wait_for('animationstart');
|
||||
checkStateAtFiftyPctOfActiveInterval(animation);
|
||||
return promise;
|
||||
})).then(t.step_func(function() {
|
||||
|
@ -472,11 +364,9 @@ async_test(function(t) {
|
|||
// Despite going backwards from just after the active interval starts to
|
||||
// the animation start time, we now expect an animationend event
|
||||
// because we went from inside to outside the active interval.
|
||||
return eventWatcher.waitForEvent('animationend');
|
||||
return eventWatcher.wait_for('animationend');
|
||||
})).then(t.step_func(function() {
|
||||
checkStateOnReadyPromiseResolved(animation);
|
||||
|
||||
eventWatcher.stopWatching();
|
||||
})).catch(t.step_func(function(reason) {
|
||||
assert_unreached(reason);
|
||||
})).then(function() {
|
||||
|
@ -502,7 +392,7 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
|
@ -510,14 +400,13 @@ async_test(function(t) {
|
|||
animation.startTime = startTimeForBeforePhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
}, 'Redundant change, before -> active, then back');
|
||||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
|
@ -525,23 +414,21 @@ async_test(function(t) {
|
|||
animation.startTime = startTimeForBeforePhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
}, 'Redundant change, before -> after, then back');
|
||||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
eventWatcher.waitForEvent('animationstart').then(function() {
|
||||
eventWatcher.wait_for('animationstart').then(function() {
|
||||
animation.startTime = startTimeForBeforePhase(animation.timeline);
|
||||
animation.startTime = startTimeForActivePhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
});
|
||||
|
@ -551,16 +438,15 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
eventWatcher.waitForEvent('animationstart').then(function() {
|
||||
eventWatcher.wait_for('animationstart').then(function() {
|
||||
animation.startTime = startTimeForAfterPhase(animation.timeline);
|
||||
animation.startTime = startTimeForActivePhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
});
|
||||
|
@ -570,17 +456,16 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
eventWatcher.waitForEvents(['animationstart',
|
||||
'animationend']).then(function() {
|
||||
eventWatcher.wait_for(['animationstart',
|
||||
'animationend']).then(function() {
|
||||
animation.startTime = startTimeForBeforePhase(animation.timeline);
|
||||
animation.startTime = startTimeForAfterPhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
});
|
||||
|
@ -590,17 +475,16 @@ async_test(function(t) {
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
|
||||
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
|
||||
div.style.animation = ANIM_PROPERTY_VAL;
|
||||
var animation = div.getAnimations()[0];
|
||||
|
||||
eventWatcher.waitForEvents(['animationstart',
|
||||
'animationend']).then(function() {
|
||||
eventWatcher.wait_for(['animationstart',
|
||||
'animationend']).then(function() {
|
||||
animation.startTime = startTimeForActivePhase(animation.timeline);
|
||||
animation.startTime = startTimeForAfterPhase(animation.timeline);
|
||||
|
||||
waitForAnimationFrames(2).then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
t.done();
|
||||
});
|
||||
});
|
||||
|
@ -623,11 +507,11 @@ async_test(function(t) {
|
|||
return animation.ready;
|
||||
})).catch(t.step_func(function(reason) {
|
||||
assert_unreached(reason);
|
||||
})).then(function() {
|
||||
})).then(t.step_func(function() {
|
||||
assert_equals(animation.currentTime, storedCurrentTime,
|
||||
'Test that hold time is correct');
|
||||
t.done();
|
||||
});
|
||||
}));
|
||||
}, 'Setting startTime to null');
|
||||
|
||||
|
||||
|
|
|
@ -73,111 +73,6 @@ const TEN_PCT_POSITION = 110;
|
|||
const FIFTY_PCT_POSITION = 150;
|
||||
const END_POSITION = 200;
|
||||
|
||||
/**
|
||||
* CSS animation events fire asynchronously after we set 'startTime'. This
|
||||
* helper class allows us to handle such events using Promises.
|
||||
*
|
||||
* To use this class:
|
||||
*
|
||||
* var eventWatcher = new EventWatcher(watchedNode, eventTypes);
|
||||
* eventWatcher.waitForEvent(eventType).then(function() {
|
||||
* // Promise fulfilled
|
||||
* checkStuff();
|
||||
* makeSomeChanges();
|
||||
* return eventWatcher.waitForEvent(nextEventType);
|
||||
* }).then(function() {
|
||||
* // Promise fulfilled
|
||||
* checkMoreStuff();
|
||||
* eventWatcher.stopWatching(); // all done - stop listening for events
|
||||
* });
|
||||
*
|
||||
* This class will assert_unreached() if an event occurs when there is no
|
||||
* Promise created by a waitForEvent() call waiting to be fulfilled, or if the
|
||||
* event is of a different type to the type passed to waitForEvent. This helps
|
||||
* provide test coverage to ensure that only events that are expected occur, in
|
||||
* the correct order and with the correct timing. It also helps vastly simplify
|
||||
* the already complex code below by avoiding lots of gnarly error handling
|
||||
* code.
|
||||
*/
|
||||
function EventWatcher(watchedNode, eventTypes)
|
||||
{
|
||||
if (typeof eventTypes == 'string') {
|
||||
eventTypes = [eventTypes];
|
||||
}
|
||||
|
||||
var waitingFor = null;
|
||||
|
||||
function eventHandler(evt) {
|
||||
if (!waitingFor) {
|
||||
assert_unreached('Not expecting event, but got: ' + evt.type +
|
||||
' targeting element #' + evt.target.getAttribute('id'));
|
||||
return;
|
||||
}
|
||||
if (evt.type != waitingFor.types[0]) {
|
||||
assert_unreached('Expected ' + waitingFor.types[0] + ' event but got ' +
|
||||
evt.type + ' event');
|
||||
return;
|
||||
}
|
||||
if (waitingFor.types.length > 1) {
|
||||
// Pop first event from array
|
||||
waitingFor.types.shift();
|
||||
return;
|
||||
}
|
||||
// We need to null out waitingFor before calling the resolve function since
|
||||
// the Promise's resolve handlers may call waitForEvent() which will need
|
||||
// to set waitingFor.
|
||||
var resolveFunc = waitingFor.resolve;
|
||||
waitingFor = null;
|
||||
resolveFunc(evt);
|
||||
}
|
||||
|
||||
for (var i = 0; i < eventTypes.length; i++) {
|
||||
watchedNode.addEventListener(eventTypes[i], eventHandler);
|
||||
}
|
||||
|
||||
this.waitForEvent = function(type) {
|
||||
if (typeof type != 'string') {
|
||||
return Promise.reject('Event type not a string');
|
||||
}
|
||||
return this.waitForEvents([type]);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is useful when two events are expected to fire one immediately after
|
||||
* the other. This happens when we skip over the entire active interval for
|
||||
* instance. In this case an 'animationstart' and an 'animationend' are fired
|
||||
* and due to the asynchronous nature of Promise callbacks this won't work:
|
||||
*
|
||||
* eventWatcher.waitForEvent('animationstart').then(function() {
|
||||
* return waitForEvent('animationend');
|
||||
* }).then(...);
|
||||
*
|
||||
* It doesn't work because the 'animationend' listener is added too late,
|
||||
* because the resolve handler for the first Promise is called asynchronously
|
||||
* some time after the 'animationstart' event is called, rather than at the
|
||||
* time the event reaches the watched element.
|
||||
*/
|
||||
this.waitForEvents = function(types) {
|
||||
if (waitingFor) {
|
||||
return Promise.reject('Already waiting for an event');
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
waitingFor = {
|
||||
types: types,
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
this.stopWatching = function() {
|
||||
for (var i = 0; i < eventTypes.length; i++) {
|
||||
watchedNode.removeEventListener(eventTypes[i], eventHandler);
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// The terms used for the naming of the following helper functions refer to
|
||||
// terms used in the Web Animations specification for specific phases of an
|
||||
|
@ -300,7 +195,7 @@ test(function(t)
|
|||
|
||||
async_test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, 'transitionend');
|
||||
var eventWatcher = new EventWatcher(t, div, 'transitionend');
|
||||
|
||||
flushComputedStyle(div);
|
||||
div.style.marginLeft = '200px'; // initiate transition
|
||||
|
@ -317,11 +212,9 @@ async_test(function(t) {
|
|||
checkStateAtFiftyPctOfActiveInterval(animation);
|
||||
|
||||
animation.currentTime = currentTimeForEndOfActiveInterval();
|
||||
return eventWatcher.waitForEvent('transitionend');
|
||||
return eventWatcher.wait_for('transitionend');
|
||||
})).then(t.step_func(function() {
|
||||
checkStateAtActiveIntervalEndTime(animation);
|
||||
|
||||
eventWatcher.stopWatching();
|
||||
})).catch(t.step_func(function(reason) {
|
||||
assert_unreached(reason);
|
||||
})).then(function() {
|
||||
|
@ -332,7 +225,7 @@ async_test(function(t) {
|
|||
|
||||
test(function(t) {
|
||||
var div = addDiv(t, {'class': 'animated-div'});
|
||||
var eventWatcher = new EventWatcher(div, 'transitionend');
|
||||
var eventWatcher = new EventWatcher(t, div, 'transitionend');
|
||||
|
||||
flushComputedStyle(div);
|
||||
div.style.marginLeft = '200px'; // initiate transition
|
||||
|
@ -356,10 +249,9 @@ test(function(t) {
|
|||
//
|
||||
// Calling checkStateAtActiveIntervalStartTime will check computed style,
|
||||
// causing computed style to be updated and the 'transitionend' event to
|
||||
// be dispatched synchronously. We need to call waitForEvent first
|
||||
// be dispatched synchronously. We need to call wait_for first
|
||||
// otherwise eventWatcher will assert that the event was unexpected.
|
||||
eventWatcher.waitForEvent('transitionend').then(function() {
|
||||
eventWatcher.stopWatching();
|
||||
eventWatcher.wait_for('transitionend').then(function() {
|
||||
t.done();
|
||||
});
|
||||
checkStateAtActiveIntervalStartTime(animation);
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче